diff --git a/assets/images/icons/streak.svg b/assets/images/icons/streak.svg index 0f738a7d..64efbb2d 100644 --- a/assets/images/icons/streak.svg +++ b/assets/images/icons/streak.svg @@ -1,5 +1,4 @@ - - + + \ No newline at end of file diff --git a/assets/images/venues/solved ac.png b/assets/images/venues/solved ac.png new file mode 100644 index 00000000..611573e4 Binary files /dev/null and b/assets/images/venues/solved ac.png differ diff --git a/lib/components/styles/color.dart b/lib/components/styles/color.dart index 135ed3c8..f1e6e652 100644 --- a/lib/components/styles/color.dart +++ b/lib/components/styles/color.dart @@ -21,4 +21,202 @@ class MySolvedColor { static const Color divider = Color(0xFFD6D6D6); static const Color bottomNavigationBarUnselected = Color(0xFFC4C4C4); + + static Map> get streakTheme => { + 'default': [ + Color(0xDCDDDFFF), + Color(0xffa1e4ac), + Color(0xff78cb94), + Color(0xff4eb17c), + Color(0xff007950), + ], + 'color_red': [ + Color(0xDCDDDFFF), + Color(0xffFBB4B4), + Color(0xffEA8686), + Color(0xffDA5858), + Color(0xffC92A2A), + ], + 'color_wine': [ + Color(0xDCDDDFFF), + Color(0xffF4B0C8), + Color(0xffDA7F9F), + Color(0xffC04F76), + Color(0xffA61E4D), + ], + 'color_purple': [ + Color(0xDCDDDFFF), + Color(0xffE2ADF1), + Color(0xffC383D5), + Color(0xffA558B8), + Color(0xff862E9C), + ], + 'color_violet': [ + Color(0xDCDDDFFF), + Color(0xffBFADF8), + Color(0xff9F88E7), + Color(0xff7F62D5), + Color(0xff5F3DC4), + ], + 'color_indigo': [ + Color(0xDCDDDFFF), + Color(0xffACBBFA), + Color(0xff8597E9), + Color(0xff5D73D8), + Color(0xff364FC7), + ], + 'color_blue': [ + Color(0xDCDDDFFF), + Color(0xff97CAF5), + Color(0xff6DA8DC), + Color(0xff4286C4), + Color(0xff1864AB), + ], + 'color_cyan': [ + Color(0xDCDDDFFF), + Color(0xff8FD9E5), + Color(0xff63B7C5), + Color(0xff3794A5), + Color(0xff0B7285), + ], + 'color_teal': [ + Color(0xDCDDDFFF), + Color(0xff8EE1CA), + Color(0xff61C0A5), + Color(0xff35A080), + Color(0xff087F5B), + ], + 'color_green': [ + Color(0xDCDDDFFF), + Color(0xffA6E4B1), + Color(0xff7DC68B), + Color(0xff54A864), + Color(0xff2B8A3E), + ], + 'color_lime': [ + Color(0xDCDDDFFF), + Color(0xffC7E996), + Color(0xffA3CD68), + Color(0xff80B03B), + Color(0xff5C940D), + ], + 'color_yellow': [ + Color(0xDCDDDFFF), + Color(0xffF9DF8C), + Color(0xffF3BC5D), + Color(0xffEC9A2F), + Color(0xffE67700), + ], + 'color_orange': [ + Color(0xDCDDDFFF), + Color(0xffFBC694), + Color(0xffF09C68), + Color(0xffE4723B), + Color(0xffD9480F), + ], + 'tier_bronze': [ + Color(0xDCDDDFFF), + Color(0xffDDBEA0), + Color(0xffCD9B6B), + Color(0xffBD7935), + Color(0xffAD5600), + ], + 'tier_silver': [ + Color(0xDCDDDFFF), + Color(0xffB6C1CB), + Color(0xff90A0B0), + Color(0xff698095), + Color(0xff435F7A), + ], + 'tier_gold': [ + Color(0xDCDDDFFF), + Color(0xffF3D6A0), + Color(0xffF1C26B), + Color(0xffEEAE35), + Color(0xffD38200), + ], + 'tier_platinum': [ + Color(0xDCDDDFFF), + Color(0xffACF0DA), + Color(0xff80EBC8), + Color(0xff52DBA9), + Color(0xff23C188), + ], + 'tier_diamond': [ + Color(0xDCDDDFFF), + Color(0xff9EDFFA), + Color(0xff69D1FB), + Color(0xff35C1ED), + Color(0xff00A5D8), + ], + 'tier_ruby': [ + Color(0xDCDDDFFF), + Color(0xffFA9FC2), + Color(0xffFC6AA2), + Color(0xffFD3582), + Color(0xffDB0059), + ], + 'tier_master': [ + Color(0xDCDDDFFF), + Color(0xff7DF7FF), + Color(0xff95CAFF), + Color(0xffC38DEE), + Color(0xffFD7DAB), + ], + 'special_hanbyeol': [ + Color(0xDCDDDFFF), + Color(0xffFFD459), + Color(0xffFFAA69), + Color(0xffFF7C79), + Color(0xffFF5F84), + ], + 'color_solvedac': [ + Color(0xDCDDDFFF), + Color(0xffA7E9B4), + Color(0xff77E08B), + Color(0xff46CC5C), + Color(0xff15AF2F), + ], + 'color_baekjoon': [ + Color(0xDCDDDFFF), + Color(0xff8CD2EA), + Color(0xff5FB4DE), + Color(0xff3197D3), + Color(0xff0479C7), + ], + }; + + static Map get tier => { + 0: Color(0xff2d2d2d), + 1: Color(0xff9d4900), + 2: Color(0xffa54f00), + 3: Color(0xffad5600), + 4: Color(0xffb55d0a), + 5: Color(0xffc67739), + 6: Color(0xff38546e), + 7: Color(0xff3d5a74), + 8: Color(0xff435f7a), + 9: Color(0xff496580), + 10: Color(0xff4e6a86), + 11: Color(0xffd28500), + 12: Color(0xffdf8f00), + 13: Color(0xffec9a00), + 14: Color(0xfff9a518), + 15: Color(0xffffb028), + 16: Color(0xff00c78b), + 17: Color(0xff00d497), + 18: Color(0xff27e2a4), + 19: Color(0xff3ef0b1), + 20: Color(0xff51fdbd), + 21: Color(0xff009ee5), + 22: Color(0xff00a9f0), + 23: Color(0xff00b4fc), + 24: Color(0xff2bbfff), + 25: Color(0xff41caff), + 26: Color(0xffe0004c), + 27: Color(0xffea0053), + 28: Color(0xfff5005a), + 29: Color(0xffff0062), + 30: Color(0xffff3071), + }; } diff --git a/lib/extensions/color_extension.dart b/lib/extensions/color_extension.dart deleted file mode 100644 index dc2ce133..00000000 --- a/lib/extensions/color_extension.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -extension CupertinoThemeDataExtension on CupertinoThemeData { - Color get fontGray => const Color(0xFF767676); - - Color get backgroundGray => const Color(0xFFEFEFF0); - - Color get main => const Color(0xFF11CE3C); - - Color get dividerGray => const Color(0xFFD9D9D9); - - Map> get streakTheme => { - 'default': [ - Color(0xDCDDDFFF), - Color(0xffa1e4ac), - Color(0xff78cb94), - Color(0xff4eb17c), - Color(0xff007950), - ], - 'color_red': [ - Color(0xDCDDDFFF), - Color(0xffFBB4B4), - Color(0xffEA8686), - Color(0xffDA5858), - Color(0xffC92A2A), - ], - 'color_wine': [ - Color(0xDCDDDFFF), - Color(0xffF4B0C8), - Color(0xffDA7F9F), - Color(0xffC04F76), - Color(0xffA61E4D), - ], - 'color_purple': [ - Color(0xDCDDDFFF), - Color(0xffE2ADF1), - Color(0xffC383D5), - Color(0xffA558B8), - Color(0xff862E9C), - ], - 'color_violet': [ - Color(0xDCDDDFFF), - Color(0xffBFADF8), - Color(0xff9F88E7), - Color(0xff7F62D5), - Color(0xff5F3DC4), - ], - 'color_indigo': [ - Color(0xDCDDDFFF), - Color(0xffACBBFA), - Color(0xff8597E9), - Color(0xff5D73D8), - Color(0xff364FC7), - ], - 'color_blue': [ - Color(0xDCDDDFFF), - Color(0xff97CAF5), - Color(0xff6DA8DC), - Color(0xff4286C4), - Color(0xff1864AB), - ], - 'color_cyan': [ - Color(0xDCDDDFFF), - Color(0xff8FD9E5), - Color(0xff63B7C5), - Color(0xff3794A5), - Color(0xff0B7285), - ], - 'color_teal': [ - Color(0xDCDDDFFF), - Color(0xff8EE1CA), - Color(0xff61C0A5), - Color(0xff35A080), - Color(0xff087F5B), - ], - 'color_green': [ - Color(0xDCDDDFFF), - Color(0xffA6E4B1), - Color(0xff7DC68B), - Color(0xff54A864), - Color(0xff2B8A3E), - ], - 'color_lime': [ - Color(0xDCDDDFFF), - Color(0xffC7E996), - Color(0xffA3CD68), - Color(0xff80B03B), - Color(0xff5C940D), - ], - 'color_yellow': [ - Color(0xDCDDDFFF), - Color(0xffF9DF8C), - Color(0xffF3BC5D), - Color(0xffEC9A2F), - Color(0xffE67700), - ], - 'color_orange': [ - Color(0xDCDDDFFF), - Color(0xffFBC694), - Color(0xffF09C68), - Color(0xffE4723B), - Color(0xffD9480F), - ], - 'tier_bronze': [ - Color(0xDCDDDFFF), - Color(0xffDDBEA0), - Color(0xffCD9B6B), - Color(0xffBD7935), - Color(0xffAD5600), - ], - 'tier_silver': [ - Color(0xDCDDDFFF), - Color(0xffB6C1CB), - Color(0xff90A0B0), - Color(0xff698095), - Color(0xff435F7A), - ], - 'tier_gold': [ - Color(0xDCDDDFFF), - Color(0xffF3D6A0), - Color(0xffF1C26B), - Color(0xffEEAE35), - Color(0xffD38200), - ], - 'tier_platinum': [ - Color(0xDCDDDFFF), - Color(0xffACF0DA), - Color(0xff80EBC8), - Color(0xff52DBA9), - Color(0xff23C188), - ], - 'tier_diamond': [ - Color(0xDCDDDFFF), - Color(0xff9EDFFA), - Color(0xff69D1FB), - Color(0xff35C1ED), - Color(0xff00A5D8), - ], - 'tier_ruby': [ - Color(0xDCDDDFFF), - Color(0xffFA9FC2), - Color(0xffFC6AA2), - Color(0xffFD3582), - Color(0xffDB0059), - ], - 'tier_master': [ - Color(0xDCDDDFFF), - Color(0xff7DF7FF), - Color(0xff95CAFF), - Color(0xffC38DEE), - Color(0xffFD7DAB), - ], - 'special_hanbyeol': [ - Color(0xDCDDDFFF), - Color(0xffFFD459), - Color(0xffFFAA69), - Color(0xffFF7C79), - Color(0xffFF5F84), - ], - 'color_solvedac': [ - Color(0xDCDDDFFF), - Color(0xffA7E9B4), - Color(0xff77E08B), - Color(0xff46CC5C), - Color(0xff15AF2F), - ], - 'color_baekjoon': [ - Color(0xDCDDDFFF), - Color(0xff8CD2EA), - Color(0xff5FB4DE), - Color(0xff3197D3), - Color(0xff0479C7), - ], - }; - - Map get tier => { - 0: Color(0xff2d2d2d), - 1: Color(0xff9d4900), - 2: Color(0xffa54f00), - 3: Color(0xffad5600), - 4: Color(0xffb55d0a), - 5: Color(0xffc67739), - 6: Color(0xff38546e), - 7: Color(0xff3d5a74), - 8: Color(0xff435f7a), - 9: Color(0xff496580), - 10: Color(0xff4e6a86), - 11: Color(0xffd28500), - 12: Color(0xffdf8f00), - 13: Color(0xffec9a00), - 14: Color(0xfff9a518), - 15: Color(0xffffb028), - 16: Color(0xff00c78b), - 17: Color(0xff00d497), - 18: Color(0xff27e2a4), - 19: Color(0xff3ef0b1), - 20: Color(0xff51fdbd), - 21: Color(0xff009ee5), - 22: Color(0xff00a9f0), - 23: Color(0xff00b4fc), - 24: Color(0xff2bbfff), - 25: Color(0xff41caff), - 26: Color(0xffe0004c), - 27: Color(0xffea0053), - 28: Color(0xfff5005a), - 29: Color(0xffff0062), - 30: Color(0xffff3071), - }; -} diff --git a/lib/features/home/bloc/home_bloc.dart b/lib/features/home/bloc/home_bloc.dart index cce84611..f09dc535 100644 --- a/lib/features/home/bloc/home_bloc.dart +++ b/lib/features/home/bloc/home_bloc.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:meta/meta.dart'; import 'package:shared_preferences_repository/shared_preferences_repository.dart'; import 'package:solved_api/solved_api.dart'; +import 'package:timezone/timezone.dart' as tz; import 'package:user_repository/user_repository.dart'; part "home_event.dart"; @@ -40,13 +41,34 @@ class HomeBloc extends Bloc { final user = await _userRepository.getUser(_handle); final background = await _userRepository.getBackground(user.backgroundId); final organizations = await _userRepository.getOrganizations(_handle); - final isOnIllustBackground = await _sharedPreferencesRepository.getIsOnIllustBackground(); + final isOnIllustBackground = + await _sharedPreferencesRepository.getIsOnIllustBackground(); Badge? badge; if (user.badgeId != null) { badge = await _userRepository.getBadge(user.badgeId!); } final badges = await _userRepository.getBadges(_handle); + final streak = await _userRepository.getStreak(_handle, "default"); + streak.grass.sort((a, b) { + if (a.year != b.year) { + return a.year.compareTo(b.year); + } else if (a.month != b.month) { + return a.month.compareTo(b.month); + } else { + return a.day.compareTo(b.day); + } + }); + + tz.TZDateTime? today = + tz.TZDateTime.now(tz.UTC).add(const Duration(hours: 3)); + + late bool solvedToday = today.year == streak.grass.last.year && + today.month == streak.grass.last.month && + today.day == streak.grass.last.day; + + final tagRatings = await _userRepository.getTagRatings(_handle); + final problemStats = await _userRepository.getProblemStats(_handle); emit(state.copyWith( isOnIllustBackground: isOnIllustBackground, @@ -56,6 +78,9 @@ class HomeBloc extends Bloc { organizations: organizations, badge: badge, badges: badges, + solvedToday: solvedToday, + tagRatings: tagRatings, + problemStats: problemStats, )); } catch (e) { emit(state.copyWith(status: HomeStatus.failure)); diff --git a/lib/features/home/bloc/home_state.dart b/lib/features/home/bloc/home_state.dart index 64559c48..3cb1567a 100644 --- a/lib/features/home/bloc/home_state.dart +++ b/lib/features/home/bloc/home_state.dart @@ -4,8 +4,11 @@ enum HomeStatus { initial, loading, success, failure } extension HomeStatusX on HomeStatus { bool get isInitial => this == HomeStatus.initial; + bool get isLoading => this == HomeStatus.loading; + bool get isSuccess => this == HomeStatus.success; + bool get isFailure => this == HomeStatus.failure; } @@ -18,6 +21,9 @@ class HomeState extends Equatable { final List organizations; final Badge? badge; final List badges; + final bool? solvedToday; + final List? tagRatings; + final List? problemStats; const HomeState({ this.status = HomeStatus.initial, @@ -28,6 +34,9 @@ class HomeState extends Equatable { required this.organizations, this.badge, required this.badges, + this.solvedToday, + this.tagRatings, + this.problemStats, }); HomeState copyWith({ @@ -39,17 +48,22 @@ class HomeState extends Equatable { List? organizations, Badge? badge, List? badges, + bool? solvedToday, + List? tagRatings, + List? problemStats, }) { return HomeState( status: status ?? this.status, handle: handle ?? this.handle, - isOnIllustBackground: - isOnIllustBackground ?? this.isOnIllustBackground, + isOnIllustBackground: isOnIllustBackground ?? this.isOnIllustBackground, user: user ?? this.user, background: background ?? this.background, organizations: organizations ?? this.organizations, badge: badge ?? this.badge, badges: badges ?? this.badges, + solvedToday: solvedToday ?? this.solvedToday, + tagRatings: tagRatings ?? this.tagRatings, + problemStats: problemStats ?? this.problemStats, ); } @@ -57,10 +71,14 @@ class HomeState extends Equatable { List get props => [ status, handle, - isOnIllustBackground, + isOnIllustBackground, user, background, organizations, badge, + badges, + solvedToday, + tagRatings, + problemStats, ]; } diff --git a/lib/features/home/screen/home_screen.dart b/lib/features/home/screen/home_screen.dart index d8980386..67bb3d32 100644 --- a/lib/features/home/screen/home_screen.dart +++ b/lib/features/home/screen/home_screen.dart @@ -1,12 +1,18 @@ +import 'dart:math'; + import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_radar_chart/flutter_radar_chart.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:my_solved/components/styles/color.dart'; import 'package:my_solved/components/styles/font.dart'; import 'package:my_solved/features/home/bloc/home_bloc.dart'; +import 'package:pie_chart/pie_chart.dart'; import 'package:solved_api/solved_api.dart' as solved_api; +import 'package:solved_api/solved_api.dart'; import 'package:url_launcher/url_launcher_string.dart'; class HomeScreen extends StatelessWidget { @@ -22,6 +28,44 @@ class HomeScreen extends StatelessWidget { scrolledUnderElevation: 0, backgroundColor: MySolvedColor.secondaryBackground, actions: [ + SizedBox(width: 16), + OutlinedButton( + style: OutlinedButton.styleFrom( + side: BorderSide( + color: Colors.grey[300]!, + width: 1, + ), + backgroundColor: MySolvedColor.background, + padding: EdgeInsets.symmetric(horizontal: 8), + ), + onPressed: () async {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ClipOval( + child: ExtendedImage.network( + context.read().state.user?.profileImageUrl ?? + "https://static.solved.ac/misc/360x360/default_profile.png", + height: 24, + ), + ), + const SizedBox(width: 4), + Text( + context.read().state.handle, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w600, + fontFamily: MySolvedFont.pretendard, + color: MySolvedColor.font, + ), + ), + ], + )), + Spacer(), + ExtendedImage.asset( + "assets/images/venues/solved ac.png", + height: 40, + ), IconButton( onPressed: () => scaffoldKey.currentState?.openEndDrawer(), icon: Icon(Icons.menu), @@ -74,56 +118,100 @@ class _HomeViewState extends State { }, builder: (context, state) { if (state.status.isSuccess && state.user != null) { + final slideItem = [ + [ + "문제 해결", + state.user!.solvedCount.toString(), + "https://solved.ac/profile/${state.handle}/solved" + ], + [ + "기여", + state.user!.voteCount.toString(), + "https://solved.ac/profile/${state.handle}/votes" + ], + [ + "라이벌", + state.user!.reverseRivalCount.toString(), + "https://solved.ac/ranking/reverse_rival" + ], + ]; + + final solvedToday = state.solvedToday ?? false; + final tags = state.tagRatings ?? []; + final stats = state.problemStats ?? []; final gridItem = [ GridItem( - title: "문제 해결", - value: state.user!.solvedCount.toString(), - unit: "개", - onLongPress: () async { - String urlString = - "https://solved.ac/profile/${state.handle}/solved"; - launchUrlString(urlString); - }, - ), - GridItem( - title: "문제 기여", - value: state.user!.voteCount.toString(), - unit: "개", - onLongPress: () async { - String urlString = - "https://solved.ac/profile/${state.handle}/votes"; - launchUrlString(urlString); - }, - ), - GridItem( - title: "라이벌", - value: state.user!.reverseRivalCount.toString(), - unit: "명", - onLongPress: () async { - String urlString = "https://solved.ac/ranking/reverse_rival"; - launchUrlString(urlString); - }, + title: "레이팅", + value: "${_tierText(state.user!.tier)} ${state.user!.rating}", + onPressed: () {}, + backgroundColor: _ratingColor(state.user!.rating), + foregroundColor: MySolvedColor.background, ), GridItem( - title: "스트릭(최대)", + title: "스트릭", value: state.user!.maxStreak.toString(), unit: "일", + onPressed: () async { + Fluttertoast.showToast( + msg: solvedToday + ? "오늘은 이미 문제를 풀었어요" + : "오늘은 아직 문제를 풀지 않았어요", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: MySolvedColor.main.withOpacity(0.8), + textColor: Colors.white, + fontSize: 16.0); + }, foregroundColor: MySolvedColor.background, backgroundColor: MySolvedColor.main, - onPressed: () {}), + widget: Align( + alignment: Alignment.bottomLeft, + child: SvgPicture.asset( + width: 40, + "assets/images/icons/streak.svg", + colorFilter: ColorFilter.mode( + solvedToday ? Color(0xFF75D48A) : MySolvedColor.main, + BlendMode.srcIn)), + ), + icon: solvedToday + ? Icon(Icons.check_circle_outline, + color: Colors.white, size: 20) + : null), GridItem( - title: "레이팅", - value: state.user!.rank.toString(), - unit: "#", - isPrefixUnit: true, - backgroundColor: MySolvedColor.disabledButtonBackground, + title: "클래스", onPressed: () {}, + foregroundColor: MySolvedColor.background, + backgroundColor: _classColor(state.user!.userClass), + widget: SvgPicture.asset( + width: 60, + height: 60, + "assets/images/classes/c${state.user!.userClass}.svg", + ), ), GridItem( title: "뱃지", - value: state.badges.length.toString(), - unit: "개", onPressed: () {}, + widget: ExtendedImage.network( + state.badge!.badgeImageUrl, + width: 60, + height: 60, + ), + ), + GridItem( + onPressed: () {}, + title: "배경", + backgroundImageUrl: state.background?.backgroundImageUrl, + ), + GridItem( + onPressed: () {}, + title: "난이도 분포", + widget: _pieChart(stats: stats), + ), + GridItem( + onPressed: () {}, + title: "태그 분포", + widget: _tagChart(tags: tags, rating: state.user!.rating), ), ]; return CustomScrollView( @@ -131,32 +219,24 @@ class _HomeViewState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - _profileAndBackgroundImage( - profileImageURL: state.user!.profileImageUrl ?? - "https://static.solved.ac/misc/360x360/default_profile.png", - isShowIllustBackground: state.isOnIllustBackground, - background: state.background, - ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ - SizedBox(height: 16), - _handleAndBio( - handle: state.handle, - bio: state.user!.bio, + _backgroundImage( + isShowIllustBackground: state.isOnIllustBackground, + background: state.background, + ), + const SizedBox(height: 16), + Divider( + height: 8, + color: Colors.grey, ), - SizedBox(height: 16), - _tierAndBadgeAndClass( - tier: state.user!.tier, - rating: state.user!.rating, - badge: state.badge, - userClass: state.user!.userClass, - classDecoration: state.user!.classDecoration, + _solvedVotesRivals(slideItem: slideItem), + Divider( + height: 8, + color: Colors.grey, ), - SizedBox(height: 16), - if (state.organizations.isNotEmpty) - _organizations(organizations: state.organizations), ], ), ), @@ -172,17 +252,17 @@ class _HomeViewState extends State { childCount: gridItem.length, ), gridDelegate: SliverQuiltedGridDelegate( - crossAxisCount: 3, + crossAxisCount: 6, mainAxisSpacing: 16, crossAxisSpacing: 16, pattern: [ - QuiltedGridTile(1, 1), - QuiltedGridTile(1, 1), - QuiltedGridTile(1, 1), - QuiltedGridTile(1, 1), - QuiltedGridTile(1, 2), - QuiltedGridTile(1, 2), - QuiltedGridTile(1, 1), + QuiltedGridTile(2, 4), + QuiltedGridTile(2, 2), + QuiltedGridTile(2, 2), + QuiltedGridTile(2, 2), + QuiltedGridTile(2, 2), + QuiltedGridTile(3, 3), + QuiltedGridTile(3, 3), ], ), ), @@ -200,8 +280,7 @@ class _HomeViewState extends State { ); } - Widget _profileAndBackgroundImage({ - required String profileImageURL, + Widget _backgroundImage({ required bool isShowIllustBackground, required solved_api.Background? background, }) { @@ -211,204 +290,228 @@ class _HomeViewState extends State { final url = isShowIllustBackground && illustBackgroundImageUrl != null ? illustBackgroundImageUrl : backgroundImageUrl; - return Stack( - alignment: Alignment.bottomLeft, - children: [ - Column( - children: [ - Container( - width: double.infinity, - height: 160, - color: MySolvedColor.background, - child: ExtendedImage.network(url), - ), - SizedBox(height: 50), - ], - ), - Row( - children: [ - SizedBox(width: 16), - SizedBox( - width: 80, - height: 80, - child: ClipOval( - child: ExtendedImage.network(profileImageURL), - ), - ), - ], - ), - ], + return IconButton( + onPressed: () {}, + padding: EdgeInsets.zero, + icon: Container( + width: double.infinity, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(16), + ), + child: ExtendedImage.network(url)), ); } - Widget _handleAndBio({required String handle, required String? bio}) { - return Container( - width: double.infinity, - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: MySolvedColor.background, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _solvedVotesRivals({required List> slideItem}) { + return SizedBox( + height: 70, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Text( - handle, - style: MySolvedTextStyle.title3, - ), - if (bio != null) - Column( - children: [ - SizedBox(height: 4), - Text( - bio, - style: MySolvedTextStyle.body2.copyWith( - color: MySolvedColor.secondaryFont, + for (int i = 1; i < 2 * slideItem.length; i++) + i.isOdd + ? Expanded( + child: OutlinedButton( + onPressed: () { + launchUrlString(slideItem[(i / 2).floor()][2]); + }, + style: OutlinedButton.styleFrom( + padding: EdgeInsets.zero, + side: BorderSide( + color: Colors.transparent, + width: 0, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.zero), + ), + child: Container( + width: 100, + clipBehavior: Clip.none, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + slideItem[(i / 2).floor()][0], + style: MySolvedTextStyle.body2, + ), + Text( + slideItem[(i / 2).floor()][1], + style: MySolvedTextStyle.title1, + ), + ], + ), + ))) + : VerticalDivider( + color: Colors.grey, + indent: 20, + endIndent: 20, ), - ), - ], - ), ], ), ); } - Widget _organizations( - {required List organizations}) { - return Container( - width: double.infinity, - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: MySolvedColor.background, - borderRadius: BorderRadius.circular(16), - ), - child: Wrap( - children: List.generate( - organizations.length, - (index) => TextButton( - onPressed: () async { - String urlString = - "https://solved.ac/ranking/o/${organizations[index].organizationId}"; - launchUrlString(urlString); - }, - style: TextButton.styleFrom( - minimumSize: Size.zero, - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - index != organizations.length - 1 - ? "${organizations[index].name}, " - : organizations[index].name, - style: MySolvedTextStyle.body2.copyWith( - color: MySolvedColor.secondaryFont, - ), - ), + Widget _pieChart({required List stats}) { + Map dataMap = {}; + for (var stat in stats) { + dataMap[stat.level.toString()] = stat.solved.toDouble(); + } + + stats.fold(0, (sum, stat) => sum + stat.solved); + List colorList = []; + for (var i = 0; i <= 30; i++) { + colorList.add(MySolvedColor.tier[i]!); + } + + return Padding( + padding: EdgeInsets.all(24), + child: PieChart( + dataMap: dataMap, + animationDuration: Duration(milliseconds: 800), + chartLegendSpacing: 32, + colorList: colorList, + initialAngleInDegree: 270, + chartType: ChartType.ring, + ringStrokeWidth: 40, + legendOptions: LegendOptions( + showLegendsInRow: false, + showLegends: false, ), - ), - ), - ); + chartValuesOptions: ChartValuesOptions( + showChartValueBackground: true, + showChartValues: false, + showChartValuesInPercentage: false, + showChartValuesOutside: false, + ), + )); } - Widget _tierAndBadgeAndClass( - {required int tier, - required int rating, - required solved_api.Badge? badge, - required int userClass, - required String? classDecoration}) { - String classText = userClass.toString(); - if (classDecoration == "silver") classText += "s"; - if (classDecoration == "gold") classText += "g"; - return Container( - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: MySolvedColor.background, - borderRadius: BorderRadius.circular(16), - ), - child: Row( - children: [ - rating < 3000 - ? Row( - children: [ - Text( - _tierText(tier), - style: MySolvedTextStyle.title5.copyWith( - color: _ratingColor(rating), - ), - ), - SizedBox(width: 4), - Text( - rating.toString(), - style: MySolvedTextStyle.body1.copyWith( - color: _ratingColor(rating), - ), - ), - ], - ) - : ShaderMask( - shaderCallback: (rect) { - return LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFF7cf9ff), - Color(0xFFb491ff), - Color(0xFFff7ca8) - ], - ).createShader( - Rect.fromLTRB(0, 0, rect.width, rect.height)); - }, - blendMode: BlendMode.srcATop, - child: Row( - children: [ - Text( - "Master", - style: MySolvedTextStyle.title5, - ), - Text( - rating.toString(), - style: MySolvedTextStyle.body1, - ), - ], - ), - ), - Spacer(), - if (badge != null) - IconButton( - padding: EdgeInsets.zero, - onPressed: () async { - String urlString = "https://solved.ac/badges/${badge.badgeId}"; - launchUrlString(urlString); - }, - icon: ExtendedImage.network( - width: 40, - height: 40, - badge.badgeImageUrl, - ), - ), - IconButton( - padding: EdgeInsets.zero, - onPressed: () async { - String urlString = "https://solved.ac/class?class=$classText"; - launchUrlString(urlString); - }, - icon: SvgPicture.asset( - width: 40, - height: 40, - "assets/images/classes/c$classText.svg", - ), - ), - ], - ), + Widget _tagChart({required List tags, required int rating}) { + tags.sort((a, b) => b.rating.compareTo(a.rating)); + + List ticks = []; + List features = []; + List> data = [[]]; + + int? length = min(8, tags.length); + int maxTick = 0; + for (var i = 0; i < length; i++) { + // features.add(tags[i].tag.key); + features.add(""); + data[0].add(tags[i].rating); + maxTick = max(maxTick, data[0][i].toInt()); + } + maxTick = (maxTick + 500) ~/ 500 * 500; + while (maxTick > 0) { + ticks.add(maxTick); + maxTick -= 500; + } + + return RadarChart( + ticks: ticks.reversed.toList(), + features: features, + data: data, + outlineColor: Color(0xff8a8f95), + graphColors: [_ratingColor(rating)], ); } - Widget _gridItem({required GridItem item}) { - return OutlinedButton( + Color _ratingColor(int rating) { + if (rating < 30) return Color(0xFF2D2D2D); + if (rating < 200) return Color(0xffad5600); + if (rating < 800) return Color(0xFF425E79); + if (rating < 1600) return Color(0xffec9a00); + if (rating < 2200) return Color(0xff00c78b); + if (rating < 2700) return Color(0xff00b4fc); + if (rating < 3000) return Color(0xffff0062); + return Color(0xffb300e0); + } + + Color _classColor(int userClass) { + if (userClass == 1) return Color.fromARGB(255, 36, 156, 229); + if (userClass == 2) return Color.fromARGB(255, 32, 197, 233); + if (userClass == 3) return Color.fromARGB(255, 27, 223, 139); + if (userClass == 4) return Color.fromARGB(255, 43, 213, 33); + if (userClass == 5) return Color.fromARGB(255, 176, 219, 21); + if (userClass == 6) return Color.fromARGB(255, 235, 202, 15); + if (userClass == 7) return Color.fromARGB(255, 243, 180, 18); + if (userClass == 8) return Color.fromARGB(255, 255, 125, 0); + if (userClass == 9) return Color.fromARGB(255, 243, 27, 116); + if (userClass == 10) return Color.fromARGB(255, 167, 32, 232); + return Color.fromARGB(255, 79, 82, 87); + } + + String _tierText(int tier) { + if (tier == 1) return 'Bronze V'; + if (tier == 2) return 'Bronze IV'; + if (tier == 3) return 'Bronze III'; + if (tier == 4) return 'Bronze II'; + if (tier == 5) return 'Bronze I'; + if (tier == 6) return 'Silver V'; + if (tier == 7) return 'Silver IV'; + if (tier == 8) return 'Silver III'; + if (tier == 9) return 'Silver II'; + if (tier == 10) return 'Silver I'; + if (tier == 11) return 'Gold V'; + if (tier == 12) return 'Gold IV'; + if (tier == 13) return 'Gold III'; + if (tier == 14) return 'Gold II'; + if (tier == 15) return 'Gold I'; + if (tier == 16) return 'Platinum V'; + if (tier == 17) return 'Platinum IV'; + if (tier == 18) return 'Platinum III'; + if (tier == 19) return 'Platinum II'; + if (tier == 20) return 'Platinum I'; + if (tier == 21) return 'Diamond V'; + if (tier == 22) return 'Diamond IV'; + if (tier == 23) return 'Diamond III'; + if (tier == 24) return 'Diamond II'; + if (tier == 25) return 'Diamond I'; + if (tier == 26) return 'Ruby V'; + if (tier == 27) return 'Ruby IV'; + if (tier == 28) return 'Ruby III'; + if (tier == 29) return 'Ruby II'; + if (tier == 30) return 'Ruby I'; + if (tier == 31) return 'Master'; + return 'Unrated'; + } +} + +class GridItem { + String title; + String value; + String unit; + bool isPrefixUnit; + Color foregroundColor; + Color backgroundColor; + VoidCallback? onPressed; + String? backgroundImageUrl; + Widget? widget; + Widget? icon; + + GridItem({ + required this.title, + this.value = "", + this.unit = "", + this.isPrefixUnit = false, + this.foregroundColor = MySolvedColor.font, + this.backgroundColor = MySolvedColor.background, + this.onPressed, + this.backgroundImageUrl, + this.widget, + this.icon, + }); +} + +Widget _gridItem({required GridItem item}) { + if (item.backgroundImageUrl != null) { + return IconButton( + onPressed: item.onPressed, + padding: EdgeInsets.zero, style: OutlinedButton.styleFrom( - backgroundColor: item.backgroundColor, - padding: EdgeInsets.only(top: 20, left: 16, right: 16, bottom: 20), + backgroundColor: Colors.grey, side: BorderSide( color: MySolvedColor.background, width: 0, @@ -417,18 +520,63 @@ class _HomeViewState extends State { borderRadius: BorderRadius.circular(16), ), ), - onPressed: item.onPressed, - onLongPress: item.onLongPress, - child: Column( + icon: Stack(clipBehavior: Clip.none, children: [ + Container( + height: double.infinity, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(16), + ), + child: ExtendedImage.network( + item.backgroundImageUrl!, + fit: BoxFit.fitHeight, + opacity: AlwaysStoppedAnimation(0.6), + )), + Positioned( + top: 20, + left: 16, + child: Text( + item.title, + style: + MySolvedTextStyle.body1.copyWith(color: item.backgroundColor), + ), + ), + ])); + } + return OutlinedButton( + style: OutlinedButton.styleFrom( + backgroundColor: item.backgroundColor, + padding: EdgeInsets.only(top: 20, left: 16, right: 16, bottom: 20), + side: BorderSide( + color: MySolvedColor.background, + width: 0, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + onPressed: item.onPressed, + child: Stack(clipBehavior: Clip.none, children: [ + Center( + child: Padding( + padding: const EdgeInsets.only(top: 20), + child: item.widget ?? Container(), + ), + ), + Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - item.title, - style: MySolvedTextStyle.body2.copyWith( - color: item.foregroundColor, + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text( + item.title, + style: MySolvedTextStyle.body1.copyWith( + color: item.foregroundColor, + ), ), - ), + if (item.icon != null) item.icon!, + ]), Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, @@ -469,123 +617,6 @@ class _HomeViewState extends State { ], ), ], - )); - } - - Color _ratingColor(int rating) { - if (rating < 30) { - return Color(0xFF2D2D2D); - } else if (rating < 200) { - // bronze - return Color(0xffad5600); - } else if (rating < 800) { - // silver - return Color(0xFF425E79); - } else if (rating < 1600) { - // gold - return Color(0xffec9a00); - } else if (rating < 2200) { - // platinum - return Color(0xff00c78b); - } else if (rating < 2700) { - // diamond - return Color(0xff00b4fc); - } else if (rating < 3000) { - // ruby - return Color(0xffff0062); - } else { - // master - return Color(0xffb300e0); - } - } - - String _tierText(int tier) { - if (tier == 1) { - return 'Bronze V'; - } else if (tier == 2) { - return 'Bronze IV'; - } else if (tier == 3) { - return 'Bronze III'; - } else if (tier == 4) { - return 'Bronze II'; - } else if (tier == 5) { - return 'Bronze I'; - } else if (tier == 6) { - return 'Silver V'; - } else if (tier == 7) { - return 'Silver IV'; - } else if (tier == 8) { - return 'Silver III'; - } else if (tier == 9) { - return 'Silver II'; - } else if (tier == 10) { - return 'Silver I'; - } else if (tier == 11) { - return 'Gold V'; - } else if (tier == 12) { - return 'Gold IV'; - } else if (tier == 13) { - return 'Gold III'; - } else if (tier == 14) { - return 'Gold II'; - } else if (tier == 15) { - return 'Gold I'; - } else if (tier == 16) { - return 'Platinum V'; - } else if (tier == 17) { - return 'Platinum IV'; - } else if (tier == 18) { - return 'Platinum III'; - } else if (tier == 19) { - return 'Platinum II'; - } else if (tier == 20) { - return 'Platinum I'; - } else if (tier == 21) { - return 'Diamond V'; - } else if (tier == 22) { - return 'Diamond IV'; - } else if (tier == 23) { - return 'Diamond III'; - } else if (tier == 24) { - return 'Diamond II'; - } else if (tier == 25) { - return 'Diamond I'; - } else if (tier == 26) { - return 'Ruby V'; - } else if (tier == 27) { - return 'Ruby IV'; - } else if (tier == 28) { - return 'Ruby III'; - } else if (tier == 29) { - return 'Ruby II'; - } else if (tier == 30) { - return 'Ruby I'; - } else if (tier == 31) { - return 'Master'; - } else { - return 'Unrated'; - } - } -} - -class GridItem { - String title; - String value; - String unit; - bool isPrefixUnit; - Color foregroundColor; - Color backgroundColor; - VoidCallback? onPressed; - VoidCallback? onLongPress; - - GridItem({ - required this.title, - required this.value, - required this.unit, - this.isPrefixUnit = false, - this.foregroundColor = MySolvedColor.font, - this.backgroundColor = MySolvedColor.background, - this.onPressed, - this.onLongPress, - }); + ), + ])); } diff --git a/lib/models/badge.dart b/lib/models/badge.dart deleted file mode 100644 index 1ac5b87b..00000000 --- a/lib/models/badge.dart +++ /dev/null @@ -1,31 +0,0 @@ -class Badge { - final String badgeId; - final String badgeImageUrl; - final int unlockedUserCount; - final String displayName; - final String displayDescription; - final String badgeTier; - final String badgeCategory; - - const Badge({ - required this.badgeId, - required this.badgeImageUrl, - required this.unlockedUserCount, - required this.displayName, - required this.displayDescription, - required this.badgeTier, - required this.badgeCategory, - }); - - factory Badge.fromJson(Map json) { - return Badge( - badgeId: json['badgeId'], - badgeImageUrl: json['badgeImageUrl'], - unlockedUserCount: json['unlockedUserCount'], - displayName: json['displayName'], - displayDescription: json['displayDescription'], - badgeTier: json['badgeTier'], - badgeCategory: json['badgeCategory'], - ); - } -} diff --git a/lib/models/contest.dart b/lib/models/contest.dart deleted file mode 100644 index 86be2ba9..00000000 --- a/lib/models/contest.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:html/dom.dart'; - -class Contest { - final String? venue; - final String name; - final String? url; - final DateTime startTime; - final DateTime endTime; - - const Contest({ - required this.venue, - required this.name, - required this.url, - required this.startTime, - required this.endTime, - }); - - factory Contest.fromElement(Element element) { - String? url; - try { - url = element - .getElementsByTagName('td')[1] - .getElementsByTagName('a')[0] - .attributes['href']; - } catch (e) { - url = null; - } - List startTimeList = element - .getElementsByTagName('td')[2] - .text - .toString() - .replaceAll('년', '') - .replaceAll('월', '') - .replaceAll('일', '') - .split(' '); - DateTime startTime = DateTime.parse( - "${startTimeList[0].padLeft(4, "0")}-${startTimeList[1].padLeft(2, "0")}-${startTimeList[2].padLeft(2, "0")}T${startTimeList[3].padLeft(2, "0")}:00+09:00") - .toLocal(); - List endTimeList = element - .getElementsByTagName('td')[3] - .text - .toString() - .replaceAll('년', '') - .replaceAll('월', '') - .replaceAll('일', '') - .split(' '); - DateTime endTime = DateTime.parse( - "${endTimeList[0].padLeft(4, "0")}-${endTimeList[1].padLeft(2, "0")}-${endTimeList[2].padLeft(2, "0")}T${endTimeList[3].padLeft(2, "0")}:00+09:00") - .toLocal(); - - return Contest( - venue: element.getElementsByTagName('td')[0].text, - name: element.getElementsByTagName('td')[1].text, - url: url, - startTime: startTime, - endTime: endTime, - ); - } -} diff --git a/lib/models/problem.dart b/lib/models/problem.dart deleted file mode 100644 index 9eb73dc2..00000000 --- a/lib/models/problem.dart +++ /dev/null @@ -1,40 +0,0 @@ -class Problem { - final int problemId; - final String titleKo; - final bool isSolvable; - final bool isPartial; - final int acceptedUserCount; - final int level; - final int votedUserCount; - final bool isLevelLocked; - final num averageTries; - final List tags; - - const Problem({ - required this.problemId, - required this.titleKo, - required this.isSolvable, - required this.isPartial, - required this.acceptedUserCount, - required this.level, - required this.votedUserCount, - required this.isLevelLocked, - required this.averageTries, - required this.tags, - }); - - factory Problem.fromJson(Map json) { - return Problem( - problemId: json['problemId'], - titleKo: json['titleKo'], - isSolvable: json['isSolvable'], - isPartial: json['isPartial'], - acceptedUserCount: json['acceptedUserCount'], - level: json['level'], - votedUserCount: json['votedUserCount'], - isLevelLocked: json['isLevelLocked'], - averageTries: json['averageTries'], - tags: json['tags'], - ); - } -} diff --git a/lib/models/search/object.dart b/lib/models/search/object.dart deleted file mode 100644 index a06e0d53..00000000 --- a/lib/models/search/object.dart +++ /dev/null @@ -1,16 +0,0 @@ -class SearchObject { - final int count; - final List items; - - const SearchObject({ - required this.count, - required this.items, - }); - - factory SearchObject.fromJson(Map json) { - return SearchObject( - count: json['count'], - items: json['items'], - ); - } -} diff --git a/lib/models/search/suggestion.dart b/lib/models/search/suggestion.dart deleted file mode 100644 index fe8842b5..00000000 --- a/lib/models/search/suggestion.dart +++ /dev/null @@ -1,31 +0,0 @@ -class SearchSuggestion { - final List autocomplete; - final List problems; - final int problemCount; - final List users; - final int userCount; - final List tags; - final int tagCount; - - const SearchSuggestion({ - required this.autocomplete, - required this.problems, - required this.problemCount, - required this.users, - required this.userCount, - required this.tags, - required this.tagCount, - }); - - factory SearchSuggestion.fromJson(Map json) { - return SearchSuggestion( - autocomplete: json['autocomplete'], - problems: json['problems'], - problemCount: json['problemCount'], - users: json['users'], - userCount: json['userCount'], - tags: json['tags'], - tagCount: json['tagCount'], - ); - } -} diff --git a/lib/models/site_stats.dart b/lib/models/site_stats.dart deleted file mode 100644 index 7f60404c..00000000 --- a/lib/models/site_stats.dart +++ /dev/null @@ -1,25 +0,0 @@ -class SiteStats { - final int problemCount; - final int problemVotedCount; - final int userCount; - final int contributorCount; - final int contributionCount; - - const SiteStats({ - required this.problemCount, - required this.problemVotedCount, - required this.userCount, - required this.contributorCount, - required this.contributionCount, - }); - - factory SiteStats.fromJson(Map json) { - return SiteStats( - problemCount: json['problemCount'], - problemVotedCount: json['problemVotedCount'], - userCount: json['userCount'], - contributorCount: json['contributorCount'], - contributionCount: json['contributionCount'], - ); - } -} diff --git a/lib/models/streak_date.dart b/lib/models/streak_date.dart deleted file mode 100644 index 0cd674a5..00000000 --- a/lib/models/streak_date.dart +++ /dev/null @@ -1,22 +0,0 @@ -class StreakDate { - final int day; - final int month; - final int year; - final int weekDay; - final int solvedCount; - final bool isSolved; - final bool isFuture; - final bool isFrozen; - final bool isRepaired; - - const StreakDate( - {required this.day, - required this.month, - required this.year, - required this.weekDay, - required this.solvedCount, - required this.isSolved, - required this.isFuture, - required this.isFrozen, - required this.isRepaired}); -} diff --git a/lib/models/tag.dart b/lib/models/tag.dart deleted file mode 100644 index f2deb8c8..00000000 --- a/lib/models/tag.dart +++ /dev/null @@ -1,22 +0,0 @@ -class ProblemTag { - final String key; - final bool isMeta; - final int bojTagId; - final int problemCount; - - const ProblemTag({ - required this.key, - required this.isMeta, - required this.bojTagId, - required this.problemCount, - }); - - factory ProblemTag.fromJson(Map json) { - return ProblemTag( - key: json['key'], - isMeta: json['isMeta'], - bojTagId: json['bojTagId'], - problemCount: json['problemCount'], - ); - } -} diff --git a/lib/models/user.dart b/lib/models/user.dart deleted file mode 100644 index 22127f4d..00000000 --- a/lib/models/user.dart +++ /dev/null @@ -1,82 +0,0 @@ -class User { - final String? handle; - final String? bio; - final String? badgeId; - final String? backgroundId; - final String? profileImageUrl; - final int solvedCount; - final int voteCount; - final int userClass; - final String? classDecoration; - final int rivalCount; - final int reverseRivalCount; - final int tier; - final int rating; - final int ratingByProblemsSum; - final int ratingByClass; - final int ratingBySolvedCount; - final int ratingByVoteCount; - final int maxStreak; - final int coins; - final int stardusts; - final String? joinedAt; - final String? bannedUntil; - final String? proUntil; - final int rank; - - const User({ - required this.handle, - required this.bio, - required this.badgeId, - required this.backgroundId, - required this.profileImageUrl, - required this.solvedCount, - required this.voteCount, - required this.userClass, - required this.classDecoration, - required this.tier, - required this.rating, - required this.ratingByProblemsSum, - required this.ratingByClass, - required this.ratingBySolvedCount, - required this.ratingByVoteCount, - required this.rivalCount, - required this.reverseRivalCount, - required this.maxStreak, - required this.coins, - required this.stardusts, - required this.joinedAt, - required this.bannedUntil, - required this.proUntil, - required this.rank, - }); - - factory User.fromJson(Map json) { - return User( - handle: json['handle'], - bio: json['bio'], - badgeId: json['badgeId'], - backgroundId: json['backgroundId'], - profileImageUrl: json['profileImageUrl'], - solvedCount: json['solvedCount'], - voteCount: json['voteCount'], - userClass: json['class'], - classDecoration: json['classDecoration'], - tier: json['tier'], - rating: json['rating'], - ratingByProblemsSum: json['ratingByProblemsSum'], - ratingByClass: json['ratingByClass'], - ratingBySolvedCount: json['ratingBySolvedCount'], - ratingByVoteCount: json['ratingByVoteCount'], - rivalCount: json['rivalCount'], - reverseRivalCount: json['reverseRivalCount'], - maxStreak: json['maxStreak'], - coins: json['coins'], - stardusts: json['stardusts'], - joinedAt: json['joinedAt'], - bannedUntil: json['bannedUntil'], - proUntil: json['proUntil'], - rank: json['rank'], - ); - } -} diff --git a/lib/models/user/background.dart b/lib/models/user/background.dart deleted file mode 100644 index e30be6a7..00000000 --- a/lib/models/user/background.dart +++ /dev/null @@ -1,40 +0,0 @@ -class Background { - final String backgroundId; - final String backgroundImageUrl; - final String? fallbackBackgroundImageUrl; - final String? backgroundVideoUrl; - final int unlockedUserCount; - final String displayDescription; - final String? conditions; - final bool hiddenConditions; - final bool isIllust; - final String displayName; - - Background({ - required this.backgroundId, - required this.backgroundImageUrl, - required this.fallbackBackgroundImageUrl, - required this.backgroundVideoUrl, - required this.unlockedUserCount, - required this.displayName, - required this.displayDescription, - required this.conditions, - required this.hiddenConditions, - required this.isIllust, - }); - - factory Background.fromJson(Map json) { - return Background( - backgroundId: json['backgroundId'], - backgroundImageUrl: json['backgroundImageUrl'], - fallbackBackgroundImageUrl: json['fallbackBackgroundImageUrl'], - backgroundVideoUrl: json['backgroundVideoUrl'], - unlockedUserCount: json['unlockedUserCount'], - displayName: json['displayName'], - displayDescription: json['displayDescription'], - conditions: json['conditions'], - hiddenConditions: json['hiddenConditions'], - isIllust: json['isIllust'], - ); - } -} diff --git a/lib/models/user/badges.dart b/lib/models/user/badges.dart deleted file mode 100644 index 9c4a412d..00000000 --- a/lib/models/user/badges.dart +++ /dev/null @@ -1,16 +0,0 @@ -class Badges { - final int count; - final List items; - - const Badges({ - required this.count, - required this.items, - }); - - factory Badges.fromJson(Map json) { - return Badges( - count: json['count'], - items: json['items'], - ); - } -} diff --git a/lib/models/user/grass.dart b/lib/models/user/grass.dart deleted file mode 100644 index 86076b68..00000000 --- a/lib/models/user/grass.dart +++ /dev/null @@ -1,25 +0,0 @@ -class Grass { - final List grass; - final dynamic theme; - final int currentStreak; - final int longestStreak; - final String topic; - - const Grass({ - required this.grass, - required this.theme, - required this.currentStreak, - required this.longestStreak, - required this.topic, - }); - - factory Grass.fromJson(Map json) { - return Grass( - grass: json['grass'], - theme: json['theme'], - currentStreak: json['currentStreak'], - longestStreak: json['longestStreak'], - topic: json['topic'], - ); - } -} diff --git a/lib/models/user/organization.dart b/lib/models/user/organization.dart deleted file mode 100644 index 3f395f63..00000000 --- a/lib/models/user/organization.dart +++ /dev/null @@ -1,34 +0,0 @@ -class Organization { - final int organizationId; - final String name; - final String type; - final int rating; - final int userCount; - final int voteCount; - final int solvedCount; - final String color; - - Organization({ - required this.organizationId, - required this.name, - required this.type, - required this.rating, - required this.userCount, - required this.voteCount, - required this.solvedCount, - required this.color, - }); - - factory Organization.fromJson(Map json) { - return Organization( - organizationId: json['organizationId'], - name: json['name'], - type: json['type'], - rating: json['rating'], - userCount: json['userCount'], - voteCount: json['voteCount'], - solvedCount: json['solvedCount'], - color: json['color'], - ); - } -} diff --git a/lib/models/user/organizations.dart b/lib/models/user/organizations.dart deleted file mode 100644 index 79076005..00000000 --- a/lib/models/user/organizations.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'organization.dart'; - -class Organizations { - final List organizations; - - Organizations({ - required this.organizations, - }); - - factory Organizations.fromJson(List json) { - List organizations = []; - organizations = json.map((i) => Organization.fromJson(i)).toList(); - - return Organizations( - organizations: organizations, - ); - } -} diff --git a/lib/models/user/tag_ratings.dart b/lib/models/user/tag_ratings.dart deleted file mode 100644 index 3073cd08..00000000 --- a/lib/models/user/tag_ratings.dart +++ /dev/null @@ -1,32 +0,0 @@ -class TagRatings { - final dynamic tag; - final dynamic aliases; - final int solvedCount; - final int rating; - final int ratingByProblemsSum; - final int ratingByClass; - final int ratingBySolvedCount; - final int ratingProblemsCutoff; - - const TagRatings( - {required this.tag, - required this.aliases, - required this.solvedCount, - required this.rating, - required this.ratingByProblemsSum, - required this.ratingByClass, - required this.ratingBySolvedCount, - required this.ratingProblemsCutoff}); - - factory TagRatings.fromJson(Map json) { - return TagRatings( - tag: json['tag'], - aliases: json['aliases'], - solvedCount: json['solvedCount'], - rating: json['rating'], - ratingByProblemsSum: json['ratingByProblemsSum'], - ratingByClass: json['ratingByClass'], - ratingBySolvedCount: json['ratingBySolvedCount'], - ratingProblemsCutoff: json['ratingProblemsCutoff']); - } -} diff --git a/lib/models/user/top_100.dart b/lib/models/user/top_100.dart deleted file mode 100644 index 83eb27e8..00000000 --- a/lib/models/user/top_100.dart +++ /dev/null @@ -1,16 +0,0 @@ -class Top100 { - final int count; - final List items; - - const Top100({ - required this.count, - required this.items, - }); - - factory Top100.fromJson(Map json) { - return Top100( - count: json['count'], - items: json['items'], - ); - } -} diff --git a/lib/services/contest_service.dart b/lib/services/contest_service.dart deleted file mode 100644 index 95b63c38..00000000 --- a/lib/services/contest_service.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class ContestService extends ChangeNotifier { - static final ContestService _instance = ContestService._privateConstructor(); - - ContestService._privateConstructor() { - init(); - } - - bool _disposed = false; - - Map showVenues = { - 'AtCoder': false, - 'BOJ Open': false, - 'Codeforces': false, - 'Programmers': false, - 'Others': false, - }; - - factory ContestService() { - return _instance; - } - - @override - void dispose() { - _disposed = true; - super.dispose(); - } - - @override - notifyListeners() { - if (!_disposed) { - super.notifyListeners(); - } - } - - Future init() async { - await _initializeContest(); - notifyListeners(); - } - - Future _initializeContest() async { - initContestShow(); - notifyListeners(); - } - - Future initContestShow() async { - final prefs = await SharedPreferences.getInstance(); - for (var venue in showVenues.keys) { - final show = prefs.getBool('show$venue') ?? true; - showVenues[venue] = show; - } - notifyListeners(); - } - - Future> getContestShow() async { - return showVenues; - } - - Future toggleContestShow(String venue) async { - final prefs = await SharedPreferences.getInstance(); - final show = prefs.getBool('show$venue') ?? false; - showVenues[venue] = !show; - prefs.setBool('show$venue', !show); - notifyListeners(); - } -} diff --git a/lib/services/network_service.dart b/lib/services/network_service.dart deleted file mode 100644 index f7a2de49..00000000 --- a/lib/services/network_service.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:html/dom.dart' as dom; -import 'package:html/parser.dart' as parser; -import 'package:http/http.dart' as http; -import 'package:my_solved/models/badge.dart'; -import 'package:my_solved/models/search/object.dart'; -import 'package:my_solved/models/search/suggestion.dart'; -import 'package:my_solved/models/site_stats.dart'; -import 'package:my_solved/models/user.dart'; -import 'package:my_solved/models/user/background.dart'; -import 'package:my_solved/models/user/badges.dart'; -import 'package:my_solved/models/user/grass.dart'; -import 'package:my_solved/models/user/organizations.dart'; -import 'package:my_solved/models/user/tag_ratings.dart'; -import 'package:my_solved/models/user/top_100.dart'; -import 'package:my_solved/services/user_service.dart'; - -import '../models/contest.dart'; - -class NetworkService { - static final NetworkService _instance = NetworkService._privateConstructor(); - - NetworkService._privateConstructor(); - - factory NetworkService() { - return _instance; - } - - // Request for Home - Future requestUser(String handle) async { - final response = await http - .get(Uri.parse("https://solved.ac/api/v3/user/show?handle=$handle")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - User user = User.fromJson(jsonDecode(response.body)); - return user; - } else { - throw Exception('Failed to load'); - } - } - - Future requestOrganizations(String handle) async { - final response = await http.get(Uri.parse( - "https://solved.ac/api/v3/user/organizations?handle=$handle")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - return Organizations.fromJson(jsonDecode(response.body)); - } else { - throw Exception('Failed to load'); - } - } - - Future requestBackground(String backgroundId) async { - final response = await http.get(Uri.parse( - "https://solved.ac/api/v3/background/show?backgroundId=$backgroundId")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - return Background.fromJson(jsonDecode(response.body)); - } else { - throw Exception('Failed to load'); - } - } - - Future requestBadge(String badgeId) async { - final response = await http.get( - Uri.parse("https://solved.ac/api/v3/badge/show?badgeId=$badgeId"), - headers: {'x-solvedac-language': 'ko'}); - final statusCode = response.statusCode; - - if (statusCode == 200) { - return Badge.fromJson(jsonDecode(response.body)); - } else { - throw Exception('Failed to load'); - } - } - - Future requestBadges(String handle) async { - final response = await http.get( - Uri.parse( - "https://solved.ac/api/v3/user/available_badges?handle=$handle"), - headers: {'x-solvedac-language': 'ko'}); - final statusCode = response.statusCode; - - if (statusCode == 200) { - Badges badges = Badges.fromJson(jsonDecode(response.body)); - return badges; - } else { - throw Exception('Failed to load'); - } - } - - Future requestStreak(String handle) async { - final response = await http.get(Uri.parse( - "https://solved.ac/api/v3/user/grass?handle=$handle&topic=default")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - Grass streak = Grass.fromJson(jsonDecode(response.body)); - return streak; - } else { - throw Exception('Failed to load'); - } - } - - Future requestTop100(String handle) async { - final response = await http - .get(Uri.parse("https://solved.ac/api/v3/user/top_100?handle=$handle")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - Top100 top100 = Top100.fromJson(jsonDecode(response.body)); - return top100; - } else { - throw Exception('Failed to load'); - } - } - - Future> requestTagRatings(String handle) async { - final response = await http.get( - Uri.parse("https://solved.ac/api/v3/user/tag_ratings?handle=$handle")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - List tagRatings = - json.decode(response.body).map((json) { - return TagRatings.fromJson(json); - }).toList(); - return tagRatings; - } else { - throw Exception('Failed to load'); - } - } - - // Request for Search - Future requestSearchSuggestion(String query) async { - final response = await http.get( - Uri.parse("https://solved.ac/api/v3/search/suggestion?query=$query")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - SearchSuggestion search = - SearchSuggestion.fromJson(jsonDecode(response.body)); - return search; - } else { - throw Exception('Fail to load'); - } - } - - // 문제 검색 - Future requestSearchProblem( - String query, int? page, String? sort, String? direction) async { - String url = "https://solved.ac/api/v3/search/problem?query=$query"; - if (page != null) { - url += "&page=$page"; - } - if (sort != null) { - url += "&sort=$sort"; - } - if (direction != null) { - url += "&direction=$direction"; - } - - final response = await http.get(Uri.parse(url - .replaceAll('\$me', UserService().name) - .replaceAll(' ', '%20') - .replaceAll('#', '%23') - .replaceAll('@', '%40'))); - final statusCode = response.statusCode; - - if (statusCode == 200) { - SearchObject search = SearchObject.fromJson(jsonDecode(response.body)); - return search; - } else { - throw Exception('Fail to load'); - } - } - - // 사용자 검색 - Future requestSearchUser(String query, int? page) async { - String url = "https://solved.ac/api/v3/search/user?query=$query"; - if (page != null) { - url += "&page=$page"; - } - final response = await http.get(Uri.parse(url)); - final statusCode = response.statusCode; - - if (statusCode == 200) { - SearchObject search = SearchObject.fromJson(jsonDecode(response.body)); - return search; - } else { - throw Exception('Fail to load'); - } - } - - // 태그 검색 - Future requestSearchTag(String query, int? page) async { - String url = "https://solved.ac/api/v3/search/tag?query=$query"; - if (page != null) { - url += "&page=$page"; - } - final response = await http.get(Uri.parse(url)); - final statusCode = response.statusCode; - - if (statusCode == 200) { - SearchObject search = SearchObject.fromJson(jsonDecode(response.body)); - return search; - } else { - throw Exception('Fail to load'); - } - } - - Future>> requestContests() async { - List upcomingContests(dom.Element element) { - if (element.getElementsByClassName('col-md-12').length < 5) { - return element - .getElementsByClassName('col-md-12')[2] - .getElementsByTagName('tbody') - .first - .getElementsByTagName('tr') - .toList() - .map((e) { - return Contest.fromElement(e); - }).toList(); - } else { - return element - .getElementsByClassName('col-md-12')[4] - .getElementsByTagName('tbody') - .first - .getElementsByTagName('tr') - .toList() - .map((e) { - return Contest.fromElement(e); - }).toList(); - } - } - - List ongoingContests(dom.Element element) { - if (element.getElementsByClassName('col-md-12').length < 5) { - return []; - } else { - return element - .getElementsByClassName('col-md-12')[2] - .getElementsByTagName('tbody') - .first - .getElementsByTagName('tr') - .toList() - .map((e) { - return Contest.fromElement(e); - }).toList(); - } - } - - List endedContests(dom.Element element) { - final endedContestList = element - .getElementsByClassName('col-md-12')[1] - .getElementsByTagName('tbody') - .first - .getElementsByTagName('tr') - .where( - (element) => element.getElementsByTagName('td')[5].text == '종료') - .toList(); - - return endedContestList.map((e) { - List startTimeList = e - .getElementsByTagName('td')[3] - .text - .toString() - .replaceAll('년', '') - .replaceAll('월', '') - .replaceAll('일', '') - .split(' '); - List endTimeList = e - .getElementsByTagName('td')[4] - .text - .toString() - .replaceAll('년', '') - .replaceAll('월', '') - .replaceAll('일', '') - .split(' '); - - DateTime startTime = DateTime.parse( - "${startTimeList[0].padLeft(4, "0")}-${startTimeList[1].padLeft(2, "0")}-${startTimeList[2].padLeft(2, "0")}T${startTimeList[3].padLeft(2, "0")}:00+09:00") - .toLocal(); - DateTime endTime = DateTime.parse( - "${endTimeList[0].padLeft(4, "0")}-${endTimeList[1].padLeft(2, "0")}-${endTimeList[2].padLeft(2, "0")}T${endTimeList[3].padLeft(2, "0")}:00+09:00") - .toLocal(); - - return Contest( - venue: 'BOJ Open', - name: e.getElementsByTagName('td')[0].text.trim(), - url: - 'https://www.acmicpc.net${e.getElementsByTagName('td')[0].getElementsByTagName('a')[0].attributes['href']}', - startTime: startTime, - endTime: endTime, - ); - }).toList(); - } - - final others = - await http.get(Uri.parse("https://www.acmicpc.net/contest/other/list")); - dom.Document docOthers = parser.parse(others.body); - final othersStatus = others.statusCode; - - final ended = await http - .get(Uri.parse("https://www.acmicpc.net/contest/official/list")); - dom.Document docEnded = parser.parse(ended.body); - final endedStatus = ended.statusCode; - - if (othersStatus != 200 || endedStatus != 200) { - throw Exception('Failed to load'); - } - - return [ - ongoingContests(docOthers.body!), - upcomingContests(docOthers.body!), - endedContests(docEnded.body!), - ]; - } - - Future> requestArenaContests() async { - final response = - await http.get(Uri.parse("https://solved.ac/api/v3/arena/contests")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - Map contestMap = jsonDecode(response.body); - Set contestIds = {}; - - for (var contests in contestMap.values) { - for (var contest in contests) { - if (contest['arenaBojContestId'] != null) { - contestIds.add(contest['arenaBojContestId']); - } - } - } - return contestIds; - } else { - throw Exception('Fail to load'); - } - } - - Future requestSiteStats() async { - final response = - await http.get(Uri.parse("https://solved.ac/api/v3/site/stats")); - final statusCode = response.statusCode; - - if (statusCode == 200) { - SiteStats siteStats = SiteStats.fromJson(jsonDecode(response.body)); - return siteStats; - } else { - throw Exception('Fail to load'); - } - } -} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart deleted file mode 100644 index 0353d54e..00000000 --- a/lib/services/notification_service.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_app_badger/flutter_app_badger.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; -import 'package:my_solved/services/user_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:timezone/data/latest.dart' as tz; -import 'package:timezone/timezone.dart' as tz; - -import '../models/contest.dart'; - -class NotificationService extends ChangeNotifier { - static final NotificationService _instance = - NotificationService._privateConstructor(); - - NotificationService._privateConstructor(); - - bool _disposed = false; - - factory NotificationService() { - return _instance; - } - - @override - void dispose() { - _disposed = true; - super.dispose(); - } - - @override - notifyListeners() { - if (!_disposed) { - super.notifyListeners(); - } - } - - final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - - void removeBadge() { - FlutterAppBadger.removeBadge(); - } - - Future init() async { - await _configureLocalTimeZone(); - await _initializeNotification(); - } - - Future _configureLocalTimeZone() async { - tz.initializeTimeZones(); - final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone(); - tz.setLocalLocation(tz.getLocation(timeZoneName)); - } - - Future _initializeNotification() async { - const AndroidInitializationSettings androidInitializationSettings = - AndroidInitializationSettings('@android:drawable/ic_lock_idle_alarm'); - const DarwinInitializationSettings iosInitializationSettings = - DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, - requestSoundPermission: true, - ); - const InitializationSettings settings = InitializationSettings( - android: androidInitializationSettings, - iOS: iosInitializationSettings, - ); - - _flutterLocalNotificationsPlugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestPermission(); - _flutterLocalNotificationsPlugin.initialize(settings); - } - - // 스트릭 알림 id: 2 - void setStreakPush(int hour, int minute) async { - NotificationDetails details = NotificationDetails( - android: AndroidNotificationDetails( - 'Notification_channel', - 'Notification_channel', - // color: const Color(0xFF11CE3C), - channelDescription: 'Notification_channel', - ), - iOS: DarwinNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - badgeNumber: 1, - ), - ); - - await _flutterLocalNotificationsPlugin.zonedSchedule( - 2, - '오늘도 백준 한 문제, 잊지 않으셨나요?', - '스트릭을 이어보세요!', - _timeZoneSetting(hour, minute), - details, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - androidAllowWhileIdle: true, - matchDateTimeComponents: DateTimeComponents.time, - ); - } - - Future cancelStreakPush() async { - _flutterLocalNotificationsPlugin.cancel(2); - } - - Future getContestPush(String contestName) async { - final prefs = await SharedPreferences.getInstance(); - final String key = contestName.hashCode.toString(); - return prefs.getBool(key) ?? false; - } - - void setContestPush(String contestName, bool value) async { - final prefs = await SharedPreferences.getInstance(); - final String key = contestName.hashCode.toString(); - prefs.setBool(key, value); - notifyListeners(); - } - - // 대회 알림 id: 대회 해시 - void toggleContestPush(Contest contest) async { - final bool isPush = await getContestPush(contest.name); - - if (isPush) { - _flutterLocalNotificationsPlugin.cancel(contest.name.hashCode); - setContestPush(contest.name, false); - } else { - NotificationDetails details = NotificationDetails( - android: AndroidNotificationDetails( - 'Contest_Notification', - 'Contest_Notification', - channelDescription: 'Contest_Notification', - importance: Importance.max, - playSound: true, - enableVibration: true, - ), - iOS: DarwinNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - badgeNumber: 1, - interruptionLevel: InterruptionLevel.timeSensitive), - ); - - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Asia/Seoul')); - - int beforeHour = UserService().contestAlarmHour; - int beforeMinute = UserService().contestAlarmMinute; - - tz.TZDateTime startDate = tz.TZDateTime( - tz.local, - contest.startTime.year, - contest.startTime.month, - contest.startTime.day, - contest.startTime.hour, - contest.startTime.minute, - ).subtract(Duration(hours: beforeHour, minutes: beforeMinute)); - - await _flutterLocalNotificationsPlugin.zonedSchedule( - contest.name.hashCode, - beforeHour + beforeMinute == 0 - ? '대회 시작!' - : ('${beforeHour == 0 ? '' : '$beforeHour시간'} ${beforeMinute == 0 ? '' : '$beforeMinute분'} 뒤 대회 시작!'), - contest.name, - startDate, - details, - uiLocalNotificationDateInterpretation: - UILocalNotificationDateInterpretation.absoluteTime, - androidAllowWhileIdle: true, - ); - setContestPush(contest.name, true); - } - notifyListeners(); - } - - tz.TZDateTime _timeZoneSetting(int hour, int minute) { - tz.initializeTimeZones(); - tz.setLocalLocation(tz.getLocation('Asia/Seoul')); - tz.TZDateTime now = tz.TZDateTime.now(tz.local); - tz.TZDateTime scheduledDate = - tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute); - return scheduledDate; - } -} diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart deleted file mode 100644 index c46f1b0b..00000000 --- a/lib/services/user_service.dart +++ /dev/null @@ -1,321 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -enum UserState { unknown, loggedIn, loading } - -class UserService extends ChangeNotifier { - static final UserService _instance = UserService._privateConstructor(); - UserState state = UserState.loading; - - String name = ''; - bool isIllustration = true; - - bool showTier = true; - bool showTags = true; - - int searchDefaultOpt = 0; - bool searchDefaultSort = true; - - bool isOnStreakAlarm = false; - int streakAlarmHour = 0; - int streakAlarmMinute = 0; - - int contestAlarmHour = 0; - int contestAlarmMinute = 0; - - String currentTimeZone = ''; - bool solvedToday = false; - - int tagChartType = 0; - int currentHomeTab = 0; - int currentUserTab = 0; - - int userCount = 0; - - bool _disposed = false; - - UserService._privateConstructor() { - Future valUserName = initUserName(); - valUserName.then((name) { - this.name = name; - if (name.isNotEmpty) { - state = UserState.loggedIn; - } else { - state = UserState.unknown; - } - notifyListeners(); - }); - - Future valIllustration = initIllustration(); - valIllustration.then((isOn) { - isIllustration = isOn; - }); - - Future valTier = initTier(); - valTier.then((isOn) { - showTier = isOn; - }); - - Future valTags = initTags(); - valTags.then((isOn) { - showTags = isOn; - }); - - Future valStreakAlarm = initStreakAlarm(); - valStreakAlarm.then((isOn) { - isOnStreakAlarm = isOn; - }); - - Future valStreakAlarmTime = initStreakAlarmTime(); - valStreakAlarmTime.then((time) { - streakAlarmHour = time.hour; - streakAlarmMinute = time.minute; - }); - - Future valSearchDefaultOpt = initSearchDefaultOpt(); - valSearchDefaultOpt.then((opt) { - searchDefaultOpt = opt; - }); - - Future valSearchDefaultSort = initSearchDefaultSort(); - valSearchDefaultSort.then((isAsc) { - searchDefaultSort = isAsc; - }); - - Future valContestAlarmTime = initContestAlarmTime(); - valContestAlarmTime.then((time) { - contestAlarmHour = time.hour; - contestAlarmMinute = time.minute; - }); - - Future valTimeZone = FlutterNativeTimezone.getLocalTimezone(); - valTimeZone.then((zone) { - currentTimeZone = zone; - }); - - Future valSolvedToday = initSolvedToday(); - valSolvedToday.then((isSolved) { - solvedToday = isSolved; - }); - - Future valTagChartType = initTagChartType(); - valTagChartType.then((type) { - tagChartType = type; - }); - - Future valHomeTab = initCurrentHomeTab(); - valHomeTab.then((tab) { - currentHomeTab = tab; - }); - - Future valUserTab = initCurrentUserTab(); - valUserTab.then((tab) { - currentUserTab = tab; - }); - - Future valUserCount = initUserCount(); - valUserCount.then((count) { - userCount = count; - }); - } - - factory UserService() { - return _instance; - } - - @override - void dispose() { - _disposed = true; - super.dispose(); - } - - @override - notifyListeners() { - if (!_disposed) { - super.notifyListeners(); - } - } - - void logout() async { - state = UserState.unknown; - name = ''; - final prefs = await SharedPreferences.getInstance(); - prefs.remove('username'); - notifyListeners(); - } - - Future initUserName() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getString('username') ?? ''; - } - - void setUserName(String name) async { - this.name = name; - final prefs = await SharedPreferences.getInstance(); - prefs.setString('username', name); - notifyListeners(); - } - - Future initIllustration() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool('isIllust') ?? true; - } - - void setIllustration(bool isOn) async { - isIllustration = isOn; - final prefs = await SharedPreferences.getInstance(); - prefs.setBool('isIllust', isOn); - notifyListeners(); - } - - Future initTier() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool('showTier') ?? true; - } - - void setTier(bool isOn) async { - showTier = isOn; - final prefs = await SharedPreferences.getInstance(); - prefs.setBool('showTier', isOn); - notifyListeners(); - } - - Future initTags() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool('showTags') ?? true; - } - - void setTags(bool isOn) async { - showTags = isOn; - final prefs = await SharedPreferences.getInstance(); - prefs.setBool('showTags', isOn); - notifyListeners(); - } - - Future initSearchDefaultOpt() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt('searchDefaultOpt') ?? 0; - } - - void setSearchDefaultOpt(int opt) async { - searchDefaultOpt = opt; - final prefs = await SharedPreferences.getInstance(); - prefs.setInt('searchDefaultOpt', opt); - notifyListeners(); - } - - Future initSearchDefaultSort() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool('searchDefaultSort') ?? true; - } - - void setSearchDefaultSort(bool isAsc) async { - searchDefaultSort = isAsc; - final prefs = await SharedPreferences.getInstance(); - prefs.setBool('searchDefaultSort', isAsc); - notifyListeners(); - } - - Future initStreakAlarm() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool('isOnStreakAlarm') ?? false; - } - - void setStreakAlarm(bool isOn) async { - isOnStreakAlarm = isOn; - final prefs = await SharedPreferences.getInstance(); - prefs.setBool('isOnStreakAlarm', isOn); - notifyListeners(); - } - - Future initStreakAlarmTime() async { - final prefs = await SharedPreferences.getInstance(); - return DateTime(0, 0, 0, prefs.getInt('streakAlarmHour') ?? 0, - prefs.getInt('streakAlarmMinute') ?? 0); - } - - void setStreakAlarmTime(int hour, int minute) async { - final prefs = await SharedPreferences.getInstance(); - prefs.setInt('streakAlarmHour', hour); - prefs.setInt('streakAlarmMinute', minute); - streakAlarmHour = hour; - streakAlarmMinute = minute; - notifyListeners(); - } - - Future initContestAlarmTime() async { - final prefs = await SharedPreferences.getInstance(); - return DateTime(0, 0, 0, prefs.getInt('contestAlarmHour') ?? 1, - prefs.getInt('contestAlarmMinute') ?? 0); - } - - void setContestAlarmTime(int hour, int minute) async { - final prefs = await SharedPreferences.getInstance(); - prefs.setInt('contestAlarmHour', hour); - prefs.setInt('contestAlarmMinute', minute); - contestAlarmHour = hour; - contestAlarmMinute = minute; - notifyListeners(); - } - - Future initSolvedToday() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool('solvedToday') ?? true; - } - - void setSolvedToday(bool solved) async { - solvedToday = solved; - final prefs = await SharedPreferences.getInstance(); - prefs.setBool('solvedToday', solved); - notifyListeners(); - } - - Future initTagChartType() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt('tagChartType') ?? 0; - } - - void setTagChartType(int type) async { - tagChartType = type; - final prefs = await SharedPreferences.getInstance(); - prefs.setInt('tagChartType', type); - notifyListeners(); - } - - Future initCurrentHomeTab() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt('currentHomeTab') ?? 0; - } - - void setCurrentHomeTab(int tab) async { - currentHomeTab = tab; - final prefs = await SharedPreferences.getInstance(); - prefs.setInt('currentHomeTab', tab); - notifyListeners(); - } - - Future initCurrentUserTab() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt('currentUserTab') ?? 0; - } - - void setCurrentUserTab(int tab) async { - currentUserTab = tab; - final prefs = await SharedPreferences.getInstance(); - prefs.setInt('currentUserTab', tab); - notifyListeners(); - } - - Future initUserCount() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getInt('userCount') ?? 0; - } - - void setUserCount(int count) async { - userCount = count; - final prefs = await SharedPreferences.getInstance(); - prefs.setInt('userCount', count); - notifyListeners(); - } -} diff --git a/lib/views/contest_view.dart b/lib/views/contest_view.dart deleted file mode 100644 index 74e20e5e..00000000 --- a/lib/views/contest_view.dart +++ /dev/null @@ -1,321 +0,0 @@ -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:my_solved/extensions/color_extension.dart'; -import 'package:my_solved/models/contest.dart'; -import 'package:my_solved/services/contest_service.dart'; -import 'package:my_solved/services/network_service.dart'; -import 'package:my_solved/services/notification_service.dart'; -import 'package:my_solved/views/search_view.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -class ContestView extends StatefulWidget { - const ContestView({super.key}); - - @override - State createState() => _ContestViewState(); -} - -class _ContestViewState extends State { - ContestService contestService = ContestService(); - NetworkService networkService = NetworkService(); - NotificationService notificationService = NotificationService(); - - Map _selectedVenues = {}; - Set _arenaContests = {}; - - int _selectedSegment = 0; - Map contestStatus = { - 0: '진행중인 대회', - 1: '예정된 대회', - 2: '종료된 대회', - }; - - void updateSelectedVenues() { - _selectedVenues = contestService.showVenues; - } - - void _updateSelectedSegment(int value) { - setState(() { - _selectedSegment = value; - }); - } - - @override - Widget build(BuildContext context) { - networkService.requestArenaContests().then((value) { - _arenaContests = value; - }); - - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - child: SafeArea( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - venueFilter(context), - contestTap(context), - contestBody(_selectedSegment), - ], - ), - ))); - } -} - -extension _ContestStateExtension on _ContestViewState { - /// 대회 필터링 위젯 - Widget venueFilter(BuildContext context) { - updateSelectedVenues(); - - return FutureBuilder( - future: contestService.getContestShow(), - builder: (context, snapshot) { - return Container( - margin: EdgeInsets.only(left: 10, right: 10, top: 20), - height: 30, - child: ListView.builder( - itemCount: snapshot.data?.length, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemBuilder: (context, index) { - String venue = - snapshot.data?.keys.elementAt(index) ?? "boj open"; - bool isSelected = snapshot.data?[venue] ?? true; - bool isOthers = venue == 'Others'; - - return CupertinoButton( - minSize: 0, - padding: EdgeInsets.zero, - child: Container( - margin: EdgeInsets.only(right: 10), - padding: EdgeInsets.symmetric(horizontal: 10), - decoration: BoxDecoration( - color: isSelected ? Colors.grey[400] : Colors.grey[200], - borderRadius: BorderRadius.circular(10), - ), - alignment: Alignment.center, - child: Row( - children: [ - isOthers - ? Icon( - Icons.more_horiz, - size: 14, - color: isSelected - ? Colors.grey[200] - : Colors.grey[400], - ) - : ExtendedImage.asset( - 'lib/assets/venues/${venue.toLowerCase()}.png', - fit: BoxFit.fill, - width: 14, - ), - SizedBox( - width: 5, - ), - Text( - venue, - style: TextStyle( - fontSize: 14, - color: isSelected - ? Colors.grey[200] - : Colors.grey[400]), - ) - ], - )), - onPressed: () { - snapshot.data![venue] = !isSelected; - contestService.toggleContestShow(venue); - // ignore: invalid_use_of_protected_member - setState(() { - _selectedVenues = snapshot.data!; - }); - }, - ); - }, - ), - ); - }); - } - - /// 대회 탭 - Widget contestTap(BuildContext context) { - return UnderlineSegmentControl( - children: contestStatus, - fontSize: 14, - onValueChanged: (value) { - _updateSelectedSegment(value); - }); - } - - /// 대회 목록 - Widget contestBody(int index) { - return FutureBuilder>>( - future: networkService.requestContests(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - List contests = snapshot.data?[index].where((contest) { - return _selectedVenues[contest.venue] == true; - }).toList(); - - if (contests.isEmpty) { - return Container( - alignment: Alignment.center, - height: MediaQuery.of(context).size.height * 0.8, - child: Text('현재 ${contestStatus[index]}가 없습니다.', - style: TextStyle(fontSize: 14, color: Colors.grey))); - } - - return Container( - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - for (var c in contests) contest(context, c, index), - ], - )); - } else { - return const Center(child: CupertinoActivityIndicator()); - } - }, - ); - } - - /// 대회 위젯 - Widget contest(BuildContext context, Contest contest, int contestType) { - bool hasUrl = contest.url != null; - bool isArena = false; - if (contest.venue == 'BOJ Open') { - int contestId = int.parse(contest.url?.split('/').last ?? ""); - isArena = _arenaContests.contains(contestId); - } - - /// 대회 위젯 상단 - /// 플랫폼 아이콘, 대회 이름, 대회 일정 - Widget contestTop(Contest contest) { - String venue = contest.venue?.toLowerCase() ?? "boj open"; - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ExtendedImage.asset( - 'lib/assets/venues/$venue.png', - width: 30, - ), - SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * 0.7, - child: Text( - contest.name, - style: TextStyle(fontSize: 16, color: Colors.black), - ), - ), - Text( - '일정: ${contest.startTime.month}월 ${contest.startTime.day}일 ${contest.startTime.hour}:${contest.startTime.minute.toString().padLeft(2, '0')} ~ ${contest.endTime.month}월 ${contest.endTime.day}일 ${contest.endTime.hour}:${contest.endTime.minute.toString().padLeft(2, '0')}', - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - ], - ) - ], - ); - } - - /// 대회 위젯 하단 - /// 대회 정보, 알림 설정, (아레나 대회일 경우) 아레나 아이콘 - Widget contestBottom(Contest contest) { - return Row( - children: [ - TextButton( - style: ButtonStyle( - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10))), - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(horizontal: 10, vertical: 5)), - minimumSize: MaterialStateProperty.all(Size(0, 0)), - backgroundColor: MaterialStateProperty.all( - hasUrl ? CupertinoTheme.of(context).main : Colors.black12, - ), - ), - child: Text('대회 정보', - style: TextStyle( - color: hasUrl ? Colors.white : Colors.grey, fontSize: 12)), - onPressed: () { - hasUrl - ? launchUrlString(contest.url ?? "", - mode: LaunchMode.externalApplication) - : null; - }, - ), - SizedBox(width: 10), - if (contestType == 1) - FutureBuilder( - future: notificationService.getContestPush(contest.name), - builder: (context, snapshot) { - bool isPush = snapshot.data ?? false; - return TextButton( - style: ButtonStyle( - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10))), - minimumSize: MaterialStateProperty.all(Size(0, 0)), - backgroundColor: MaterialStateProperty.all(isPush - ? CupertinoTheme.of(context).main - : Colors.black12), - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(horizontal: 10, vertical: 5)), - ), - child: Text(isPush ? '알림 설정 완료' : '알림 설정하기', - style: TextStyle( - color: isPush ? Colors.white : Colors.grey, - fontSize: 12, - )), - onPressed: () { - // ignore: invalid_use_of_protected_member - setState(() { - notificationService.toggleContestPush(contest); - }); - Fluttertoast.showToast( - msg: isPush ? '알림이 해제되었습니다.' : '알림이 설정되었습니다.', - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - backgroundColor: Colors.grey[700], - textColor: Colors.white, - fontSize: 14.0, - ); - }, - ); - }, - ), - Spacer(), - isArena - ? ExtendedImage.asset( - 'lib/assets/venues/ac arena.png', - height: 20, - ) - : SizedBox.shrink() - ], - ); - } - - return Container( - width: MediaQuery.of(context).size.width * 0.9, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Colors.white, - ), - margin: EdgeInsets.all(5), - padding: EdgeInsets.all(15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - contestTop(contest), - SizedBox(height: 5), - contestBottom(contest), - ], - )); - } -} diff --git a/lib/views/home_view.dart b/lib/views/home_view.dart deleted file mode 100644 index 8170fe17..00000000 --- a/lib/views/home_view.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:my_solved/models/user.dart'; -import 'package:my_solved/services/network_service.dart'; -import 'package:my_solved/services/user_service.dart'; -import 'package:my_solved/widgets/user_widget.dart'; -import 'package:provider/provider.dart'; - -class HomeView extends StatefulWidget { - const HomeView({super.key}); - - @override - State createState() => _HomeViewState(); -} - -class _HomeViewState extends State { - final UserService userService = UserService(); - final NetworkService networkService = NetworkService(); - - @override - Widget build(BuildContext context) { - String handle = userService.name; - - networkService.requestSiteStats().then((value) { - userService.setUserCount(value.userCount); - }); - - return CupertinoPageScaffold( - backgroundColor: Colors.white, - child: SafeArea( - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: FutureBuilder( - future: networkService.requestUser(handle), - builder: (context, snapshot) { - if (snapshot.hasData) { - final User? user = snapshot.data; - final Widget widgetGrass = grass(context, snapshot, - networkService.requestStreak(user?.handle ?? '')); - final Widget widgetTop100 = top100(context, snapshot, - networkService.requestTop100(user?.handle ?? '')); - final Widget widgetTagChart = tagChart(context, snapshot); - final Widget widgetBadges = badges(context, - networkService.requestBadges(user?.handle ?? '')); - - late Widget widgetBackground = backgroundImage( - context, - networkService - .requestBackground(user?.backgroundId ?? '')); - - late Widget widgetBadge; - if (user?.badgeId != null) { - widgetBadge = badge(context, - networkService.requestBadge(user?.badgeId ?? '')); - } else { - widgetBadge = SizedBox.shrink(); - } - - final PageController pageController = PageController( - initialPage: userService.currentHomeTab, - ); - - return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - profileHeader(context, snapshot, widgetBackground), - const SizedBox(height: 50), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * 0.05), - margin: EdgeInsets.only(bottom: 20), - child: profileDetail(context, snapshot, widgetBadge), - ), - Consumer( - builder: (context, userService, child) { - return CupertinoSlidingSegmentedControl( - children: const { - 0: Text('스트릭'), - 1: Text('레이팅'), - 2: Text('태그'), - 3: Text('뱃지'), - }, - groupValue: userService.currentHomeTab, - onValueChanged: (int? value) { - if (value != null) { - userService.setCurrentHomeTab(value); - pageController.animateToPage(value, - duration: const Duration(milliseconds: 300), - curve: Curves.ease); - } - }, - ); - }, - ), - const SizedBox(height: 10), - Container( - alignment: Alignment.topCenter, - height: 1.5 * MediaQuery.of(context).size.height, - child: PageView( - controller: pageController, - onPageChanged: (int index) { - userService.setCurrentHomeTab(index); - }, - children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * 0.05), - child: widgetGrass, - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * 0.05), - child: widgetTop100, - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * 0.05), - child: widgetTagChart, - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * 0.05), - child: widgetBadges, - ) - ], - ), - ), - ], - ); - } else if (snapshot.hasError) { - return Text('${snapshot.error}'); - } - return Center(child: CupertinoActivityIndicator()); - }, - )), - ), - ); - } -} diff --git a/lib/views/main_tab_view.dart b/lib/views/main_tab_view.dart deleted file mode 100644 index ce455fcb..00000000 --- a/lib/views/main_tab_view.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:my_solved/extensions/color_extension.dart'; -import 'package:my_solved/views/contest_view.dart'; -import 'package:my_solved/views/home_view.dart'; -import 'package:my_solved/views/search_view.dart'; - -class MainTabView extends StatelessWidget { - MainTabView({super.key}); - - final List options = [ - const HomeView(), - const SearchView(), - const ContestView(), - ]; - - @override - Widget build(BuildContext context) { - return CupertinoTabScaffold( - resizeToAvoidBottomInset: false, - tabBar: CupertinoTabBar( - activeColor: CupertinoTheme.of(context).main, - items: const [ - BottomNavigationBarItem( - icon: Icon(CupertinoIcons.house_fill), label: 'Home'), - BottomNavigationBarItem( - icon: Icon(CupertinoIcons.search), label: 'Search'), - BottomNavigationBarItem( - icon: Icon(CupertinoIcons.calendar), label: 'Contest'), - ], - ), - tabBuilder: (BuildContext context, int index) { - return CupertinoTabView( - builder: (BuildContext context) { - return options[index]; - }, - ); - }, - ); - } -} diff --git a/lib/views/search_view.dart b/lib/views/search_view.dart deleted file mode 100644 index c3f6b226..00000000 --- a/lib/views/search_view.dart +++ /dev/null @@ -1,473 +0,0 @@ -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:my_solved/extensions/color_extension.dart'; -import 'package:my_solved/models/search/object.dart'; -import 'package:my_solved/services/network_service.dart'; -import 'package:my_solved/services/notification_service.dart'; -import 'package:my_solved/services/user_service.dart'; -import 'package:my_solved/views/user_view.dart'; -import 'package:my_solved/widgets/user_widget.dart'; -import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -class SearchView extends StatefulWidget { - const SearchView({super.key}); - - @override - State createState() => _SearchViewState(); -} - -class _SearchViewState extends State { - NetworkService networkService = NetworkService(); - NotificationService notificationService = NotificationService(); - String input = ''; - bool isSubmitted = false; - Future? futureProblem; - Future? futureUser; - Future? futureTag; - int _selectedSegment = 0; - - void _updateSelectedSegment(int value) { - setState(() { - _selectedSegment = value; - }); - } - - @override - Widget build(BuildContext context) { - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - child: SafeArea( - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Container( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - searchBar(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - UnderlineSegmentControl( - children: { - 0: '문제', - 1: '사용자', - 2: '태그', - }, - onValueChanged: (value) { - _updateSelectedSegment(value); - }), - Builder( - builder: (context) { - if (_selectedSegment == 0) { - return FutureBuilder( - future: futureProblem, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Column( - children: [ - for (dynamic problem - in snapshot.data!.items) - problem['problemId'] == null - ? SizedBox.shrink() - : CupertinoButton( - padding: EdgeInsets.zero, - minSize: 0, - onPressed: () async { - String url = - 'https://acmicpc.net/problem/${problem['problemId']}'; - launchUrlString(url, - mode: LaunchMode - .externalApplication); - }, - child: Container( - decoration: BoxDecoration( - color: CupertinoTheme.of( - context) - .backgroundGray, - borderRadius: - const BorderRadius.all( - Radius.circular(10), - ), - ), - margin: const EdgeInsets.only( - top: 10), - padding: - const EdgeInsets.all(20), - width: MediaQuery.of(context) - .size - .width, - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Row( - crossAxisAlignment: - CrossAxisAlignment - .end, - children: [ - Consumer( - builder: (context, - userService, - child) { - return userService - .showTier - ? Container( - margin: const EdgeInsets - .only( - right: - 5), - child: SvgPicture - .asset( - 'lib/assets/tiers/${problem['level']}.svg', - height: - 18, - )) - : Container(); - }), - Consumer( - builder: (context, - userService, - child) { - return Text( - '${problem['problemId']}번', - style: TextStyle( - fontSize: 16, - color: userService - .showTier - ? levelColor( - problem['level'] ?? - 0) - : Colors - .black), - ); - }), - ], - ), - const SizedBox(height: 5), - Text( - '${problem['titleKo']}', - style: TextStyle( - fontSize: 20, - color: - Colors.black), - ), - Consumer( - builder: (context, - userService, - child) { - return userService - .showTags - ? problem['tags'] != - null && - problem['tags'] - .isNotEmpty - ? Wrap( - children: [ - for (dynamic tag - in problem[ - 'tags']) - Text( - '#${tag['displayNames'][0]['name']} ', - style: - TextStyle( - fontSize: - 12, - color: - Colors.black.withOpacity(0.5), - ), - ), - ], - ) - : SizedBox - .shrink() - : SizedBox.shrink(); - }), - ], - ), - ), - ), - ], - ); - } else if (snapshot.hasError) { - return Text('${snapshot.error}'); - } else { - return SizedBox.shrink(); - } - }); - } else if (_selectedSegment == 1) { - return FutureBuilder( - future: futureUser, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Column( - children: [ - for (dynamic user in snapshot.data!.items) - user['handle'] == null - ? SizedBox.shrink() - : Container( - decoration: BoxDecoration( - color: - CupertinoTheme.of(context) - .backgroundGray, - borderRadius: - const BorderRadius.all( - Radius.circular(10))), - margin: const EdgeInsets.only( - top: 10), - width: MediaQuery.of(context) - .size - .width, - child: CupertinoButton( - alignment: Alignment.centerLeft, - padding: - EdgeInsets.only(left: 20), - child: Row( - children: [ - SvgPicture.asset( - 'lib/assets/tiers/${user['tier'] ?? 0}.svg', - height: 20, - ), - const SizedBox(width: 5), - ClipRRect( - borderRadius: - BorderRadius.circular( - 10), - child: - ExtendedImage.network( - user['profileImageUrl'] ?? - 'https://static.solved.ac/misc/360x360/default_profile.png', - width: 20, - fit: BoxFit.cover, - loadStateChanged: - (ExtendedImageState - state) { - switch (state - .extendedImageLoadState) { - case LoadState - .loading: - return CupertinoActivityIndicator(); - case LoadState - .completed: - return state - .completedWidget; - case LoadState - .failed: - return SizedBox - .shrink(); - } - }, - ), - ), - const SizedBox(width: 5), - Text( - '${user['handle']}', - style: TextStyle( - fontSize: 14, - color: ratingColor( - user['rating'] ?? - 0)), - ), - ], - ), - onPressed: () => - Navigator.of(context).push( - CupertinoPageRoute( - builder: - (BuildContext context) { - return UserView( - username: - user['handle']); - }, - ), - ), - ), - ), - ], - ); - } else if (snapshot.hasError) { - return Text('${snapshot.error}'); - } else { - return SizedBox.shrink(); - } - }, - ); - } else { - return FutureBuilder( - future: futureTag, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Column( - children: [ - for (dynamic tag in snapshot.data!.items) - tag['key'] == null - ? SizedBox.shrink() - : Container( - decoration: BoxDecoration( - color: - CupertinoTheme.of(context) - .backgroundGray, - borderRadius: - const BorderRadius.all( - Radius.circular(10))), - margin: const EdgeInsets.only( - top: 10), - width: MediaQuery.of(context) - .size - .width, - child: CupertinoButton( - alignment: Alignment.centerLeft, - padding: - EdgeInsets.only(left: 20), - child: Text( - '${tag['key']}:${tag['problemCount']}', - style: - TextStyle(fontSize: 14), - ), - onPressed: () async { - String url = - 'https://solved.ac/search?query=%23${tag['key']}'; - launchUrlString(url, - mode: LaunchMode - .externalApplication); - }, - ), - ), - ], - ); - } else if (snapshot.hasError) { - return Text('${snapshot.error}'); - } else { - return SizedBox.shrink(); - } - }, - ); - } - }, - ) - ], - ) - ], - ), - ), - ), - ), - ); - } -} - -extension _SearchStateExtension on _SearchViewState { - Widget searchBar() { - return Container( - margin: const EdgeInsets.only(top: 20), - height: 40, - child: CupertinoSearchTextField( - placeholder: '문제 번호, 문제 제목을 입력해주세요.', - onChanged: (text) { - // ignore: invalid_use_of_protected_member - setState(() { - input = text; - }); - }, - onSubmitted: (text) { - // ignore: invalid_use_of_protected_member - setState(() { - const optList = ['id', 'level', 'title', 'solved', 'average_try']; - int opt = UserService().searchDefaultOpt; - bool asc = UserService().searchDefaultSort; - futureProblem = networkService.requestSearchProblem( - input, null, optList[opt], asc ? 'asc' : 'desc'); - futureUser = NetworkService().requestSearchUser(input, null); - futureTag = NetworkService().requestSearchTag(input, null); - isSubmitted = true; - }); - }, - ), - ); - } -} - -class UnderlineSegmentControl extends StatefulWidget { - final Map children; - final ValueChanged onValueChanged; - final Color color; - final double fontSize; - final FontWeight selectionFontWeight; - final FontWeight unselectionFontWeight; - final double indicatorHeight; - final double indicatorWidth; - - const UnderlineSegmentControl({ - super.key, - required this.children, - required this.onValueChanged, - this.color = CupertinoColors.label, - this.fontSize = 16.0, - this.selectionFontWeight = FontWeight.bold, - this.unselectionFontWeight = FontWeight.normal, - this.indicatorHeight = 2.0, - this.indicatorWidth = 50.0, - }); - - @override - State createState() => - _UnderlineSegmentedControlState(); -} - -class _UnderlineSegmentedControlState extends State { - int _selectedIndex = 0; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: widget.children.entries - .map( - (entry) => GestureDetector( - onTap: () { - setState(() { - _selectedIndex = entry.key; - widget.onValueChanged(_selectedIndex); - }); - }, - child: Column( - children: [ - Container( - margin: EdgeInsets.only(top: 10), - padding: EdgeInsets.only( - left: 10, - top: 10, - right: 10, - bottom: 10, - ), - child: Text( - entry.value, - style: TextStyle( - fontSize: widget.fontSize, - fontWeight: _selectedIndex == entry.key - ? widget.selectionFontWeight - : widget.unselectionFontWeight, - color: widget.color, - ), - ), - ), - if (_selectedIndex == entry.key) - Container( - width: widget.indicatorWidth, - height: widget.indicatorHeight, - decoration: BoxDecoration( - color: widget.color, - ), - ) - ], - ), - ), - ) - .toList(), - ); - } -} diff --git a/lib/views/setting_view.dart b/lib/views/setting_view.dart deleted file mode 100644 index 2229a265..00000000 --- a/lib/views/setting_view.dart +++ /dev/null @@ -1,477 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:my_solved/extensions/color_extension.dart'; -import 'package:my_solved/services/notification_service.dart'; -import 'package:my_solved/services/user_service.dart'; - -const _sortList = ['ID', '레벨', '제목', '푼 사람 수', '평균 시도']; - -class SettingView extends StatefulWidget { - const SettingView({super.key}); - - @override - State createState() => _SettingViewState(); -} - -class _SettingViewState extends State { - NotificationService notificationService = NotificationService(); - UserService userService = UserService(); - - bool _isIllustration = UserService().isIllustration; - bool _showTier = UserService().showTier; - bool _showTags = UserService().showTags; - - int _searchDefaultOpt = UserService().searchDefaultOpt; - bool _searchDefaultSort = UserService().searchDefaultSort; - - bool _isOnStreakAlarm = UserService().isOnStreakAlarm; - int _streakAlarmHour = UserService().streakAlarmHour; - int _streakAlarmMinute = UserService().streakAlarmMinute; - - int _contestAlarmHour = UserService().contestAlarmHour; - int _contestAlarmMinute = UserService().contestAlarmMinute; - - @override - Widget build(BuildContext context) { - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - leading: CupertinoNavigationBarBackButton( - color: CupertinoColors.label, - onPressed: () => Navigator.of(context).pop(), - ), - middle: Text('설정'), - ), - child: SafeArea( - child: Container( - padding: EdgeInsets.only(left: 20, right: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 20), - Text('사용자 정보', - style: TextStyle( - fontSize: 12, color: CupertinoColors.systemGrey)), - Divider( - thickness: 1, - height: 5, - color: CupertinoTheme.of(context).dividerGray, - ), - id(userService.name), - const SizedBox(height: 20), - Text('설정', - style: TextStyle( - fontSize: 12, color: CupertinoColors.systemGrey)), - Divider( - thickness: 1, - height: 5, - color: CupertinoTheme.of(context).dividerGray, - ), - searchDefaultOpt(context), - illustration(context), - tierIcon(context), - tags(context), - const SizedBox(height: 20), - Text('알림', - style: TextStyle( - fontSize: 12, color: CupertinoColors.systemGrey)), - Divider( - thickness: 1, - height: 5, - color: CupertinoTheme.of(context).dividerGray, - ), - streakAlarm(context), - contestAlarm(context), - SizedBox(height: 40), - // Divider( - // thickness: 1, - // height: 20, - // color: CupertinoTheme.of(context).dividerGray, - // ), - // Container( - // // padding: EdgeInsets.only(top: 20), - // alignment: Alignment.center, - // child: Text('구현 예정 기능')), - // Divider( - // thickness: 1, - // height: 20, - // color: CupertinoTheme.of(context).dividerGray, - // ), - logoutButton(), - ], - ), - ), - ), - ); - } - - void _showDialog(Widget child, BuildContext context) { - showCupertinoModalPopup( - context: context, - builder: (BuildContext context) => Container( - height: 216, - padding: const EdgeInsets.only(top: 6), - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - color: CupertinoColors.systemBackground.resolveFrom(context), - child: SafeArea(top: false, child: child), - )); - } -} - -extension _SettingStateExtension on _SettingViewState { - Widget id(String name) { - return Row( - children: [ - Text('백준 핸들'), - Spacer(), - Text(name), - ], - ); - } - - Widget illustration(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: 5, bottom: 5), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('일러스트 배경'), - Text( - '일러스트 배경을 사용합니다.', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - ], - ), - Spacer(), - CupertinoSwitch( - value: _isIllustration, - activeColor: CupertinoTheme.of(context).main, - onChanged: (bool value) { - // ignore: invalid_use_of_protected_member - setState(() { - _isIllustration = value; - UserService().setIllustration(value); - }); - }, - ), - ], - ), - ); - } - - Widget tierIcon(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: 5, bottom: 5), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('난이도 표시'), - Text( - '문제 검색시 난이도를 표시합니다.', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - ], - ), - Spacer(), - CupertinoSwitch( - value: _showTier, - activeColor: CupertinoTheme.of(context).main, - onChanged: (bool value) { - // ignore: invalid_use_of_protected_member - setState(() { - _showTier = value; - UserService().setTier(value); - }); - }, - ), - ], - ), - ); - } - - Widget tags(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: 5, bottom: 5), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('태그 표시'), - Text( - '문제 검색시 태그를 표시합니다.', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - ], - ), - Spacer(), - CupertinoSwitch( - value: _showTags, - activeColor: CupertinoTheme.of(context).main, - onChanged: (bool value) { - // ignore: invalid_use_of_protected_member - setState(() { - _showTags = value; - UserService().setTags(value); - }); - }, - ), - ], - ), - ); - } - - Widget searchDefaultOpt(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: 5, bottom: 5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('문제 정렬 기본 옵션'), - Text( - '검색한 문제들의 정렬 기본 옵션입니다.', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - ], - ), - Spacer(), - CupertinoButton( - padding: EdgeInsets.zero, - minSize: 0, - child: Text( - _sortList[UserService().searchDefaultOpt], - textAlign: TextAlign.right, - ), - onPressed: () => _showDialog( - CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - itemExtent: 32, - scrollController: FixedExtentScrollController( - initialItem: _searchDefaultOpt, - ), - onSelectedItemChanged: (int selected) { - // ignore: invalid_use_of_protected_member - setState(() { - _searchDefaultOpt = selected; - UserService().setSearchDefaultOpt(selected); - }); - }, - children: List.generate(5, (index) { - return Center( - child: Text( - _sortList[index], - ), - ); - }), - ), - context), - ), - SizedBox(width: 10), - CupertinoButton( - padding: EdgeInsets.zero, - minSize: 0, - child: Text(_searchDefaultSort ? '오름차순' : '내림차순'), - onPressed: () => _showDialog( - CupertinoPicker( - magnification: 1.22, - squeeze: 1.2, - useMagnifier: true, - itemExtent: 32, - scrollController: FixedExtentScrollController( - initialItem: UserService().searchDefaultSort ? 0 : 1, - ), - onSelectedItemChanged: (int selected) { - // ignore: invalid_use_of_protected_member - setState(() { - _searchDefaultSort = selected == 0 ? true : false; - UserService() - .setSearchDefaultSort(selected == 0 ? true : false); - }); - }, - children: List.generate(2, (index) { - return Center( - child: Text( - index == 0 ? '오름차순' : '내림차순', - ), - ); - }), - ), - context), - ) - ], - )); - } - - Widget streakAlarm(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: 5, bottom: 5), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('스트릭 알림'), - Text( - '스트릭 알림 시간을 설정합니다.', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - ], - ), - Spacer(), - CupertinoButton( - padding: EdgeInsets.zero, - child: Text( - '$_streakAlarmHour:${_streakAlarmMinute.toString().padLeft(2, '0')} ${_streakAlarmHour > 11 ? 'PM' : 'AM'}', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - onPressed: () => _showDialog( - CupertinoDatePicker( - use24hFormat: false, - mode: CupertinoDatePickerMode.time, - initialDateTime: - DateTime(0, 0, 0, _streakAlarmHour, _streakAlarmMinute), - minuteInterval: 10, - onDateTimeChanged: (DateTime newDateTime) { - // ignore: invalid_use_of_protected_member - setState(() { - _streakAlarmHour = newDateTime.hour; - _streakAlarmMinute = newDateTime.minute; - - userService.setStreakAlarmTime( - newDateTime.hour, newDateTime.minute); - - if (_isOnStreakAlarm) { - notificationService.setStreakPush( - newDateTime.hour, newDateTime.minute); - } - }); - }, - ), - context), - ), - CupertinoSwitch( - value: _isOnStreakAlarm, - activeColor: CupertinoTheme.of(context).main, - onChanged: (bool value) { - // ignore: invalid_use_of_protected_member - setState(() { - _isOnStreakAlarm = value; - }); - userService.setStreakAlarm(value); - - if (value) { - notificationService.setStreakPush( - _streakAlarmHour, _streakAlarmMinute); - } else { - notificationService.cancelStreakPush(); - } - }, - ), - ], - ), - ); - } - - Widget contestAlarm(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: 5, bottom: 5), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('대회 시작 알림'), - Text( - '대회 시작 미리 알림 시간을 설정합니다.', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - Text('알림 설정 이후, 알림 시간을 변경하시면 반영되지 않습니다.', - style: TextStyle( - color: CupertinoColors.destructiveRed, - fontSize: 10, - )) - ], - ), - Spacer(), - CupertinoButton( - padding: EdgeInsets.zero, - child: Text( - '$_contestAlarmHour시간 ${_contestAlarmMinute.toString().padLeft(2, '0')}분 전', - style: TextStyle( - color: CupertinoColors.systemGrey, - fontSize: 12, - ), - ), - onPressed: () => _showDialog( - CupertinoDatePicker( - use24hFormat: true, - mode: CupertinoDatePickerMode.time, - initialDateTime: - DateTime(0, 0, 0, _contestAlarmHour, _contestAlarmMinute), - minuteInterval: 10, - onDateTimeChanged: (DateTime newDateTime) { - // ignore: invalid_use_of_protected_member - setState(() { - _contestAlarmHour = newDateTime.hour; - _contestAlarmMinute = newDateTime.minute; - - userService.setContestAlarmTime( - newDateTime.hour, newDateTime.minute); - }); - }, - ), - context), - ) - ], - ), - ); - } - - Widget logoutButton() { - return Container( - margin: EdgeInsets.only(top: 24, bottom: 14), - child: CupertinoButton( - padding: EdgeInsets.zero, - child: Text( - '로그아웃', - style: TextStyle(color: CupertinoColors.systemRed), - ), - onPressed: () { - userService.logout(); - }, - ), - ); - } -} diff --git a/lib/views/user_view.dart b/lib/views/user_view.dart deleted file mode 100644 index d24bb04e..00000000 --- a/lib/views/user_view.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:my_solved/models/user.dart'; -import 'package:my_solved/services/network_service.dart'; -import 'package:my_solved/services/user_service.dart'; -import 'package:my_solved/widgets/user_widget.dart'; -import 'package:provider/provider.dart'; - -class UserView extends StatefulWidget { - const UserView({super.key, required this.username}); - - final String username; - - @override - State createState() => _UserViewState(); -} - -class _UserViewState extends State { - final UserService userService = UserService(); - final NetworkService networkService = NetworkService(); - - @override - Widget build(BuildContext context) { - String username = widget.username; - var navigationBar = CupertinoNavigationBar( - leading: CupertinoNavigationBarBackButton( - color: CupertinoColors.label, - onPressed: () => Navigator.of(context).pop(), - ), - middle: Text(widget.username), - ); - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: navigationBar, - child: SafeArea( - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: FutureBuilder( - future: networkService.requestUser(username), - builder: (context, snapshot) { - if (snapshot.hasData) { - final User? user = snapshot.data; - final Widget widgetGrass = grass(context, snapshot, - networkService.requestStreak(user?.handle ?? '')); - final Widget widgetTop100 = top100(context, snapshot, - networkService.requestTop100(user?.handle ?? '')); - final Widget widgetTagChart = tagChart(context, snapshot); - final Widget widgetBadges = badges(context, - networkService.requestBadges(user?.handle ?? '')); - - late Widget widgetBackground = backgroundImage( - context, - networkService - .requestBackground(user?.backgroundId ?? '')); - - late Widget widgetBadge; - if (user?.badgeId != null) { - widgetBadge = badge(context, - networkService.requestBadge(user?.badgeId ?? '')); - } else { - widgetBadge = SizedBox.shrink(); - } - - final PageController pageController = PageController( - initialPage: userService.currentHomeTab, - ); - - return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - profileHeader(context, snapshot, widgetBackground), - SizedBox(height: 50), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * 0.05), - margin: EdgeInsets.only(bottom: 20), - child: - profileDetail(context, snapshot, widgetBadge), - ), - Consumer( - builder: (context, userService, child) { - return CupertinoSlidingSegmentedControl( - children: const { - 0: Text('스트릭'), - 1: Text('레이팅'), - 2: Text('태그'), - 3: Text('뱃지'), - }, - groupValue: userService.currentHomeTab, - onValueChanged: (int? value) { - if (value != null) { - userService.setCurrentHomeTab(value); - pageController.animateToPage(value, - duration: - const Duration(milliseconds: 300), - curve: Curves.ease); - } - }, - ); - }, - ), - const SizedBox(height: 10), - Container( - alignment: Alignment.topCenter, - height: 1.5 * MediaQuery.of(context).size.height, - child: PageView( - controller: pageController, - onPageChanged: (int index) { - userService.setCurrentHomeTab(index); - }, - children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * - 0.05), - child: widgetGrass, - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * - 0.05), - child: widgetTop100, - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * - 0.05), - child: widgetTagChart, - ), - Container( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * - 0.05), - child: widgetBadges, - ) - ], - ), - ), - ], - ); - } else if (snapshot.hasError) { - return Text('${snapshot.error}'); - } - return Center(child: CupertinoActivityIndicator()); - }, - )))); - } -} diff --git a/lib/widgets/user_widget.dart b/lib/widgets/user_widget.dart deleted file mode 100644 index 0b8f2367..00000000 --- a/lib/widgets/user_widget.dart +++ /dev/null @@ -1,1618 +0,0 @@ -import 'dart:math'; - -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_radar_chart/flutter_radar_chart.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:my_solved/extensions/color_extension.dart'; -import 'package:my_solved/models/streak_date.dart'; -import 'package:my_solved/models/user.dart'; -import 'package:my_solved/models/user/background.dart'; -import 'package:my_solved/models/user/badges.dart'; -import 'package:my_solved/models/user/grass.dart'; -import 'package:my_solved/models/user/tag_ratings.dart'; -import 'package:my_solved/models/user/top_100.dart'; -import 'package:my_solved/services/network_service.dart'; -import 'package:my_solved/services/user_service.dart'; -import 'package:my_solved/views/setting_view.dart'; -import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -/// ************************************************ -/// User Widget -/// ************************************************* -Widget profileHeader(BuildContext context, AsyncSnapshot snapshot, - Widget backgroundImage) { - String handle = UserService().name; - - return Stack( - clipBehavior: Clip.none, - children: [ - backgroundImage, - snapshot.data!.handle == handle - ? Align( - alignment: Alignment.topRight, - child: CupertinoButton( - child: Icon( - CupertinoIcons.gear_solid, - color: ratingColor(snapshot.data!.rating), - ), - onPressed: () => Navigator.of(context).push( - CupertinoPageRoute( - maintainState: true, - builder: (BuildContext context) { - return SettingView(); - }, - ), - ), - ), - ) - : Container(), - Positioned( - top: MediaQuery.of(context).size.height * 0.25 - 20, - child: Container( - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.symmetric(horizontal: 20), - alignment: Alignment.bottomCenter, - child: Container( - margin: const EdgeInsets.only(top: 20), - clipBehavior: Clip.none, - height: 50, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - solvedCount(context, snapshot), - voteCount(context, snapshot), - reverseRivalCount(context, snapshot) - ], - ), - ))), - Positioned( - bottom: -40, - left: 20, - child: profileImage(context, snapshot), - ), - ], - ); -} - -Widget profileDetail( - BuildContext context, AsyncSnapshot snapshot, Widget badge) { - return Container( - width: MediaQuery.of(context).size.width, - margin: const EdgeInsets.only(top: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - handle(context, snapshot), - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - badge, - classes(context, snapshot), - ], - ) - ], - ), - snapshot.data!.bio!.isNotEmpty - ? SizedBox(height: 5) - : SizedBox.shrink(), - bio(context, snapshot), - // snapshot.data!.organizations.isNotEmpty - // ? SizedBox(height: 5) - // : SizedBox.shrink(), - // organizations(context, snapshot), - ], - ), - ); -} - -Widget grass(BuildContext context, AsyncSnapshot userSnapshot, - Future future) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - if (snapshot.hasData) { - List grass = List.from(snapshot.data!.grass); - int currentStreak = snapshot.data!.currentStreak; - int longestStreak = snapshot.data!.longestStreak; - String theme = snapshot.data!.theme ?? 'default'; - String topic = snapshot.data!.topic; - - bool solvedToday = false; - - grass.sort((a, b) => a['date'].compareTo(b['date'])); - - List streakDates = []; - DateTime now = DateTime.now().toUtc().add(Duration(hours: 3)); - - StreakDate today = StreakDate( - year: now.year, - month: now.month, - day: now.day, - weekDay: now.weekday, - solvedCount: -1, - isSolved: false, - isFuture: false, - isFrozen: false, - isRepaired: false); - if (grass.isNotEmpty) { - String gl = grass.last['date'].toString(); - if (today.year == int.parse(gl.substring(0, 4)) && - today.month == int.parse(gl.substring(5, 7)) && - today.day == int.parse(gl.substring(8, 10))) { - today = StreakDate( - year: now.year, - month: now.month, - day: now.day, - weekDay: now.weekday, - solvedCount: grass.last['value'], - isSolved: grass.last['value'] >= 0, - isFuture: false, - isFrozen: false, - isRepaired: false); - grass.removeLast(); - if (today.solvedCount > 0) { - solvedToday = true; - } - } - } - streakDates.add(today); - while (streakDates.last.weekDay != 6) { - DateTime tomorrow = DateTime(streakDates.last.year, - streakDates.last.month, streakDates.last.day) - .add(Duration(days: 1)); - int year = tomorrow.year; - int month = tomorrow.month; - int day = tomorrow.day; - int weekDay = tomorrow.weekday; - streakDates.add(StreakDate( - year: year, - month: month, - day: day, - weekDay: weekDay, - solvedCount: 0, - isSolved: false, - isFuture: true, - isFrozen: false, - isRepaired: false)); - } - streakDates = streakDates.reversed.toList(); - - int maxSolvedCount = 0; - while (streakDates.length < 365) { - DateTime yesterday = DateTime(streakDates.last.year, - streakDates.last.month, streakDates.last.day) - .subtract(Duration(days: 1)); - int year = yesterday.year; - int month = yesterday.month; - int day = yesterday.day; - int weekDay = yesterday.weekday; - int solvedCount = -1; - bool isSolved = false; - bool isFrozen = false; - bool isRepaired = false; - String gl = ''; - if (grass.isNotEmpty) { - gl = grass.last['date'].toString(); - if (year == int.parse(gl.substring(0, 4)) && - month == int.parse(gl.substring(5, 7)) && - day == int.parse(gl.substring(8, 10))) { - if (grass.last['value'] == 'frozen') { - isFrozen = true; - } else if (grass.last['value'] == 'repaired-incremented') { - isRepaired = true; - } else { - solvedCount = grass.last['value']; - isSolved = solvedCount >= 0; - maxSolvedCount = max(maxSolvedCount, solvedCount); - } - grass.removeLast(); - } - } - streakDates.add(StreakDate( - year: year, - month: month, - day: day, - weekDay: weekDay, - solvedCount: solvedCount, - isSolved: isSolved, - isFuture: false, - isFrozen: isFrozen, - isRepaired: isRepaired)); - } - const List week = ['일', '월', '화', '수', '목', '금', '토']; - - maxSolvedCount = max(min(maxSolvedCount, 50), 4); - int themeAccent(int solvedCount) { - if (solvedCount == 0) { - return 0; - } else if (solvedCount <= (maxSolvedCount * 0.1).ceil()) { - return 1; - } else if (solvedCount <= (maxSolvedCount * 0.3).ceil()) { - return 2; - } else if (solvedCount <= (maxSolvedCount * 0.6).ceil()) { - return 3; - } else { - return 4; - } - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const SizedBox(width: 5), - RichText( - text: TextSpan(children: [ - TextSpan( - text: '현재 ', - style: TextStyle( - color: Colors.black, - fontSize: MediaQuery.of(context).size.width * 0.045), - ), - TextSpan( - text: '$currentStreak', - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: - MediaQuery.of(context).size.width * 0.045)), - TextSpan( - text: '일', - style: TextStyle( - color: Colors.black, - fontSize: - MediaQuery.of(context).size.width * 0.045)), - ])), - Spacer(), - Tooltip( - preferBelow: false, - message: solvedToday ? '풀었습니다!!' : '오늘의 문제를 풀어보세요', - triggerMode: TooltipTriggerMode.tap, - child: SvgPicture.asset( - 'lib/assets/icons/streak.svg', - width: 20, - colorFilter: ColorFilter.mode( - solvedToday - ? CupertinoTheme.of(context).main - : Color(0xff8a8f95), - BlendMode.srcATop), - ), - ), - const SizedBox(width: 10), - ], - ), - const SizedBox(height: 10), - SizedBox( - height: 20, - // width: MediaQuery.of(context).size.width * 0.8, - child: GridView.builder( - itemBuilder: (BuildContext context, int index) { - return Text( - week[index], - style: TextStyle(color: Color(0xff8a8f95)), - textAlign: TextAlign.center, - ); - }, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 7, - childAspectRatio: 1, - crossAxisSpacing: 10), - itemCount: 7, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - ), - ), - RotatedBox( - quarterTurns: 2, - child: Container( - clipBehavior: Clip.none, - alignment: Alignment.bottomCenter, - // width: MediaQuery.of(context).size.width * 0.8, - child: GridView.builder( - scrollDirection: Axis.vertical, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 7, - childAspectRatio: 1, - crossAxisSpacing: 10, - mainAxisSpacing: 10), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: 35, - itemBuilder: (context, index) { - var date = streakDates[index]; - - Color color = Color(0xffdddfe0); - if (date.isRepaired) { - color = CupertinoTheme.of(context).tier[0]!; - } else if (date.isSolved) { - if (topic == 'today-solved-max-tier') { - color = CupertinoTheme.of(context) - .tier[date.solvedCount]!; - } else if (topic == 'today-solved') { - color = CupertinoTheme.of(context).streakTheme[ - theme]![themeAccent(date.solvedCount)]; - } - } - - String textTooltipBottom = ''; - if (date.isFrozen) { - textTooltipBottom = '스트릭 프리즈 사용'; - } else if (date.isRepaired) { - textTooltipBottom = '스트릭 리페어 사용'; - } else if (!date.isSolved) { - textTooltipBottom = '-'; - } else { - if (topic == 'today-solved-max-tier') { - if (date.solvedCount == 0) { - textTooltipBottom = 'Unrated'; - } else { - textTooltipBottom = tierStr(date.solvedCount); - } - } else if (topic == 'today-solved') { - textTooltipBottom = '${date.solvedCount}문제 해결'; - } - } - - try { - return date.isFuture - ? SizedBox.shrink() - : Tooltip( - richMessage: TextSpan( - style: TextStyle( - color: Colors.white, - ), - children: [ - TextSpan( - text: - '${date.year}/${date.month}/${date.day}\n', - ), - TextSpan( - text: textTooltipBottom, - ), - ]), - triggerMode: TooltipTriggerMode.tap, - preferBelow: false, - child: date.isFrozen - ? RotatedBox( - quarterTurns: 2, - child: SvgPicture.asset( - 'lib/assets/icons/freeze.svg', - clipBehavior: Clip.none, - )) - : Container( - margin: EdgeInsets.all(5), - alignment: Alignment.center, - decoration: BoxDecoration( - color: color, - borderRadius: - BorderRadius.circular(5), - ), - )); - } catch (e) { - debugPrint(e as String?); - return SizedBox.shrink(); - } - }, - ), - )), - const SizedBox(height: 10), - RichText( - text: TextSpan( - style: TextStyle( - color: Colors.black, - fontSize: MediaQuery.of(context).size.width * 0.035, - ), - children: [ - TextSpan( - text: '최장 ', - ), - TextSpan( - text: '$longestStreak', - style: TextStyle( - fontWeight: FontWeight.bold, - )), - TextSpan( - text: '일 연속 문제 해결', - ) - ], - ), - ), - ], - ); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else { - return CupertinoActivityIndicator(); - } - }); -} - -Widget top100( - BuildContext context, AsyncSnapshot snapshot, Future future) { - int rating = snapshot.data?.rating ?? 0; - int tier = snapshot.data?.tier ?? 0; - int rank = snapshot.data?.rank ?? 0; - - Widget top100Header(int rating, int tier, int rank, BuildContext context) { - Color rankBoxColor(int rankNum) { - if (rankNum == 1) { - return Color(0xFFFFB028); - } else if (rankNum < 11) { - return Color(0xFF435F7A); - } else if (rankNum < 101) { - return Color(0xFFAD5600); - } else { - return Color(0xFFDDDFE0); - } - } - - Widget masterHandle(int rating, BuildContext context) { - return ShaderMask( - shaderCallback: (rect) { - return LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0xFF7cf9ff), Color(0xFFb491ff), Color(0xFFff7ca8)], - ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); - }, - blendMode: BlendMode.srcATop, - child: Text( - 'Master $rating', - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.4 * 0.14, - fontWeight: FontWeight.bold, - ), - ), - ); - } - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - rating < 3000 - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text('${tierStr(tier)} ', - style: TextStyle( - fontSize: - MediaQuery.of(context).size.width * 0.4 * 0.14, - fontFamily: 'Pretendard', - color: ratingColor(rating), - )), - Text( - rating.toString(), - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.4 * 0.14, - fontFamily: 'Pretendard-ExtraBold', - fontWeight: FontWeight.bold, - color: ratingColor(rating), - ), - ), - ], - ) - : masterHandle(rating, context), - Spacer(), - Container( - width: MediaQuery.of(context).size.width * 0.25, - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: rankBoxColor(rank), - ), - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: TextStyle( - fontFamily: 'Pretendard-Regular', - color: - rank == 1 || 100 < rank ? Colors.black : Colors.white, - ), - children: [ - TextSpan( - text: rank < 1000 - ? '#$rank' - : '#${(rank / 1000).floor()},${(rank % 1000).toString().padLeft(3).replaceAll(' ', '0')}', - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.04, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: '\n', - ), - TextSpan( - text: - '전체 ${(((rank / UserService().userCount) * 10000).ceil() / 100).toStringAsFixed(2)}%', - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.03, - ), - ) - ]), - )) - ], - ); - } - - Widget top100Box(BuildContext context, dynamic cur) { - return CupertinoButton( - alignment: Alignment.center, - minSize: 0, - padding: EdgeInsets.zero, - color: Colors.transparent, - onPressed: () { - launchUrlString('https://www.acmicpc.net/problem/${cur['problemId']}', - mode: LaunchMode.externalApplication); - }, - child: Tooltip( - preferBelow: false, - message: cur['titleKo'], - textStyle: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.4 * 0.1, - fontFamily: 'Pretendard-Regular', - color: Colors.white, - ), - child: SvgPicture.asset('lib/assets/tiers/${cur['level']}.svg', - width: MediaQuery.of(context).size.width * 0.041))); - } - - return FutureBuilder( - future: future, - builder: (context, snapshot) { - if (snapshot.hasData) { - int count = min(100, snapshot.data?.count ?? 0); - return Column( - children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: MediaQuery.of(context).size.width * 0.02, - ), - child: top100Header(rating, tier, rank, context), - ), - const SizedBox(height: 10), - SizedBox( - height: MediaQuery.of(context).size.height * 0.5, - child: GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 10, - mainAxisExtent: MediaQuery.of(context).size.height * 0.05, - ), - physics: NeverScrollableScrollPhysics(), - itemCount: count, - itemBuilder: (BuildContext context, int index) { - return top100Box(context, snapshot.data!.items[index]); - }), - ) - ], - ); - } else { - return CupertinoActivityIndicator(); - } - }, - ); -} - -Widget tagChart(BuildContext context, AsyncSnapshot userSnapshot) { - NetworkService networkService = NetworkService(); - - return FutureBuilder>( - future: networkService.requestTagRatings(userSnapshot.data?.handle ?? ''), - builder: (context, snapshot) { - List? tags = snapshot.data; - tags?.sort((a, b) => b.rating.compareTo(a.rating)); - - List ticks = []; - List features = []; - List> data = [[]]; - - int? length = min(8, snapshot.data?.length ?? 0); - int maxTick = 0; - for (var i = 0; i < length; i++) { - features.add(snapshot.data?[i].tag['key'] ?? ''); - data[0].add(snapshot.data?[i].rating ?? 0); - maxTick = max(maxTick, data[0][i].toInt()); - } - maxTick = (maxTick + 500) ~/ 500 * 500; - while (maxTick > 0) { - ticks.add(maxTick); - maxTick -= 500; - } - if (snapshot.hasData) { - return Consumer(builder: (context, userService, child) { - int chartType = UserService().tagChartType; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Spacer(), - CupertinoSlidingSegmentedControl( - children: { - 0: Text('표'), - 1: Text('차트'), - }, - onValueChanged: (value) { - UserService().setTagChartType(value!); - }, - groupValue: chartType, - ), - ], - ), - const SizedBox(height: 10), - chartType == 0 - ? Column( - children: [ - Row( - children: [ - Text( - '태그', - style: TextStyle( - fontSize: - MediaQuery.of(context).size.width * 0.04, - color: Color(0xff8a8f95), - ), - ), - Spacer(), - Spacer(), - Spacer(), - Text( - '문제', - style: TextStyle( - fontSize: - MediaQuery.of(context).size.width * 0.04, - color: Color(0xff8a8f95), - ), - ), - Spacer(), - Spacer(), - Text( - '레이팅', - style: TextStyle( - fontSize: - MediaQuery.of(context).size.width * 0.04, - color: Color(0xff8a8f95), - ), - ), - ], - ), - Divider( - color: Colors.grey, - thickness: 0.5, - ), - ListView.separated( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width * - 0.42, - child: Text( - tags?[index].tag['displayNames'][0] - ['name'] ?? - '', - style: TextStyle( - fontSize: MediaQuery.of(context) - .size - .width * - 0.035, - color: Colors.black, - ), - ), - ), - SizedBox( - width: MediaQuery.of(context).size.width * - 0.2, - child: Text( - tags?[index].solvedCount.toString() ?? - '', - style: TextStyle( - fontSize: MediaQuery.of(context) - .size - .width * - 0.035, - color: Colors.black, - ), - ), - ), - Spacer(), - Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - 'lib/assets/tiers/${ratingToTier(tags![index].rating)}.svg', - width: MediaQuery.of(context) - .size - .width * - 0.035, - ), - SizedBox(width: 3), - SizedBox( - width: MediaQuery.of(context) - .size - .width * - 0.1, - child: Text( - tags[index].rating.toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: MediaQuery.of(context) - .size - .width * - 0.035, - color: ratingColor( - tags[index].rating), - ), - ), - ) - ], - ) - ], - ); - }, - separatorBuilder: - (BuildContext context, int index) { - return Divider(); - }, - itemCount: length), - ], - ) - : SizedBox( - height: MediaQuery.of(context).size.width * 0.6, - child: RadarChart( - ticks: ticks.reversed.toList(), - features: features, - data: data, - outlineColor: Color(0xff8a8f95), - featuresTextStyle: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.03, - color: Colors.black, - ), - graphColors: [ - ratingColor(userSnapshot.data?.rating ?? 0) - ], - ), - ) - ]); - }); - } else { - return Container(); - } - }, - ); -} - -Widget badges(BuildContext context, Future future) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - if (snapshot.hasData) { - int count = snapshot.data!.count; - - List> temp = [[], [], [], []]; - for (int i = 0; i < count; i++) { - if (snapshot.data!.items[i]['badgeCategory'] == 'achievement') { - temp[0].add(snapshot.data!.items[i]); - } else if (snapshot.data!.items[i]['badgeCategory'] == 'season') { - temp[1].add(snapshot.data!.items[i]); - } else if (snapshot.data!.items[i]['badgeCategory'] == 'event') { - temp[2].add(snapshot.data!.items[i]); - } else if (snapshot.data!.items[i]['badgeCategory'] == 'contest') { - temp[3].add(snapshot.data!.items[i]); - } - } - temp[0].sort((a, b) => a['badgeId'].compareTo(b['badgeId'])); - - List badges = [[], [], [], []]; - for (int i = 0; i < 4; i++) { - for (int j = 0; j < temp[i].length; j++) { - badges[i].add(temp[i][j]); - } - } - - Widget badgeTier(BuildContext context, dynamic badge) { - String tier = badge['badgeTier']; - bool isContest = badge['badgeCategory'] == 'contest'; - Color tierColor = Colors.white; - if (tier == 'bronze') { - tierColor = Color(0xffad5600); - } else if (tier == 'silver') { - tierColor = Color(0xff435f7a); - } else if (tier == 'gold') { - tierColor = Color(0xffec9a00); - } else if (tier == 'master') { - tierColor = Color(0xffff99d8); - } - return badge == null - ? SizedBox() - : Tooltip( - preferBelow: false, - richMessage: TextSpan(children: [ - TextSpan( - text: badge['displayName'], - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.035, - fontFamily: 'Pretendard-Regular', - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: '\n', - ), - TextSpan( - text: badge['displayDescription'], - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.03, - fontFamily: 'Pretendard-Regular', - color: Colors.white, - )) - ]), - triggerMode: TooltipTriggerMode.tap, - child: Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - ExtendedImage.network( - 'https://static.solved.ac/profile_badge/120x120/${badge['badgeId']}.png', - width: MediaQuery.of(context).size.width * 0.1, - fit: BoxFit.cover, - cache: true, - loadStateChanged: (ExtendedImageState state) { - switch (state.extendedImageLoadState) { - case LoadState.loading: - return Center( - child: CircularProgressIndicator(), - ); - case LoadState.completed: - return null; - case LoadState.failed: - return Center( - child: Icon(Icons.error), - ); - } - }, - ), - isContest - ? SizedBox.shrink() - : Positioned( - bottom: 10, - right: 10, - child: Container( - width: MediaQuery.of(context).size.width * 0.02, - height: - MediaQuery.of(context).size.width * 0.02, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - alignment: Alignment.center, - child: Container( - width: - MediaQuery.of(context).size.width * 0.014, - height: - MediaQuery.of(context).size.width * 0.014, - decoration: BoxDecoration( - color: tierColor, - shape: BoxShape.circle, - ), - ), - )), - ], - ), - ); - } - - const badgeCategory = ['도전과제', '시즌 도전과제', '이벤트', '대회']; - - Widget badgeContainer(BuildContext context, int category) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - ), - margin: EdgeInsets.only( - left: MediaQuery.of(context).size.width * 0.05), - // padding: const EdgeInsets.all(10), - child: Row( - children: [ - SvgPicture.asset( - 'lib/assets/icons/badge.svg', - width: MediaQuery.of(context).size.width * 0.04, - colorFilter: ColorFilter.mode( - CupertinoTheme.of(context).textTheme.textStyle.color!, - BlendMode.srcATop), - ), - const SizedBox(width: 5), - Text( - badgeCategory[category], - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.04, - color: Colors.black45, - ), - ), - ], - ), - ), - GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 6, - childAspectRatio: 1, - ), - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: badges[category].length, - itemBuilder: (BuildContext context, int index) { - return badgeTier(context, badges[category][index]); - }), - Divider( - color: Colors.black12, - thickness: 1, - ) - ], - ); - } - - return Column( - children: [ - const SizedBox(height: 10), - for (int i = 0; i < 4; i++) badgeContainer(context, i), - ], - ); - } else { - return CupertinoActivityIndicator(); - } - }, - ); -} - -/// ************************************************ -/// Simple Widget -/// ************************************************* - -//배경 -Widget backgroundImage(BuildContext context, Future future) { - return FutureBuilder( - future: future, - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Consumer(builder: (context, provider, child) { - bool isIllustration = provider.isIllustration; - String backgroundImageUrl = snapshot.data?.backgroundImageUrl ?? - 'https://static.solved.ac/profile_bg/abstract_001/abstract_001_light.png'; - String fallbackBackgroundImageUrl = - snapshot.data?.fallbackBackgroundImageUrl ?? backgroundImageUrl; - - return ExtendedImage.network( - isIllustration ? backgroundImageUrl : fallbackBackgroundImageUrl, - cache: true, - height: MediaQuery.of(context).size.height * 0.25, - fit: BoxFit.fitHeight, - ); - }); - } else { - return CupertinoActivityIndicator(); - } - }, - ); -} - -// 프로필 이미지 -Widget profileImage(BuildContext context, AsyncSnapshot snapshot) { - return Container( - width: 100, - height: 100, - decoration: BoxDecoration( - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: ratingColor(snapshot.data?.rating ?? 0).withOpacity(0.5), - spreadRadius: 1, - blurRadius: 10, - offset: Offset(0, 5), // changes position of shadow - ), - ], - ), - child: Stack( - clipBehavior: Clip.none, - children: [ - ClipOval( - child: ExtendedImage.network( - snapshot.data?.profileImageUrl ?? - 'https://static.solved.ac/misc/360x360/default_profile.png', - cache: true, - fit: BoxFit.cover, - )), - Positioned(bottom: -20, left: 35, child: tiers(context, snapshot)) - ], - )); -} - -// 핸들 -Widget handle(BuildContext context, AsyncSnapshot snapshot) { - return Text( - snapshot.data?.handle ?? '', - style: TextStyle( - color: CupertinoColors.black, - // color: CupertinoTheme.of(context).textTheme.textStyle.color, - fontSize: 30, - fontWeight: FontWeight.bold, - ), - ); -} - -// 소속 -// Widget organizations(BuildContext context, AsyncSnapshot snapshot) { -// List companies = []; -// List schools = []; -// List communities = []; -// for (var i = 0; i < snapshot.data!.organizations.length; i++) { -// if (snapshot.data!.organizations[i]['type'] == 'community') { -// communities.add(snapshot.data!.organizations[i]['name']); -// } else if (snapshot.data!.organizations[i]['type'] == 'company') { -// companies.add(snapshot.data!.organizations[i]['name']); -// } else { -// schools.add(snapshot.data!.organizations[i]['name']); -// } -// } -// -// return CupertinoPageScaffold( -// backgroundColor: Colors.transparent, -// child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [ -// companies.isEmpty -// ? SizedBox.shrink() -// : Container( -// margin: EdgeInsets.only(right: 5), -// child: SvgPicture.asset( -// 'lib/assets/icons/company.svg', -// color: Colors.grey, -// width: 15, -// height: 15, -// ), -// ), -// for (var i = 0; i < companies.length; i++) -// RichText( -// text: TextSpan( -// children: [ -// (0 < i) ? TextSpan(text: ", ") : TextSpan(), -// TextSpan( -// text: companies[i], -// ), -// ], -// style: TextStyle( -// color: Colors.grey, -// fontSize: MediaQuery.of(context).size.width * 0.035, -// ), -// ), -// ), -// companies.isNotEmpty && (schools.isNotEmpty || communities.isNotEmpty) -// ? SizedBox( -// width: 5, -// ) -// : SizedBox.shrink(), -// schools.isEmpty -// ? SizedBox.shrink() -// : Container( -// margin: EdgeInsets.only(right: 5), -// child: SvgPicture.asset( -// 'lib/assets/icons/school.svg', -// color: Colors.grey, -// width: 15, -// height: 15, -// ), -// ), -// for (var i = 0; i < schools.length; i++) -// RichText( -// text: TextSpan( -// children: [ -// (0 < i) ? TextSpan(text: ", ") : TextSpan(), -// TextSpan( -// text: schools[i], -// ), -// ], -// style: TextStyle( -// color: Colors.grey, -// fontSize: MediaQuery.of(context).size.width * 0.035, -// ), -// ), -// ), -// (companies.isNotEmpty || schools.isNotEmpty) && communities.isNotEmpty -// ? SizedBox.shrink() -// : SizedBox( -// width: 5, -// ), -// communities.isEmpty -// ? SizedBox.shrink() -// : Container( -// margin: EdgeInsets.only(right: 5), -// child: SvgPicture.asset( -// 'lib/assets/icons/community.svg', -// color: Colors.grey, -// width: 15, -// height: 15, -// ), -// ), -// for (var i = 0; i < communities.length; i++) -// RichText( -// text: TextSpan( -// children: [ -// (0 < i) ? TextSpan(text: ", ") : TextSpan(), -// TextSpan( -// text: communities[i], -// ), -// ], -// style: TextStyle( -// color: Colors.grey, -// fontSize: MediaQuery.of(context).size.width * 0.035, -// ), -// ), -// ), -// ])); -// } - -// 자기소개 -Widget bio(BuildContext context, AsyncSnapshot snapshot) { - return snapshot.data?.bio?.isEmpty ?? true - ? SizedBox.shrink() - : Text( - snapshot.data?.bio ?? '', - style: TextStyle( - fontSize: MediaQuery.of(context).size.width * 0.04, - ), - ); -} - -// 클래스 -Widget classes(BuildContext context, AsyncSnapshot snapshot) { - String userClass = snapshot.data?.userClass.toString() ?? ''; - if (snapshot.data?.classDecoration == "silver") { - userClass += 's'; - } else if (snapshot.data?.classDecoration == "gold") { - userClass += 'g'; - } - - return CupertinoButton( - padding: EdgeInsets.zero, - color: Colors.transparent, - onPressed: () { - launchUrlString('https://solved.ac/class', - mode: LaunchMode.externalApplication); - }, - child: SvgPicture.asset( - 'lib/assets/classes/c$userClass.svg', - width: 50, - height: 50, - )); -} - -// 티어 -Widget tiers(BuildContext context, AsyncSnapshot snapshot) { - return CupertinoPageScaffold( - backgroundColor: Colors.transparent, - child: Container( - padding: EdgeInsets.only(top: 20), - child: SvgPicture.asset( - 'lib/assets/tiers/${snapshot.data?.tier}.svg', - width: 40, - height: 40, - ), - )); -} - -// 레이팅 -Widget rating(BuildContext context, AsyncSnapshot snapshot) { - return CupertinoPageScaffold( - backgroundColor: Colors.transparent, - child: Text( - snapshot.data?.rating.toString() ?? '', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - fontStyle: FontStyle.italic, - color: Colors.grey, - ), - ), - ); -} - -// 푼 문제 수 -Widget solvedCount(BuildContext context, AsyncSnapshot snapshot) { - return Container( - width: MediaQuery.of(context).size.width / 5, - clipBehavior: Clip.none, - child: Column( - children: [ - Text( - snapshot.data?.solvedCount.toString() ?? '', - style: TextStyle( - color: Colors.black, - fontSize: MediaQuery.of(context).size.width * 0.04, - fontWeight: FontWeight.bold, - ), - ), - Text( - '해결', - style: TextStyle( - color: Colors.grey, - fontSize: MediaQuery.of(context).size.width * 0.04, - ), - ), - ], - ), - ); -} - -// 기여 수 -Widget voteCount(BuildContext context, AsyncSnapshot snapshot) { - return Container( - width: MediaQuery.of(context).size.width / 5, - clipBehavior: Clip.none, - child: Column( - children: [ - Text( - snapshot.data?.voteCount.toString() ?? '', - style: TextStyle( - color: Colors.black, - fontSize: MediaQuery.of(context).size.width * 0.04, - fontWeight: FontWeight.bold, - ), - ), - Text( - '기여', - style: TextStyle( - color: Colors.grey, - fontSize: MediaQuery.of(context).size.width * 0.04, - ), - ), - ], - ), - ); -} - -// 라이벌 수 -Widget reverseRivalCount(BuildContext context, AsyncSnapshot snapshot) { - return Container( - width: MediaQuery.of(context).size.width / 5, - clipBehavior: Clip.none, - child: Column( - children: [ - Text( - snapshot.data?.reverseRivalCount.toString() ?? '', - style: TextStyle( - color: Colors.black, - fontSize: MediaQuery.of(context).size.width * 0.04, - fontWeight: FontWeight.bold, - ), - ), - Text( - '라이벌', - style: TextStyle( - color: Colors.grey, - fontSize: MediaQuery.of(context).size.width * 0.04, - ), - ), - ], - ), - ); -} - -// 랭크 -Widget rank(BuildContext context, AsyncSnapshot snapshot) { - return CupertinoPageScaffold( - backgroundColor: Colors.transparent, - child: Container( - padding: EdgeInsets.only(top: 20), - child: Text( - snapshot.data?.rank.toString() ?? '', - ), - )); -} - -//배지 -Widget badge(BuildContext context, Future future) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - if (snapshot.hasData) { - if (snapshot.data?.badgeId == null) { - return SizedBox(); - } - - String tier = snapshot.data!.badgeTier; - bool isContest = snapshot.data!.badgeCategory == 'contest'; - Color tierColor = Colors.white; - if (tier == 'bronze') { - tierColor = Color(0xffad5600); - } else if (tier == 'silver') { - tierColor = Color(0xff435f7a); - } else if (tier == 'gold') { - tierColor = Color(0xffec9a00); - } else if (tier == 'master') { - tierColor = Color(0xffff99d8); - } - return snapshot.data?.badgeId == null - ? SizedBox() - : Tooltip( - preferBelow: false, - triggerMode: TooltipTriggerMode.tap, - richMessage: TextSpan( - children: [ - TextSpan( - text: snapshot.data!.displayName, - style: TextStyle( - fontSize: - MediaQuery.of(context).size.width * 0.4 * 0.1, - fontFamily: 'Pretendard-Regular', - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - TextSpan(text: '\n'), - TextSpan( - text: snapshot.data!.displayDescription, - style: TextStyle( - fontSize: - MediaQuery.of(context).size.width * 0.4 * 0.1, - fontFamily: 'Pretendard-Regular', - color: Colors.white, - ), - ) - ], - ), - child: Stack( - clipBehavior: Clip.none, - children: [ - ExtendedImage.network( - 'https://static.solved.ac/profile_badge/120x120/${snapshot.data!.badgeId}.png', - width: MediaQuery.of(context).size.width * 0.11, - fit: BoxFit.cover, - cache: true, - ), - isContest - ? SizedBox.shrink() - : Positioned( - bottom: -5, - right: -1, - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(100), - ), - child: Container( - width: - MediaQuery.of(context).size.width * 0.02, - height: - MediaQuery.of(context).size.width * 0.02, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - alignment: Alignment.center, - child: Container( - width: MediaQuery.of(context).size.width * - 0.014, - height: MediaQuery.of(context).size.width * - 0.014, - decoration: BoxDecoration( - color: tierColor, - shape: BoxShape.circle, - ), - ), - ), - )), - ], - ), - ); - } else { - return CupertinoActivityIndicator(); - } - }); -} - -String tierStr(int tier) { - if (tier == 1) { - return 'Bronze V'; - } else if (tier == 2) { - return 'Bronze IV'; - } else if (tier == 3) { - return 'Bronze III'; - } else if (tier == 4) { - return 'Bronze II'; - } else if (tier == 5) { - return 'Bronze I'; - } else if (tier == 6) { - return 'Silver V'; - } else if (tier == 7) { - return 'Silver IV'; - } else if (tier == 8) { - return 'Silver III'; - } else if (tier == 9) { - return 'Silver II'; - } else if (tier == 10) { - return 'Silver I'; - } else if (tier == 11) { - return 'Gold V'; - } else if (tier == 12) { - return 'Gold IV'; - } else if (tier == 13) { - return 'Gold III'; - } else if (tier == 14) { - return 'Gold II'; - } else if (tier == 15) { - return 'Gold I'; - } else if (tier == 16) { - return 'Platinum V'; - } else if (tier == 17) { - return 'Platinum IV'; - } else if (tier == 18) { - return 'Platinum III'; - } else if (tier == 19) { - return 'Platinum II'; - } else if (tier == 20) { - return 'Platinum I'; - } else if (tier == 21) { - return 'Diamond V'; - } else if (tier == 22) { - return 'Diamond IV'; - } else if (tier == 23) { - return 'Diamond III'; - } else if (tier == 24) { - return 'Diamond II'; - } else if (tier == 25) { - return 'Diamond I'; - } else if (tier == 26) { - return 'Ruby V'; - } else if (tier == 27) { - return 'Ruby IV'; - } else if (tier == 28) { - return 'Ruby III'; - } else if (tier == 29) { - return 'Ruby II'; - } else if (tier == 30) { - return 'Ruby I'; - } else if (tier == 31) { - return 'Master'; - } else { - return 'Unrated'; - } -} - -Color levelColor(int level) { - if (level == 0) { - return Color(0xFF2D2D2D); - } else if (level < 6) { - // bronze - return Color(0xffad5600); - } else if (level < 11) { - // silver - return Color(0xFF425E79); - } else if (level < 16) { - // gold - return Color(0xffec9a00); - } else if (level < 21) { - // platinum - return Color(0xff00c78b); - } else if (level < 26) { - // diamond - return Color(0xff00b4fc); - } else if (level < 31) { - // ruby - return Color(0xffff0062); - } else { - // master - return Color(0xffb300e0); - } -} - -Color ratingColor(int rating) { - if (rating < 30) { - return Color(0xFF2D2D2D); - } else if (rating < 200) { - // bronze - return Color(0xffad5600); - } else if (rating < 800) { - // silver - return Color(0xFF425E79); - } else if (rating < 1600) { - // gold - return Color(0xffec9a00); - } else if (rating < 2200) { - // platinum - return Color(0xff00c78b); - } else if (rating < 2700) { - // diamond - return Color(0xff00b4fc); - } else if (rating < 3000) { - // ruby - return Color(0xffff0062); - } else { - // master - return Color(0xffb300e0); - } -} - -int ratingToTier(int rating) { - if (3000 <= rating) { - return 31; - } else if (2950 <= rating) { - return 30; - } else if (2900 <= rating) { - return 29; - } else if (2850 <= rating) { - return 28; - } else if (2800 <= rating) { - return 27; - } else if (2700 <= rating) { - return 26; - } else if (2600 <= rating) { - return 25; - } else if (2500 <= rating) { - return 24; - } else if (2400 <= rating) { - return 23; - } else if (2300 <= rating) { - return 22; - } else if (2200 <= rating) { - return 21; - } else if (2100 <= rating) { - return 20; - } else if (2000 <= rating) { - return 19; - } else if (1900 <= rating) { - return 18; - } else if (1750 <= rating) { - return 17; - } else if (1600 <= rating) { - return 16; - } else if (1400 <= rating) { - return 15; - } else if (1250 <= rating) { - return 14; - } else if (1100 <= rating) { - return 13; - } else if (950 <= rating) { - return 12; - } else if (800 <= rating) { - return 11; - } else if (650 <= rating) { - return 10; - } else if (500 <= rating) { - return 9; - } else if (400 <= rating) { - return 8; - } else if (300 <= rating) { - return 7; - } else if (200 <= rating) { - return 6; - } else if (150 <= rating) { - return 5; - } else if (120 <= rating) { - return 4; - } else if (90 <= rating) { - return 3; - } else if (60 <= rating) { - return 2; - } else if (30 <= rating) { - return 1; - } else { - return 0; - } -} diff --git a/packages/apis/solved_api/lib/src/models/models.dart b/packages/apis/solved_api/lib/src/models/models.dart index 445163fc..3ebe4207 100644 --- a/packages/apis/solved_api/lib/src/models/models.dart +++ b/packages/apis/solved_api/lib/src/models/models.dart @@ -3,6 +3,7 @@ export 'background.dart'; export 'badge.dart'; export 'organization.dart'; export 'problem.dart'; +export 'problem_stats.dart'; export 'search_object.dart'; export 'search_suggestion.dart'; export 'site_stats.dart'; diff --git a/lib/models/user/problem_stats.dart b/packages/apis/solved_api/lib/src/models/problem_stats.dart similarity index 67% rename from lib/models/user/problem_stats.dart rename to packages/apis/solved_api/lib/src/models/problem_stats.dart index 27777dd5..e3d9d1ec 100644 --- a/lib/models/user/problem_stats.dart +++ b/packages/apis/solved_api/lib/src/models/problem_stats.dart @@ -1,28 +1,25 @@ -class ProblemStats { +class ProblemStat { final int level; final int total; final int solved; final int partial; final int tried; - final int exp; - ProblemStats({ + ProblemStat({ required this.level, required this.total, required this.solved, required this.partial, required this.tried, - required this.exp, }); - factory ProblemStats.fromJson(Map json) { - return ProblemStats( + factory ProblemStat.fromJson(Map json) { + return ProblemStat( level: json['level'], total: json['total'], solved: json['solved'], partial: json['partial'], tried: json['tried'], - exp: json['exp'], ); } } diff --git a/packages/apis/solved_api/lib/src/solved_api_client.dart b/packages/apis/solved_api/lib/src/solved_api_client.dart index 29c86d8b..bba4f9de 100644 --- a/packages/apis/solved_api/lib/src/solved_api_client.dart +++ b/packages/apis/solved_api/lib/src/solved_api_client.dart @@ -145,6 +145,29 @@ class SolvedApiClient { .toList(); } + Future> userProblemStats(String handle) async { + final userRequest = + Uri.https(_baseUrl, '/api/v3/user/problem_stats', {'handle': handle}); + + http.Response userResponse; + + try { + userResponse = await _httpClient.get(userRequest); + } on ClientException { + final bypassRequest = Uri.https( + _bypassUrl, '/solved/user/problem_stats', {'handle': handle}); + + userResponse = await _httpClient.get(bypassRequest); + } catch (e) { + throw UserRequestFailed(); + } + + final userJson = jsonDecode(userResponse.body); + + return List.from( + userJson.map((problemStats) => ProblemStat.fromJson(problemStats))); + } + Future> userTagRatings(String handle) async { final userRequest = Uri.https(_baseUrl, '/api/v3/user/tag_ratings', {'handle': handle}); diff --git a/packages/apis/solved_api/test/solved_api_test.dart b/packages/apis/solved_api/test/solved_api_test.dart index c3cdf845..e4dfacee 100644 --- a/packages/apis/solved_api/test/solved_api_test.dart +++ b/packages/apis/solved_api/test/solved_api_test.dart @@ -12015,6 +12015,261 @@ void main() { }); }); + group('userProblemStats', () { + const handle = 'w8385'; + test('makes correct http request', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn('{}'); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + try { + await apiClient.userProblemStats(handle); + } catch (_) {} + verify( + () => httpClient.get( + Uri.https( + 'solved.ac', + '/api/v3/user/problem_stats', + {'handle': handle}, + ), + ), + ).called(1); + }); + + test('returns List on valid response', () async { + final response = MockResponse(); + when(() => response.statusCode).thenReturn(200); + when(() => response.body).thenReturn(''' +[ + { + "level": 0, + "solved": 15, + "tried": 2, + "partial": 0, + "total": 6277 + }, + { + "level": 1, + "solved": 152, + "tried": 0, + "partial": 0, + "total": 154 + }, + { + "level": 2, + "solved": 300, + "tried": 0, + "partial": 0, + "total": 307 + }, + { + "level": 3, + "solved": 670, + "tried": 2, + "partial": 0, + "total": 721 + }, + { + "level": 4, + "solved": 506, + "tried": 4, + "partial": 0, + "total": 945 + }, + { + "level": 5, + "solved": 227, + "tried": 7, + "partial": 0, + "total": 799 + }, + { + "level": 6, + "solved": 214, + "tried": 4, + "partial": 0, + "total": 828 + }, + { + "level": 7, + "solved": 162, + "tried": 6, + "partial": 0, + "total": 891 + }, + { + "level": 8, + "solved": 127, + "tried": 4, + "partial": 0, + "total": 955 + }, + { + "level": 9, + "solved": 122, + "tried": 5, + "partial": 0, + "total": 942 + }, + { + "level": 10, + "solved": 109, + "tried": 4, + "partial": 0, + "total": 1055 + }, + { + "level": 11, + "solved": 109, + "tried": 10, + "partial": 0, + "total": 1126 + }, + { + "level": 12, + "solved": 96, + "tried": 12, + "partial": 1, + "total": 1523 + }, + { + "level": 13, + "solved": 64, + "tried": 16, + "partial": 1, + "total": 1428 + }, + { + "level": 14, + "solved": 47, + "tried": 6, + "partial": 1, + "total": 1235 + }, + { + "level": 15, + "solved": 63, + "tried": 10, + "partial": 0, + "total": 1093 + }, + { + "level": 16, + "solved": 152, + "tried": 13, + "partial": 1, + "total": 1269 + }, + { + "level": 17, + "solved": 107, + "tried": 10, + "partial": 0, + "total": 1217 + }, + { + "level": 18, + "solved": 80, + "tried": 11, + "partial": 1, + "total": 1296 + }, + { + "level": 19, + "solved": 29, + "tried": 2, + "partial": 0, + "total": 1210 + }, + { + "level": 20, + "solved": 14, + "tried": 4, + "partial": 0, + "total": 960 + }, + { + "level": 21, + "solved": 15, + "tried": 3, + "partial": 0, + "total": 1010 + }, + { + "level": 22, + "solved": 5, + "tried": 3, + "partial": 0, + "total": 942 + }, + { + "level": 23, + "solved": 1, + "tried": 0, + "partial": 0, + "total": 642 + }, + { + "level": 24, + "solved": 2, + "tried": 1, + "partial": 0, + "total": 487 + }, + { + "level": 25, + "solved": 1, + "tried": 0, + "partial": 0, + "total": 376 + }, + { + "level": 26, + "solved": 1, + "tried": 0, + "partial": 0, + "total": 301 + }, + { + "level": 27, + "solved": 0, + "tried": 1, + "partial": 0, + "total": 156 + }, + { + "level": 28, + "solved": 0, + "tried": 0, + "partial": 0, + "total": 100 + }, + { + "level": 29, + "solved": 0, + "tried": 0, + "partial": 0, + "total": 37 + }, + { + "level": 30, + "solved": 0, + "tried": 0, + "partial": 0, + "total": 28 + } +] +'''); + when(() => httpClient.get(any())).thenAnswer((_) async => response); + + final actual = await apiClient.userProblemStats(handle); + expect(actual, isA>()); + for (final stat in actual) { + expect(stat, isA()); + } + }); + }); + group('userTagRatings', () { const handle = 'w8385'; test('makes correct http request', () async { @@ -12065,7 +12320,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 70, @@ -12615,7 +12870,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 118, @@ -12820,7 +13075,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 1373, @@ -12854,7 +13109,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 1335, @@ -12888,7 +13143,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 263, @@ -13009,7 +13264,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 14, @@ -13043,7 +13298,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 120, @@ -13077,7 +13332,7 @@ void main() { } ], "aliases": [ - + ] }, "solvedCount": 161, @@ -13501,7 +13756,7 @@ void main() { } ], "aliases": [ - + ] }, { @@ -13607,7 +13862,7 @@ void main() { } ], "aliases": [ - + ] }, { @@ -13633,12 +13888,12 @@ void main() { } ], "aliases": [ - + ] } ], "metadata": { - + } } ] @@ -13812,7 +14067,7 @@ void main() { } ], "aliases": [ - + ] } ] @@ -13880,7 +14135,7 @@ void main() { "needsAclToRegister": false, "cancellationDisabled": false, "languages": [ - + ], "isRegistered": false } diff --git a/packages/repositories/user_repository/lib/src/user_repository.dart b/packages/repositories/user_repository/lib/src/user_repository.dart index fe365624..f7a1e6e5 100644 --- a/packages/repositories/user_repository/lib/src/user_repository.dart +++ b/packages/repositories/user_repository/lib/src/user_repository.dart @@ -28,6 +28,10 @@ class UserRepository { return await _solvedApiClient.userTop100(handle); } + Future> getProblemStats(String handle) async { + return await _solvedApiClient.userProblemStats(handle); + } + Future> getTagRatings(String handle) async { return await _solvedApiClient.userTagRatings(handle); } diff --git a/packages/repositories/user_repository/test/user_repository_test.dart b/packages/repositories/user_repository/test/user_repository_test.dart index 61561ed9..5756709a 100644 --- a/packages/repositories/user_repository/test/user_repository_test.dart +++ b/packages/repositories/user_repository/test/user_repository_test.dart @@ -15,6 +15,8 @@ class MockStreak extends Mock implements solved_api.Streak {} class MockProblem extends Mock implements solved_api.Problem {} +class MockProblemStats extends Mock implements solved_api.ProblemStat {} + class MockTagRating extends Mock implements solved_api.TagRating {} class MockBackground extends Mock implements solved_api.Background {} @@ -90,7 +92,7 @@ void main() { group('getStreak', () { const handle = 'w8385'; - const topic = 'default'; + const topic = 'today-solved'; test('calls userGrass with correct handle and topic', () async { try { @@ -108,6 +110,25 @@ void main() { }); }); + group('getProblemStats', () { + const handle = 'w8385'; + + test('calls userProblemStats with correct handle', () async { + try { + await userRepository.getProblemStats(handle); + } catch (_) {} + verify(() => solvedApiClient.userProblemStats(handle)).called(1); + }); + + test('returns correct problemStats on success', () async { + final problemStats = [MockProblemStats()]; + when(() => solvedApiClient.userProblemStats(handle)) + .thenAnswer((_) async => problemStats); + + expect(await userRepository.getProblemStats(handle), problemStats); + }); + }); + group('getTopProblems', () { const handle = 'w8385'; diff --git a/pubspec.yaml b/pubspec.yaml index 75a21dde..e1c9350b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,12 +15,13 @@ dependencies: cupertino_icons: ^1.0.5 equatable: ^2.0.5 extended_image: ^8.1.0 - fluttertoast: ^8.2.1 + 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 flutter_radar_chart: ^0.2.1 + pie_chart: ^5.4.0 flutter_staggered_grid_view: ^0.7.0 flutter_svg: ^2.0.7 flutter_widget_from_html_core: ^0.14.11