From dc2b77a43aef8e54ab15588612e57c202ac3ab97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EA=B7=BC=ED=98=95?= Date: Wed, 6 Sep 2023 11:23:12 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=ED=9A=8C=ED=83=AD=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- my_solved/lib/assets/venues/ac arena.png | Bin 0 -> 5509 bytes my_solved/lib/main.dart | 1 + my_solved/lib/models/contest.dart | 44 + my_solved/lib/services/contest_service.dart | 8 +- my_solved/lib/services/network_service.dart | 120 ++- my_solved/lib/views/contest_view.dart | 839 ++++++-------------- my_solved/lib/views/home_view.dart | 2 + 7 files changed, 433 insertions(+), 581 deletions(-) create mode 100644 my_solved/lib/assets/venues/ac arena.png diff --git a/my_solved/lib/assets/venues/ac arena.png b/my_solved/lib/assets/venues/ac arena.png new file mode 100644 index 0000000000000000000000000000000000000000..8875ec098788f332ffda8fe2e3e9f5d6c7ce4334 GIT binary patch literal 5509 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 { ), theme: const CupertinoThemeData( brightness: Brightness.light, + scaffoldBackgroundColor: Color(0xFFF9F9F9), textTheme: CupertinoTextThemeData( textStyle: TextStyle( fontFamily: 'Pretendard', diff --git a/my_solved/lib/models/contest.dart b/my_solved/lib/models/contest.dart index 1bef15fd..24ef4f96 100644 --- a/my_solved/lib/models/contest.dart +++ b/my_solved/lib/models/contest.dart @@ -1,3 +1,5 @@ +import 'package:html/dom.dart'; + class Contest { final String venue; final String name; @@ -12,4 +14,46 @@ class Contest { 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/my_solved/lib/services/contest_service.dart b/my_solved/lib/services/contest_service.dart index ef153f8e..95b63c38 100644 --- a/my_solved/lib/services/contest_service.dart +++ b/my_solved/lib/services/contest_service.dart @@ -10,7 +10,7 @@ class ContestService extends ChangeNotifier { bool _disposed = false; - Map showVenues = { + Map showVenues = { 'AtCoder': false, 'BOJ Open': false, 'Codeforces': false, @@ -37,10 +37,12 @@ class ContestService extends ChangeNotifier { Future init() async { await _initializeContest(); + notifyListeners(); } Future _initializeContest() async { initContestShow(); + notifyListeners(); } Future initContestShow() async { @@ -52,6 +54,10 @@ class ContestService extends ChangeNotifier { notifyListeners(); } + Future> getContestShow() async { + return showVenues; + } + Future toggleContestShow(String venue) async { final prefs = await SharedPreferences.getInstance(); final show = prefs.getBool('show$venue') ?? false; diff --git a/my_solved/lib/services/network_service.dart b/my_solved/lib/services/network_service.dart index 5056a234..8ad49dba 100644 --- a/my_solved/lib/services/network_service.dart +++ b/my_solved/lib/services/network_service.dart @@ -17,6 +17,8 @@ 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 '../models/contest.dart'; + class NetworkService { static final NetworkService _instance = NetworkService._privateConstructor(); @@ -210,14 +212,124 @@ class NetworkService { } } - Future requestContests() async { - final response = + 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 element + .getElementsByClassName('col-md-12')[2] + .getElementsByTagName('tbody') + .first + .getElementsByTagName('tr') + .toList() + .map((e) { + return Contest.fromElement(e); + }).toList(); + } else { + return []; + } + } + + 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 = element + .getElementsByTagName('td')[3] + .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')[4] + .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: '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 ended = await http + .get(Uri.parse("https://www.acmicpc.net/contest/official/list")); + dom.Document docEnded = parser.parse(ended.body); + + 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) { - dom.Document document = parser.parse(response.body); - return document; + 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'); } diff --git a/my_solved/lib/views/contest_view.dart b/my_solved/lib/views/contest_view.dart index f2f75367..3d083129 100644 --- a/my_solved/lib/views/contest_view.dart +++ b/my_solved/lib/views/contest_view.dart @@ -2,12 +2,12 @@ import 'package:extended_image/extended_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:html/dom.dart' as dom; 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 { @@ -22,610 +22,297 @@ class _ContestViewState extends State { NetworkService networkService = NetworkService(); NotificationService notificationService = NotificationService(); - Map _selectedVenues = ContestService().showVenues; + Map _selectedVenues = {}; + Set _arenaContests = {}; + + int _selectedSegment = 0; + Map contestStatus = { + 0: '진행중인 대회', + 1: '예정된 대회', + 2: '종료된 대회', + }; void updateSelectedVenues() { _selectedVenues = contestService.showVenues; + } + + void _updateSelectedSegment(int value) { setState(() { - _selectedVenues = _selectedVenues; + _selectedSegment = value; }); } - List currentContests = []; - List futureContests = []; - @override Widget build(BuildContext context) { + networkService.requestArenaContests().then((value) { + _arenaContests = value; + }); + return CupertinoPageScaffold( resizeToAvoidBottomInset: false, child: SafeArea( - child: SingleChildScrollView( - child: contestList(), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + venueFilter(context), + contestTap(context), + contestBody(_selectedSegment), + ], ), - )); + ))); } } extension _ContestStateExtension on _ContestViewState { - Widget contestList() { - return FutureBuilder( - future: networkService.requestContests(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - if (snapshot.data.body.getElementsByClassName('col-md-12').length < - 5) { - futureContests = snapshot.data.body - .getElementsByClassName('col-md-12')[2] - .getElementsByTagName('tbody') - .first - .getElementsByTagName('tr') - .toList() - .map((e) { - String venue = e.getElementsByTagName('td')[0].text; - String name = e.getElementsByTagName('td')[1].text; - String? url; - if (e - .getElementsByTagName('td')[1] - .getElementsByTagName('a') - .isNotEmpty) { - url = e - .getElementsByTagName('td')[1] - .getElementsByTagName('a') - .first - .attributes['href']; - } - List startTimeList = e - .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 = e - .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: venue, - name: name, - url: url, - startTime: startTime, - endTime: endTime); - }).toList(); - } else { - currentContests = snapshot.data.body - .getElementsByClassName('col-md-12')[2] - .getElementsByTagName('tbody') - .first - .getElementsByTagName('tr') - .toList() - .map((e) { - String venue = e.getElementsByTagName('td')[0].text; - String name = e.getElementsByTagName('td')[1].text; - String? url; - if (e - .getElementsByTagName('td')[1] - .getElementsByTagName('a') - .isNotEmpty) { - url = e - .getElementsByTagName('td')[1] - .getElementsByTagName('a') - .first - .attributes['href']; - } - List startTimeList = e - .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 = e - .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: venue, - name: name, - url: url, - startTime: startTime, - endTime: endTime); - }).toList(); + /// 대회 필터링 위젯 + Widget venueFilter(BuildContext context) { + updateSelectedVenues(); - futureContests = snapshot.data.body - .getElementsByClassName('col-md-12')[4] - .getElementsByTagName('tbody') - .first - .getElementsByTagName('tr') - .toList() - .map((e) { - String venue = e.getElementsByTagName('td')[0].text; - String name = e.getElementsByTagName('td')[1].text; - String? url; - if (e - .getElementsByTagName('td')[1] - .getElementsByTagName('a') - .isNotEmpty) { - url = e - .getElementsByTagName('td')[1] - .getElementsByTagName('a') - .first - .attributes['href']; - } - List startTimeList = e - .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(); + 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); + bool isSelected = snapshot.data![venue]!; + bool isOthers = venue == 'Others'; - List endTimeList = e - .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: venue, - name: name, - url: url, - startTime: startTime, - endTime: endTime); - }).toList(); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FutureBuilder(builder: (context, snapshot) { - return Container( - margin: EdgeInsets.only(left: 10, right: 10, top: 20), - height: 40, - child: ListView.builder( - itemCount: _selectedVenues.length, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemBuilder: (context, index) { - String venue = _selectedVenues.keys.elementAt(index); - bool isSelected = _selectedVenues[venue]!; - 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: 20, - color: isSelected - ? Colors.grey[200] - : Colors.grey[400], - ) - : ExtendedImage.asset( - 'lib/assets/venues/${venue.toLowerCase()}.png', - fit: BoxFit.fill, - width: 20, - ), - SizedBox( - width: 5, - ), - Text( - venue, - style: TextStyle( - fontSize: 16, - color: isSelected - ? Colors.grey[200] - : Colors.grey[400]), + 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], ) - ], - )), - onPressed: () { - ContestService().toggleContestShow(venue); - updateSelectedVenues(); - }, - ); - }, - ), + : 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!; + }); + }, ); - }), - Container( - decoration: BoxDecoration( - color: Color(0xfff6f6f6), - borderRadius: BorderRadius.circular(10), - ), - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), - margin: - EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: double.infinity, - child: Text( - '진행 중${currentContests.isEmpty ? '인 대회가 없습니다.' : ''}', - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - if (currentContests.isNotEmpty) - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: currentContests.length, - itemBuilder: (BuildContext context, int index) { - final contest = currentContests[index]; - Color linkColor = contest.url == null - ? Colors.black - : Colors.blue; - - final String startYear = - contest.startTime.year.toString(); - final String startMonth = contest.startTime.month - .toString() - .padLeft(2, '0'); - final String startDay = contest.startTime.day - .toString() - .padLeft(2, '0'); - final String startHour = contest.startTime.hour - .toString() - .padLeft(2, '0'); - final String startMinute = contest.startTime.minute - .toString() - .padLeft(2, '0'); - - final String endYear = - contest.endTime.year.toString(); - final String endMonth = contest.endTime.month - .toString() - .padLeft(2, '0'); - final String endDay = - contest.endTime.day.toString().padLeft(2, '0'); - final String endHour = - contest.endTime.hour.toString().padLeft(2, '0'); - final String endMinute = contest.endTime.minute - .toString() - .padLeft(2, '0'); - - bool isOther = - _selectedVenues.keys.contains(contest.venue) == - false; - if (isOther && _selectedVenues['Others'] == false) { - return Container(); - } + }, + ), + ); + }); + } - bool show = _selectedVenues[contest.venue]!; - return show - ? Container( - decoration: const BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - border: Border( - top: BorderSide( - width: 1.0, color: Colors.black26), - bottom: BorderSide( - width: 1.0, color: Colors.black26), - left: BorderSide( - width: 1.0, color: Colors.black26), - right: BorderSide( - width: 1.0, color: Colors.black26), - ), - ), - margin: const EdgeInsets.only( - top: 5, bottom: 5), - padding: const EdgeInsets.only( - top: 10, bottom: 10, left: 20), - child: CupertinoButton( - alignment: Alignment.centerLeft, - minSize: 0, - padding: EdgeInsets.zero, - onPressed: () async { - if (contest.url != null) { - await launchUrlString(contest.url!, - mode: LaunchMode - .externalApplication); - } - }, - child: Row( - children: [ - Expanded( - flex: 4, - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: const TextStyle( - color: Colors.black87, - ), - children: [ - TextSpan( - text: contest.venue, - style: TextStyle( - color: Colors.black87, - fontSize: 12, - fontWeight: - FontWeight.bold, - ), - ), - const TextSpan( - text: '\n', - ), - TextSpan( - text: contest.name, - style: TextStyle( - color: linkColor, - fontSize: 15, - fontWeight: - FontWeight.bold, - ), - ), - const TextSpan( - text: '\n', - ), - TextSpan( - text: - '$startYear-$startMonth-$startDay $startHour:$startMinute ~ $endYear-$endMonth-$endDay $endHour:$endMinute', - style: const TextStyle( - fontSize: 10, - ), - ), - ], - )), - ), - Expanded( - flex: 1, - child: Icon( - Icons.arrow_forward_ios, - size: 15, - color: Colors.blue, - ), - ) - ], - ))) - : Container(); - }, - ), - ], - )), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '예정', - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold), - ), - if (futureContests.isEmpty) - const Text( - '예정된 대회가 없습니다.', - style: TextStyle(fontSize: 15), - ) - else - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: futureContests.length, - itemBuilder: (BuildContext context, int index) { - final contest = futureContests[index]; - Color linkColor = contest.url == null - ? Colors.black - : Colors.blue; - final startYear = contest.startTime.year.toString(); - final startMonth = contest.startTime.month - .toString() - .padLeft(2, '0'); - final startDay = contest.startTime.day - .toString() - .padLeft(2, '0'); - final startHour = contest.startTime.hour - .toString() - .padLeft(2, '0'); - final startMinute = contest.startTime.minute - .toString() - .padLeft(2, '0'); + /// 대회 탭 + Widget contestTap(BuildContext context) { + return UnderlineSegmentControl( + children: contestStatus, + fontSize: 14, + onValueChanged: (value) { + _updateSelectedSegment(value); + }); + } - final endYear = contest.endTime.year.toString(); - final endMonth = contest.endTime.month - .toString() - .padLeft(2, '0'); - final endDay = - contest.endTime.day.toString().padLeft(2, '0'); - final endHour = - contest.endTime.hour.toString().padLeft(2, '0'); - final endMinute = contest.endTime.minute - .toString() - .padLeft(2, '0'); + /// 대회 목록 + 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(); - bool isOther = - _selectedVenues.keys.contains(contest.venue) == - false; - if (isOther && _selectedVenues['Others'] == false) { - return Container(); - } + 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))); + } - bool show = _selectedVenues[contest.venue] ?? true; - return show - ? Container( - decoration: const BoxDecoration( - border: Border( - top: BorderSide( - width: 1.0, color: Colors.black26), - bottom: BorderSide( - width: 1.0, color: Colors.black26), - left: BorderSide( - width: 1.0, color: Colors.black26), - right: BorderSide( - width: 1.0, color: Colors.black26), - ), - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - margin: const EdgeInsets.only( - top: 5, bottom: 5), - padding: const EdgeInsets.only( - top: 10, bottom: 10, left: 20), - child: Row( - mainAxisAlignment: - MainAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: CupertinoButton( - alignment: Alignment.centerLeft, - padding: EdgeInsets.zero, - minSize: 0, - onPressed: () { - if (contest.url != null) { - launchUrlString(contest.url!, - mode: LaunchMode - .externalApplication); - } - }, - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: const TextStyle( - color: Colors.black87, - ), - children: [ - TextSpan( - text: contest.venue, - style: TextStyle( - color: Colors.black87, - fontSize: 12, - fontWeight: - FontWeight.bold, - ), - ), - const TextSpan( - text: '\n', - ), - TextSpan( - text: contest.name, - style: TextStyle( - color: linkColor, - fontSize: 15, - fontWeight: - FontWeight.bold, - ), - ), - const TextSpan( - text: '\n', - ), - TextSpan( - text: - '$startYear-$startMonth-$startDay $startHour:$startMinute ~ $endYear-$endMonth-$endDay $endHour:$endMinute', - style: const TextStyle( - fontSize: 10, - ), - ), - ], - )), - ), - ), - Expanded( - flex: 1, - child: FutureBuilder( - future: notificationService - .getContestPush(contest.name), - builder: (BuildContext context, - AsyncSnapshot - snapshot) { - if (snapshot.hasData) { - bool isOn = snapshot.data!; - return CupertinoButton( - onPressed: () async { - notificationService - .toggleContestPush( - contest); - Fluttertoast.showToast( - msg: isOn - ? '알림이 해제되었습니다.' - : '알림이 설정되었습니다.'); - // ignore: invalid_use_of_protected_member - setState(() { - notificationService - .setContestPush( - contest.name, - !isOn); - }); - }, - padding: EdgeInsets.zero, - minSize: 0, - child: Icon( - CupertinoIcons.alarm, - color: snapshot.data! - ? CupertinoTheme.of( - context) - .main - : Colors.black26, - ), - ); - } else { - return const SizedBox(); - } - }, - )) - ], - )) - : Container(); - }, - ) - ], - )) - ], - ); + 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), + ], + )); } else { return const Center(child: CupertinoActivityIndicator()); } }, ); } + + /// 대회 위젯 + Widget contest(BuildContext context, Contest contest) { + 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) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ExtendedImage.asset( + 'lib/assets/venues/${contest.venue.toLowerCase()}.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), + 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/my_solved/lib/views/home_view.dart b/my_solved/lib/views/home_view.dart index a4367b17..39ef73ad 100644 --- a/my_solved/lib/views/home_view.dart +++ b/my_solved/lib/views/home_view.dart @@ -1,4 +1,5 @@ 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'; @@ -25,6 +26,7 @@ class _HomeViewState extends State { }); return CupertinoPageScaffold( + backgroundColor: Colors.white, child: SafeArea( child: SingleChildScrollView( scrollDirection: Axis.vertical,