diff --git a/assets/icons/OTL.svg b/assets/icons/OTL.svg new file mode 100644 index 00000000..da4a0ab3 --- /dev/null +++ b/assets/icons/OTL.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/addLecture.svg b/assets/icons/addLecture.svg new file mode 100644 index 00000000..fb450b3d --- /dev/null +++ b/assets/icons/addLecture.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/alert.svg b/assets/icons/alert.svg new file mode 100644 index 00000000..e7cb8914 --- /dev/null +++ b/assets/icons/alert.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/deleteLecture.svg b/assets/icons/deleteLecture.svg new file mode 100644 index 00000000..22e4ef49 --- /dev/null +++ b/assets/icons/deleteLecture.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/overlap.svg b/assets/icons/overlap.svg new file mode 100644 index 00000000..19a8ae50 --- /dev/null +++ b/assets/icons/overlap.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/timetable.svg b/assets/icons/timetable.svg new file mode 100644 index 00000000..6b936aee --- /dev/null +++ b/assets/icons/timetable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/translations/en.json b/assets/translations/en.json index 83cab03d..8d1efffa 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -65,15 +65,20 @@ } }, "timetable": { - "ask_delete_lecture": "Do you want to delete class '{}'?", - "ask_delete_tab": "Do you really want to delete '{}'?", "my_tab": "My Table", "tab": "Table {}", - "add_lecture": "Add Lecture", - "remove_lecture": "Remove Lecture", "dialog": { "add_lecture": "Add Lecture", - "ask_add_lecture": "There is a lecture with overlapping hours. If added, the previous lecture will be deleted.\nDo you want to add new lecture to the timetable?" + "add_overlapping_lecture": "Add Overlapping Lecture", + "delete_lecture": "Delete Lecture", + "delete_tab": "Delete Table", + "ask_add_lecture": "Do you want to add lecture '{lecture}'?", + "ask_add_lecture_with_tab": "Do you want to add lecture '{lecture}' to {timetable}?", + "ask_add_overlapping_lecture": "Time overlaps with {lectures}. If you add '{lecture}', the lecture(s) will be deleted. Do you want to add it to the timetable?", + "ask_add_overlapping_lecture_with_tab": "Time overlaps with {lectures} in {timetable}. If you add '{lecture}', the lecture(s) will be deleted. Do you want to add it to the timetable?", + "ask_delete_lecture": "Do you want to delete lecture '{lecture}'?", + "ask_delete_lecture_with_tab": "Do you want to delete lecture '{lecture}' from {timetable}?", + "ask_delete_tab": "Do you really want to delete {timetable}?" }, "tab_menu": { "copy": "Copy Timetable", @@ -130,7 +135,12 @@ "speech": "Speech", "like": "Like", "report": "Report", - "expand": "more" + "expand": "more", + "mailto": { + "subject": "[Report Review]", + "body_reason": "[Reason for Reporting]\n*Please describe a reason for reporting.\n---------------------------------\n\n\n\n---------------------------------\n\n", + "body_info": "[Review Information]\n*This information has been filled automatically. Please do NOT edit.\nLecture: {title} ({oldCode})\nSemester: {semesterTitle}\nProf.: {professors}\nContent:\n{content}\n" + } }, "user": { "name": "Name", @@ -154,10 +164,15 @@ "send_anonymously": "Send anonymously", "send_anonymously_desc": "Not including user ID in error logs", "reset_all": "Reset all settings", - "reset_all_desc": "Are you really want to reset all data except login data?", + "dialog": { + "reset": "Reset", + "reset_settings": "Reset settings", + "reset_settings_desc": "Are you really want to reset all data? However, it will be reset except for login data." + }, "throw_test": "Throw Test Exception", "throw_test_desc": "for testing firebase crashlytics", - "about": "About" + "about": "About", + "view_licenses": "View Licenses" }, "department": { "department": "Dept.", diff --git a/assets/translations/ko.json b/assets/translations/ko.json index 97c5a78f..98bc1c3d 100644 --- a/assets/translations/ko.json +++ b/assets/translations/ko.json @@ -4,7 +4,7 @@ "alert": "경고", "cancel": "취소", "delete": "삭제", - "add": "추가하기", + "add": "추가", "reset": "초기화", "reset_all": "전체 초기화", "search": "검색", @@ -65,15 +65,20 @@ } }, "timetable": { - "ask_delete_lecture": "'{}' 수업을 삭제하시겠습니까?", - "ask_delete_tab": "'{}'을(를) 정말 삭제하시겠습니까?", "my_tab": "내 시간표", "tab": "시간표 {}", - "add_lecture": "시간표에 추가", - "remove_lecture": "시간표에서 제거", "dialog": { "add_lecture": "수업 추가", - "ask_add_lecture": "시간이 겹치는 수업이 있습니다. 추가하시면 해당 수업은 삭제됩니다.\n시간표에 추가하시겠습니까?" + "add_overlapping_lecture": "겹치는 수업 추가", + "delete_lecture": "수업 삭제", + "delete_tab": "시간표 삭제", + "ask_add_lecture": "'{lecture}' 수업을 추가하시겠습니까?", + "ask_add_lecture_with_tab": "'{lecture}' 수업을 {timetable}에 추가하시겠습니까?", + "ask_add_overlapping_lecture": "{lectures}와(과) 시간이 겹칩니다. '{lecture}'을(를) 추가하시면 해당 수업은 삭제됩니다. 시간표에 추가하시겠습니까?", + "ask_add_overlapping_lecture_with_tab": "{timetable}의 {lectures}와(과) 시간이 겹칩니다. '{lecture}'을(를) 추가하시면 해당 수업은 삭제됩니다. 시간표에 추가하시겠습니까?", + "ask_delete_lecture": "'{lecture}' 수업을 삭제하시겠습니까?", + "ask_delete_lecture_with_tab": "'{lecture}' 수업을 {timetable}에서 삭제하시겠습니까?", + "ask_delete_tab": "{timetable}을(를) 정말 삭제하시겠습니까?" }, "tab_menu": { "copy": "시간표 복제하기", @@ -130,7 +135,12 @@ "speech": "강의", "like": "추천", "report": "신고하기", - "expand": "더 보기" + "expand": "더 보기", + "mailto": { + "subject": "[후기 신고]", + "body_reason": "[신고 사유]\n*후기를 신고한 이유를 서술해 주세요.\n---------------------------------\n\n\n\n---------------------------------\n\n", + "body_info": "[신고할 후기 정보]\n*자동으로 작성된 후기 정보입니다. 원활한 신고를 위해서 수정하지 말아주세요.\n과목: {title} ({oldCode})\n학기: {semesterTitle}\n교수: {professors}\n내용:\n{content}\n" + } }, "user": { "name": "이름", @@ -142,7 +152,7 @@ "logout": "로그아웃", "delete_account": "계정 삭제", "ask_delete_account": "계정을 정말 삭제하시겠습니까?", - "account_deleted": "계정 삭제됨", + "account_deleted": "삭제된 계정", "deleted_account": "삭제된 계정입니다." }, "settings": { @@ -154,10 +164,15 @@ "send_anonymously": "익명으로 전송", "send_anonymously_desc": "오류 로그에 사용자 ID를 포함하지 않고 익명으로 전송합니다.", "reset_all": "모든 설정 데이터 초기화", - "reset_all_desc": "정말 모든 설정 데이터를 초기화하시겠습니까? 로그인 정보는 초기화되지 않습니다.", + "dialog": { + "reset": "초기화", + "reset_settings": "설정 초기화", + "reset_settings_desc": "정말 모든 설정 데이터를 초기화하시겠습니까? 단, 로그인 정보는 제외하고 초기화됩니다." + }, "throw_test": "테스트 오류 발생", "throw_test_desc": "Firebase crashlytics 테스트용", - "about": "정보" + "about": "정보", + "view_licenses": "라이선스 보기" }, "department": { "department": "학과", diff --git a/lib/constants/color.dart b/lib/constants/color.dart index 21768701..fe208309 100644 --- a/lib/constants/color.dart +++ b/lib/constants/color.dart @@ -10,6 +10,7 @@ class OTLColor { static const grayD = Color(0xFFDDDDDD); static const grayE = Color(0xFFEEEEEE); static const grayF = Color(0xFFFFFFFF); + static const barrier = Color(0x40000000); static const pinksLight = Color(0xFFF9F0F0); static const pinksSub = Color(0xFFF6C5CD); diff --git a/lib/constants/text_styles.dart b/lib/constants/text_styles.dart index c7d0fe41..b4f29c88 100644 --- a/lib/constants/text_styles.dart +++ b/lib/constants/text_styles.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; -const regularBase = TextStyle(letterSpacing: 0.15); +const regularBase = TextStyle( + letterSpacing: 0.15, + leadingDistribution: TextLeadingDistribution.even, +); final labelRegular = regularBase.copyWith( fontSize: 12.0, @@ -31,16 +34,3 @@ final displayRegular = regularBase.copyWith( height: 1.6, ); final displayBold = displayRegular.copyWith(fontWeight: FontWeight.bold); - -final evenLabelRegular = labelRegular.copyWith( - leadingDistribution: TextLeadingDistribution.even, -); -final evenBodyRegular = bodyRegular.copyWith( - leadingDistribution: TextLeadingDistribution.even, -); -final evenBodyBold = bodyBold.copyWith( - leadingDistribution: TextLeadingDistribution.even, -); -final evenTitleBold = titleBold.copyWith( - leadingDistribution: TextLeadingDistribution.even, -); diff --git a/lib/constants/url.dart b/lib/constants/url.dart index e5f8763e..9cfb2780 100644 --- a/lib/constants/url.dart +++ b/lib/constants/url.dart @@ -22,3 +22,5 @@ const API_LIKED_REVIEW_URL = API_URL + "/users/{user_id}/liked-reviews"; const API_SHARE_URL = API_URL + "share/timetable/{share_type}"; enum ShareType { image, ical } + +const CONTACT = "otlplus@sparcs.org"; diff --git a/lib/main.dart b/lib/main.dart index ea4b4bb3..58365a6c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -127,7 +127,7 @@ class OTLFirebaseApp extends StatelessWidget { final base = ThemeData( fontFamily: 'NotoSansKR', primarySwatch: createMaterialColor(OTLColor.pinksMain), - canvasColor: Colors.white, + canvasColor: OTLColor.grayF, iconTheme: const IconThemeData(color: OTLColor.gray3), inputDecorationTheme: const InputDecorationTheme( border: InputBorder.none, diff --git a/lib/pages/course_detail_page.dart b/lib/pages/course_detail_page.dart index a4136721..f01a68ea 100644 --- a/lib/pages/course_detail_page.dart +++ b/lib/pages/course_detail_page.dart @@ -134,7 +134,7 @@ class CourseDetailPage extends StatelessWidget { : (isEn ? (professor.nameEn == '' ? professor.name : professor.nameEn) : professor.name), - style: evenLabelRegular, + style: labelRegular, ), selected: (professor == null ? selectedFilter == "ALL" diff --git a/lib/pages/lecture_detail_page.dart b/lib/pages/lecture_detail_page.dart index f99dbbed..39fb2067 100644 --- a/lib/pages/lecture_detail_page.dart +++ b/lib/pages/lecture_detail_page.dart @@ -2,8 +2,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_web_browser/flutter_web_browser.dart'; import 'package:otlplus/constants/text_styles.dart'; +import 'package:otlplus/extensions/semester.dart'; import 'package:otlplus/models/review.dart'; import 'package:otlplus/pages/course_detail_page.dart'; +import 'package:otlplus/widgets/otl_dialog.dart'; import 'package:otlplus/widgets/responsive_button.dart'; import 'package:otlplus/utils/navigator.dart'; import 'package:otlplus/widgets/otl_scaffold.dart'; @@ -98,41 +100,55 @@ class LectureDetailPage extends StatelessWidget { padding: const EdgeInsets.all(16), onTap: () { final timetableModel = context.read(); + final isKo = context.locale == Locale('ko'); + final lectureTitle = isKo ? lecture.title : lecture.titleEn; + final timetable = + '${timetableModel.selectedSemester.title} ${'timetable.tab'.tr( + args: [timetableModel.selectedIndex.toString()], + )}'; if (isAdded) { - timetableModel.removeLecture(lecture: lecture); + OTLNavigator.pushDialog( + context: context, + builder: (_) => OTLDialog( + type: OTLDialogType.deleteLectureWithTab, + namedArgs: {'lecture': lectureTitle, 'timetable': timetable}, + onTapPos: () => timetableModel.removeLecture(lecture: lecture), + ), + ); } else { timetableModel.addLecture( lecture: lecture, + noOverlap: () async { + bool result = false; + + await OTLNavigator.pushDialog( + context: context, + builder: (_) => OTLDialog( + type: OTLDialogType.addLectureWithTab, + namedArgs: {'lecture': lectureTitle, 'timetable': timetable}, + onTapPos: () => result = true, + ), + ); + + return result; + }, onOverlap: (lectures) async { bool result = false; await OTLNavigator.pushDialog( context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: Text("timetable.dialog.add_lecture".tr()), - content: Text("timetable.dialog.ask_add_lecture".tr()), - actions: [ - IconTextButton( - padding: EdgeInsets.all(12), - text: 'common.cancel'.tr(), - color: OTLColor.pinksMain, - onTap: () { - result = false; - OTLNavigator.pop(context); - }, - ), - IconTextButton( - padding: EdgeInsets.all(12), - text: 'common.add'.tr(), - color: OTLColor.pinksMain, - onTap: () { - result = true; - OTLNavigator.pop(context); - }, - ), - ], + builder: (_) => OTLDialog( + type: OTLDialogType.addOverlappingLectureWithTab, + namedArgs: { + 'lectures': lectures + .map((lecture) => + "'${isKo ? lecture.title : lecture.titleEn}'") + .join(', '), + 'lecture': lectureTitle, + 'timetable': timetable + }, + onTapPos: () => result = true, ), ); diff --git a/lib/pages/liked_review_page.dart b/lib/pages/liked_review_page.dart index 10309a42..a3624d3f 100644 --- a/lib/pages/liked_review_page.dart +++ b/lib/pages/liked_review_page.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:otlplus/constants/color.dart'; import 'package:otlplus/constants/text_styles.dart'; import 'package:otlplus/pages/course_detail_page.dart'; import 'package:otlplus/providers/course_detail_model.dart'; @@ -86,7 +87,7 @@ class LikedReviewPage extends StatelessWidget { width: 24, height: 24, child: CircularProgressIndicator( - color: Colors.black12, + color: OTLColor.grayE, strokeWidth: 2, ), ), diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 0cde36ca..66b5ca9e 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -1,10 +1,9 @@ import 'dart:io'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:otlplus/constants/color.dart'; -import 'package:otlplus/widgets/responsive_button.dart'; +import 'package:otlplus/utils/navigator.dart'; +import 'package:otlplus/widgets/otl_dialog.dart'; import 'package:otlplus/widgets/otl_scaffold.dart'; import 'package:provider/provider.dart'; import 'package:otlplus/providers/auth_model.dart'; @@ -29,21 +28,11 @@ class _LoginPageState extends State { (_) async { if (!((await SharedPreferences.getInstance()).getBool('hasAccount') ?? true)) { - await showDialog( + OTLNavigator.pushDialog( context: context, - builder: (context) => AlertDialog( - title: Text('user.account_deleted'.tr()), - content: Text('user.deleted_account'.tr()), - actions: [ - IconTextButton( - padding: EdgeInsets.all(12), - text: 'common.close'.tr(), - color: OTLColor.pinksMain, - onTap: () { - SystemNavigator.pop(); - }, - ), - ], + builder: (_) => OTLDialog( + type: OTLDialogType.accountDeleted, + onTapNeg: () => SystemNavigator.pop(), ), ); } diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 27573c6b..f0dbc839 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -111,8 +111,6 @@ class _MainPageState extends State { Image.asset( "assets/images/bg.4556cdee.jpg", fit: BoxFit.cover, - color: const Color(0xFF9B4810).withOpacity(0.1), - colorBlendMode: BlendMode.srcATop, ), Container( constraints: const BoxConstraints.expand(), @@ -157,7 +155,7 @@ class _MainPageState extends State { Expanded( child: Text( "common.search_hint".tr(), - style: evenBodyRegular.copyWith( + style: bodyRegular.copyWith( color: OTLColor.grayA, height: 1.24), ), ), @@ -181,7 +179,7 @@ class _MainPageState extends State { SliverFillRemaining( hasScrollBody: false, child: ColoredBox( - color: Colors.white, + color: OTLColor.grayF, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, diff --git a/lib/pages/review_page.dart b/lib/pages/review_page.dart index 22d57c2d..22781a37 100644 --- a/lib/pages/review_page.dart +++ b/lib/pages/review_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:otlplus/constants/color.dart'; import 'package:otlplus/pages/course_detail_page.dart'; import 'package:otlplus/providers/hall_of_fame_model.dart'; import 'package:otlplus/utils/navigator.dart'; @@ -119,7 +120,7 @@ class LatestReviewsPage extends StatelessWidget { width: 24, height: 24, child: CircularProgressIndicator( - color: Colors.black12, + color: OTLColor.grayE, strokeWidth: 2, ), ), @@ -176,7 +177,7 @@ class HallOfFamePage extends StatelessWidget { width: 24, height: 24, child: CircularProgressIndicator( - color: Colors.black12, + color: OTLColor.grayE, strokeWidth: 2, ), ), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index d0d66e00..2cb41ccc 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:otlplus/constants/color.dart'; import 'package:otlplus/constants/text_styles.dart'; +import 'package:otlplus/constants/url.dart'; import 'package:otlplus/providers/settings_model.dart'; import 'package:otlplus/widgets/dropdown.dart'; -import 'package:otlplus/widgets/responsive_button.dart'; +import 'package:otlplus/widgets/otl_dialog.dart'; import 'package:otlplus/utils/navigator.dart'; import 'package:otlplus/widgets/otl_scaffold.dart'; import 'package:provider/provider.dart'; @@ -13,8 +14,6 @@ import 'package:flutter/foundation.dart' show kDebugMode; import 'package:easy_localization/easy_localization.dart'; class SettingsPage extends StatelessWidget { - final contactEmail = 'otlplus@kaist.ac.kr'; - @override Widget build(BuildContext context) { final isEn = EasyLocalization.of(context)?.currentLocale == Locale('en'); @@ -121,65 +120,30 @@ class SettingsPage extends StatelessWidget { onTap: () { OTLNavigator.pushDialog( context: context, - builder: (context) => AlertDialog( - title: Text( - 'common.alert'.tr(), - style: titleRegular, - ), - content: Text( - 'settings.reset_all_desc'.tr(), - style: bodyRegular, - ), - actions: [ - TextButton( - child: Text( - "common.cancel".tr(), - style: bodyRegular, - ), - onPressed: () { - OTLNavigator.pop(context); - }, - ), - TextButton( - child: Text( - "common.delete".tr(), - style: bodyRegular, - ), - onPressed: () { - context - .read() - .clearAllValues() - .then((_) => OTLNavigator.pop(context)); - }, - ), - ], + builder: (_) => OTLDialog( + type: OTLDialogType.resetSettings, + onTapPos: () => + context.read().clearAllValues(), ), ); }, ), _buildListTile( title: "settings.about".tr(), - onTap: () => showAboutDialog( + onTap: () => OTLNavigator.pushDialog( context: context, - applicationName: "", - applicationIcon: - Image.asset("assets/images/logo.png", height: 48.0), - children: [ - Text( - "Online Timeplanner with Lectures Plus @ KAIST", - style: bodyRegular, + builder: (_) => OTLDialog( + type: OTLDialogType.about, + onTapContent: () => + launchUrl(Uri.parse("mailto:$CONTACT")), + onTapPos: () => showLicensePage( + context: context, + applicationName: "", + applicationIcon: + Image.asset("assets/images/logo.png", height: 48.0), ), - IconTextButton( - padding: EdgeInsets.fromLTRB(0, 4, 10, 4), - onTap: () => - launchUrl(Uri.parse("mailto:$contactEmail")), - text: contactEmail, - textStyle: - bodyRegular.copyWith(color: OTLColor.pinksMain), - ) - ], + ), ), - hasTopPadding: false, ), ], ), @@ -191,7 +155,7 @@ class SettingsPage extends StatelessWidget { Widget _buildListTile( {required String title, - String subtitle = '', + String? subtitle, Widget? trailing, void Function()? onTap, bool hasTopPadding = true}) { @@ -209,8 +173,10 @@ class SettingsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: bodyBold), - const SizedBox(height: 4), - Text(subtitle, style: bodyRegular), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text(subtitle, style: bodyRegular), + ] ], ), ), diff --git a/lib/pages/timetable_page.dart b/lib/pages/timetable_page.dart index 24b7984b..87d21560 100644 --- a/lib/pages/timetable_page.dart +++ b/lib/pages/timetable_page.dart @@ -3,8 +3,7 @@ import 'package:otlplus/pages/lecture_detail_page.dart'; import 'package:otlplus/pages/lecture_search_page.dart'; import 'package:otlplus/utils/navigator.dart'; import 'package:otlplus/providers/lecture_search_model.dart'; -import 'package:otlplus/widgets/delete_dialog.dart'; -import 'package:otlplus/widgets/responsive_button.dart'; +import 'package:otlplus/widgets/otl_dialog.dart'; import 'package:otlplus/widgets/lecture_search.dart'; import 'package:otlplus/widgets/map_view.dart'; import 'package:otlplus/widgets/otl_scaffold.dart'; @@ -158,7 +157,7 @@ class _TimetablePageState extends State { child: RepaintBoundary( key: _paintKey, child: Container( - color: Colors.white, + color: OTLColor.grayF, padding: const EdgeInsets.symmetric(horizontal: 16), child: _buildTimetable(context, lectures, isExamTime), ), @@ -178,7 +177,6 @@ class _TimetablePageState extends State { bool isFirst = true; final tempLecture = context.select((model) => model.tempLecture); - final isEn = EasyLocalization.of(context)?.currentLocale == Locale('en'); return Timetable( lectures: (tempLecture == null) ? lectures : [...lectures, tempLecture], @@ -203,47 +201,25 @@ class _TimetablePageState extends State { context.read().loadLecture(lecture.id, true); OTLNavigator.push(context, LectureDetailPage()); }, - onLongPress: isSelected - ? null - : () async { - bool result = false; - await OTLNavigator.pushDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: Text("common.delete".tr()), - content: Text("timetable.ask_delete_lecture").tr( - args: [isEn ? lecture.titleEn : lecture.title], - ), - actions: [ - IconTextButton( - padding: EdgeInsets.all(12), - text: 'common.cancel'.tr(), - color: OTLColor.pinksMain, - onTap: () { - result = false; - OTLNavigator.pop(context); + onLongPress: + isSelected || context.read().selectedIndex == 0 + ? null + : () { + OTLNavigator.pushDialog( + context: context, + builder: (_) => OTLDialog( + type: OTLDialogType.deleteLecture, + namedArgs: { + 'lecture': context.locale == Locale('ko') + ? lecture.title + : lecture.titleEn }, + onTapPos: () => context + .read() + .removeLecture(lecture: lecture), ), - IconTextButton( - padding: EdgeInsets.all(12), - text: 'common.delete'.tr(), - color: OTLColor.pinksMain, - onTap: () { - result = true; - OTLNavigator.pop(context); - }, - ), - ], - ), - ); - - if (result) { - context - .read() - .removeLecture(lecture: lecture); - } - }, + ); + }, ); }, ); @@ -274,20 +250,15 @@ class _TimetablePageState extends State { });*/ }, onDeleteTap: () { - showGeneralDialog( + OTLNavigator.pushDialog( context: context, - barrierColor: Colors.black.withOpacity(0.2), - barrierDismissible: true, - barrierLabel: - MaterialLocalizations.of(context).modalBarrierDismissLabel, - pageBuilder: (context, _, __) => DeleteDialog( - text: 'timetable.ask_delete_tab'.tr(args: [ - 'timetable.tab' + builder: (_) => OTLDialog( + type: OTLDialogType.deleteTab, + namedArgs: { + 'timetable': 'timetable.tab' .tr(args: [timetableModel.selectedIndex.toString()]) - ]), - onDelete: () { - context.read().deleteTimetable(); }, + onTapPos: () => context.read().deleteTimetable(), ), ); }, diff --git a/lib/pages/user_page.dart b/lib/pages/user_page.dart index e2819ff9..3949cfdf 100644 --- a/lib/pages/user_page.dart +++ b/lib/pages/user_page.dart @@ -7,7 +7,7 @@ import 'package:otlplus/pages/liked_review_page.dart'; import 'package:otlplus/pages/my_review_page.dart'; import 'package:otlplus/providers/auth_model.dart'; import 'package:otlplus/utils/navigator.dart'; -import 'package:otlplus/widgets/delete_dialog.dart'; +import 'package:otlplus/widgets/otl_dialog.dart'; import 'package:otlplus/widgets/responsive_button.dart'; import 'package:otlplus/widgets/otl_scaffold.dart'; import 'package:provider/provider.dart'; @@ -24,7 +24,7 @@ class UserPage extends StatelessWidget { child: OTLLayout( middle: Text('title.my_information'.tr(), style: titleBold), body: ColoredBox( - color: Colors.white, + color: OTLColor.grayF, child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Column( @@ -77,19 +77,15 @@ class UserPage extends StatelessWidget { if (Platform.isIOS) _buildAccount( Icons.highlight_off, - () async { - showGeneralDialog( + () { + OTLNavigator.pushDialog( context: context, - barrierColor: Colors.black.withOpacity(0.2), - barrierDismissible: true, - barrierLabel: MaterialLocalizations.of(context) - .modalBarrierDismissLabel, - pageBuilder: (context, _, __) => DeleteDialog( - text: 'user.ask_delete_account'.tr(), - onDelete: () { + builder: (_) => OTLDialog( + type: OTLDialogType.deleteAccount, + onTapPos: () { context.read().logout(); context.read().deleteAccount(); - Navigator.pop(context); + OTLNavigator.pop(context); }, ), ); diff --git a/lib/providers/course_search_model.dart b/lib/providers/course_search_model.dart index ad9795f6..cea6f67b 100644 --- a/lib/providers/course_search_model.dart +++ b/lib/providers/course_search_model.dart @@ -148,7 +148,7 @@ class CourseSearchModel extends ChangeNotifier { Text get courseSearchquery => (_courseSearchquery == null) ? Text( "common.search_hint".tr(), - style: evenBodyRegular.copyWith(color: OTLColor.grayA), + style: bodyRegular.copyWith(color: OTLColor.grayA), ) : _courseSearchquery!; void updateCourseSearchquery() { @@ -166,7 +166,7 @@ class CourseSearchModel extends ChangeNotifier { } else { _courseSearchquery = Text.rich( TextSpan( - style: evenBodyRegular.copyWith(color: OTLColor.grayA), + style: bodyRegular.copyWith(color: OTLColor.grayA), children: [ TextSpan( text: _courseSearchText.isEmpty ? '' : '"$_courseSearchText"', diff --git a/lib/providers/lecture_search_model.dart b/lib/providers/lecture_search_model.dart index c9e23f3a..4e5b65aa 100644 --- a/lib/providers/lecture_search_model.dart +++ b/lib/providers/lecture_search_model.dart @@ -133,7 +133,7 @@ class LectureSearchModel extends ChangeNotifier { .map((i) => i.label)))).values.expand((i) => i).toList(); _lectureSearchquery = Text.rich( TextSpan( - style: evenBodyRegular.copyWith(color: OTLColor.grayA), + style: bodyRegular.copyWith(color: OTLColor.grayA), children: [ TextSpan( text: _lectureSearchText.isEmpty ? '' : '"$_lectureSearchText"', @@ -184,10 +184,10 @@ class LectureSearchModel extends ChangeNotifier { children: [ TextSpan( text: keyword, - style: - TextStyle(fontWeight: FontWeight.bold, color: Colors.black)), + style: TextStyle( + fontWeight: FontWeight.bold, color: OTLColor.gray0)), if (filterOptions.length > 0) - TextSpan(style: TextStyle(color: Color(0xFFAAAAAA)), children: [ + TextSpan(style: TextStyle(color: OTLColor.grayA), children: [ if ((keyword ?? '').length > 0) TextSpan(text: ", "), TextSpan(text: (filterOptions).join(", ")), ]) diff --git a/lib/providers/timetable_model.dart b/lib/providers/timetable_model.dart index 5ed3a542..42997abc 100644 --- a/lib/providers/timetable_model.dart +++ b/lib/providers/timetable_model.dart @@ -168,6 +168,7 @@ class TimetableModel extends ChangeNotifier { Future addLecture( {required Lecture lecture, + required FutureOr Function() noOverlap, required FutureOr Function(Iterable) onOverlap}) async { try { final overlappedLectures = currentTimetable.lectures.where( @@ -189,7 +190,7 @@ class TimetableModel extends ChangeNotifier { data: {"lecture": lecture.id}); } currentTimetable.lectures.removeWhere(overlappedLectures.contains); - } + } else if (!await noOverlap()) return false; final response = await DioProvider().dio.post( API_TIMETABLE_ADD_LECTURE_URL diff --git a/lib/utils/navigator.dart b/lib/utils/navigator.dart index 6940caed..ac8da534 100644 --- a/lib/utils/navigator.dart +++ b/lib/utils/navigator.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:otlplus/constants/color.dart'; enum OTLNavigatorTransition { rightLeft, downUp, immediate } @@ -108,9 +109,9 @@ class OTLNavigator { required BuildContext context, required WidgetBuilder builder, bool barrierDismissible = true, - Color? barrierColor = Colors.black54, + Color? barrierColor = OTLColor.barrier, String? barrierLabel, - bool useSafeArea = true, + bool useSafeArea = false, bool useRootNavigator = true, RouteSettings? routeSettings, Offset? anchorPoint, diff --git a/lib/widgets/custom_header_delegate.dart b/lib/widgets/custom_header_delegate.dart index 9a0100b1..1938867e 100644 --- a/lib/widgets/custom_header_delegate.dart +++ b/lib/widgets/custom_header_delegate.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:otlplus/constants/color.dart'; class CustomHeaderDelegate extends SliverPersistentHeaderDelegate { final Widget Function(double) builder; @@ -12,7 +13,7 @@ class CustomHeaderDelegate extends SliverPersistentHeaderDelegate { Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( - color: Colors.white, + color: OTLColor.grayF, padding: padding, transform: Matrix4.translationValues(0, -1, 0), child: Material( diff --git a/lib/widgets/delete_dialog.dart b/lib/widgets/delete_dialog.dart deleted file mode 100644 index b5fd85db..00000000 --- a/lib/widgets/delete_dialog.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:otlplus/constants/color.dart'; - -class DeleteDialog extends StatelessWidget { - const DeleteDialog({Key? key, required this.text, this.onDelete}) - : super(key: key); - final String text; - final void Function()? onDelete; - - @override - Widget build(BuildContext context) { - return Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Material( - child: IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.fromLTRB(16, 19, 16, 20), - alignment: Alignment.center, - color: Colors.white, - child: Text( - text, - style: TextStyle( - fontSize: 12, - ), - ), - ), - Row( - children: [ - _buildButton( - () => Navigator.pop(context), - text: 'common.cancel'.tr(), - buttonColor: OTLColor.grayE, - ), - _buildButton( - () { - if (onDelete != null) onDelete!(); - Navigator.pop(context); - }, - text: 'common.delete'.tr(), - buttonColor: OTLColor.pinksMain, - textColor: Colors.white, - ), - ], - ) - ], - ), - ), - ), - ), - ); - } - - Widget _buildButton( - void Function()? onTap, { - required String text, - required Color buttonColor, - Color? textColor, - }) { - return Expanded( - child: GestureDetector( - onTap: onTap, - child: Container( - height: 40, - alignment: Alignment.center, - color: buttonColor, - child: Text( - text, - style: TextStyle( - color: textColor, - fontSize: 12, - fontWeight: FontWeight.w700, - ), - ), - ), - ), - ); - } -} diff --git a/lib/widgets/expandable_text.dart b/lib/widgets/expandable_text.dart index 5df05919..7efde15d 100644 --- a/lib/widgets/expandable_text.dart +++ b/lib/widgets/expandable_text.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart' as _; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:otlplus/constants/color.dart'; class ExpandableText extends StatefulWidget { const ExpandableText( @@ -32,7 +33,7 @@ class ExpandableTextState extends State { TextSpan( text: "review.expand".tr(), style: - (widget.style ?? TextStyle()).copyWith(color: Colors.black45), + (widget.style ?? TextStyle()).copyWith(color: OTLColor.gray75), recognizer: TapGestureRecognizer() ..onTap = () { setState(() => _expanded = true); diff --git a/lib/widgets/hall_of_fame_control.dart b/lib/widgets/hall_of_fame_control.dart index db4c66af..1a8ca82d 100644 --- a/lib/widgets/hall_of_fame_control.dart +++ b/lib/widgets/hall_of_fame_control.dart @@ -49,7 +49,7 @@ class _HallOfFameControlState extends State { _currentSemester == null ? "common.all".tr() : "${_currentSemester?.year} ${_currentSemester?.semester == 1 ? 'semester.spring'.tr() : 'semester.fall'.tr()}", - style: evenBodyBold.copyWith( + style: bodyBold.copyWith( color: OTLColor.pinksMain, ), ), diff --git a/lib/widgets/lecture_group_block.dart b/lib/widgets/lecture_group_block.dart index eecf62e9..619e2213 100644 --- a/lib/widgets/lecture_group_block.dart +++ b/lib/widgets/lecture_group_block.dart @@ -75,7 +75,7 @@ class LectureGroupBlock extends StatelessWidget { child: SizedBox( height: 1, child: ColoredBox( - color: Color(0xFFDADADA), + color: OTLColor.grayA, ), ), ), diff --git a/lib/widgets/lecture_group_block_row.dart b/lib/widgets/lecture_group_block_row.dart index 4e54cc16..bc0dad2d 100644 --- a/lib/widgets/lecture_group_block_row.dart +++ b/lib/widgets/lecture_group_block_row.dart @@ -4,6 +4,7 @@ import 'package:otlplus/constants/color.dart'; import 'package:otlplus/constants/text_styles.dart'; import 'package:otlplus/extensions/lecture.dart'; import 'package:otlplus/models/lecture.dart'; +import 'package:otlplus/widgets/otl_dialog.dart'; import 'package:otlplus/widgets/responsive_button.dart'; import 'package:otlplus/utils/navigator.dart'; import 'package:provider/provider.dart'; @@ -100,7 +101,7 @@ class _LectureGroupBlockRowState extends State { icon: 'assets/icons/info.svg', iconSize: 20.0, onTap: widget.onLongPress, - color: Color(0xFF000000), + color: OTLColor.gray0, ), SizedBox( width: 6.0, @@ -119,7 +120,7 @@ class _LectureGroupBlockRowState extends State { iconSize: 24, color: alreadyAdded ? OTLColor.pinksMain - : Color(0xFF000000), + : OTLColor.gray0, ) ], ), @@ -135,38 +136,43 @@ class _LectureGroupBlockRowState extends State { } Future _addLecture(Lecture lec) async { + final isKo = context.locale == Locale('ko'); + final lectureTitle = isKo ? lec.title : lec.titleEn; + bool result = await context.read().addLecture( lecture: lec, + noOverlap: () async { + bool result = false; + + await OTLNavigator.pushDialog( + context: context, + builder: (_) => OTLDialog( + type: OTLDialogType.addLecture, + namedArgs: {'lecture': lectureTitle}, + onTapPos: () => result = true, + ), + ); + + return result; + }, onOverlap: (lectures) async { bool result = false; + await OTLNavigator.pushDialog( context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: Text("timetable.dialog.add_lecture".tr()), - content: Text("timetable.dialog.ask_add_lecture".tr()), - actions: [ - IconTextButton( - padding: EdgeInsets.all(12), - text: 'common.cancel'.tr(), - color: OTLColor.pinksMain, - onTap: () { - result = false; - OTLNavigator.pop(context); - }, - ), - IconTextButton( - padding: EdgeInsets.all(12), - text: 'common.add'.tr(), - color: OTLColor.pinksMain, - onTap: () { - result = true; - OTLNavigator.pop(context); - }, - ), - ], + builder: (_) => OTLDialog( + type: OTLDialogType.addOverlappingLecture, + namedArgs: { + 'lectures': lectures + .map((lecture) => + "'${isKo ? lecture.title : lecture.titleEn}'") + .join(', '), + 'lecture': lectureTitle + }, + onTapPos: () => result = true, ), ); + return result; }, ); @@ -176,9 +182,17 @@ class _LectureGroupBlockRowState extends State { } Future _removeLecture(Lecture lec) async { - await context.read().removeLecture( - lecture: lec, - ); + await OTLNavigator.pushDialog( + context: context, + builder: (_) => OTLDialog( + type: OTLDialogType.deleteLecture, + namedArgs: { + 'lecture': context.locale == Locale('ko') ? lec.title : lec.titleEn + }, + onTapPos: () => + context.read().removeLecture(lecture: lec), + ), + ); context.read().setTempLecture(null); } } diff --git a/lib/widgets/lecture_group_simple_block.dart b/lib/widgets/lecture_group_simple_block.dart index 2165efa9..3ed9cd16 100644 --- a/lib/widgets/lecture_group_simple_block.dart +++ b/lib/widgets/lecture_group_simple_block.dart @@ -71,11 +71,11 @@ class LectureGroupSimpleBlock extends StatelessWidget { ), child: Text.rich( TextSpan( - style: evenBodyRegular, + style: bodyRegular, children: [ TextSpan( text: lecture.classTitle, - style: evenBodyBold, + style: bodyBold, ), TextSpan(text: ' '), TextSpan( diff --git a/lib/widgets/otl_dialog.dart b/lib/widgets/otl_dialog.dart new file mode 100644 index 00000000..e41f5a38 --- /dev/null +++ b/lib/widgets/otl_dialog.dart @@ -0,0 +1,325 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:otlplus/constants/color.dart'; +import 'package:otlplus/constants/text_styles.dart'; +import 'package:otlplus/constants/url.dart'; +import 'package:otlplus/utils/navigator.dart'; +import 'package:otlplus/widgets/responsive_button.dart'; + +enum OTLDialogType { + /// namedArgs: 'lecture' + addLecture, + + /// namedArgs: 'lecture', 'timetable' + addLectureWithTab, + + /// namedArgs: 'lectures', 'lecture' + addOverlappingLecture, + + /// namedArgs: 'lectures', 'lecture', 'timetable' + addOverlappingLectureWithTab, + + /// namedArgs: 'lecture' + deleteLecture, + + /// namedArgs: 'lecture', 'timetable' + deleteLectureWithTab, + + /// namedArgs: 'timetable' + deleteTab, + + deleteAccount, + + accountDeleted, + + resetSettings, + + about, +} + +enum BtnStyle { one, even, uneven } + +class _OTLDialogData { + /// Without tr() + /// + /// Example: 'timetable.dialog.add_lecture', 'timetable.dialog.ask_add_lecture' + final String title, content; + + /// Without 'assets/icons/' + /// + /// Example: 'addLecture' + final String icon; + + /// Without tr() + /// + /// Example: 'common.cancel', 'common.add' + final String negText, posText; + final BtnStyle btnStyle; + + _OTLDialogData({ + required this.title, + required this.content, + required this.icon, + this.negText = 'common.cancel', + this.posText = 'common.add', + this.btnStyle = BtnStyle.even, + }); +} + +extension OTLDialogTypeExt on OTLDialogType { + static final _data = { + OTLDialogType.addLecture: _OTLDialogData( + title: 'timetable.dialog.add_lecture', + content: 'timetable.dialog.ask_add_lecture', + icon: 'addLecture', + ), + OTLDialogType.addLectureWithTab: _OTLDialogData( + title: 'timetable.dialog.add_lecture', + content: 'timetable.dialog.ask_add_lecture_with_tab', + icon: 'addLecture', + ), + OTLDialogType.addOverlappingLecture: _OTLDialogData( + title: 'timetable.dialog.add_overlapping_lecture', + content: 'timetable.dialog.ask_add_overlapping_lecture', + icon: 'overlap', + ), + OTLDialogType.addOverlappingLectureWithTab: _OTLDialogData( + title: 'timetable.dialog.add_overlapping_lecture', + content: 'timetable.dialog.ask_add_overlapping_lecture_with_tab', + icon: 'overlap', + ), + OTLDialogType.deleteLecture: _OTLDialogData( + title: 'timetable.dialog.delete_lecture', + content: 'timetable.dialog.ask_delete_lecture', + icon: 'deleteLecture', + posText: 'common.delete', + ), + OTLDialogType.deleteLectureWithTab: _OTLDialogData( + title: 'timetable.dialog.delete_lecture', + content: 'timetable.dialog.ask_delete_lecture_with_tab', + icon: 'deleteLecture', + posText: 'common.delete', + ), + OTLDialogType.deleteTab: _OTLDialogData( + title: 'timetable.dialog.delete_tab', + content: 'timetable.dialog.ask_delete_tab', + icon: 'timetable', + posText: 'common.delete', + ), + OTLDialogType.deleteAccount: _OTLDialogData( + title: 'user.delete_account', + content: 'user.ask_delete_account', + icon: 'alert', + posText: 'common.delete', + ), + OTLDialogType.accountDeleted: _OTLDialogData( + title: 'user.account_deleted', + content: 'user.deleted_account', + icon: 'alert', + negText: 'common.close', + btnStyle: BtnStyle.one, + ), + OTLDialogType.resetSettings: _OTLDialogData( + title: 'settings.dialog.reset_settings', + content: 'settings.dialog.reset_settings_desc', + icon: 'alert', + posText: 'settings.dialog.reset', + ), + OTLDialogType.about: _OTLDialogData( + title: 'Online Timeplanner with Lectures Plus @ KAIST', + content: CONTACT, + icon: 'OTL', + negText: 'common.close', + posText: 'settings.view_licenses', + btnStyle: BtnStyle.uneven, + ), + }; + + String get title => _data[this]!.title; + String get content => _data[this]!.content; + String get icon => _data[this]!.icon; + String get negText => _data[this]!.negText; + String get posText => _data[this]!.posText; + BtnStyle get btnStyle => _data[this]!.btnStyle; +} + +class OTLDialog extends StatelessWidget { + const OTLDialog({ + Key? key, + required this.type, + this.namedArgs, + this.onTapContent, + this.onTapNeg, + this.onTapPos, + }) : super(key: key); + + final OTLDialogType type; + final Map? namedArgs; + final void Function()? onTapContent, onTapNeg, onTapPos; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 256, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: OTLColor.grayF, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: OTLColor.gray0.withOpacity(0.15), + offset: const Offset(2, 2), + blurRadius: 16, + ), + ], + ), + child: Material( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + 'assets/icons/${type.icon}.svg', + height: 80, + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 24), + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(type.title.tr(), style: titleBold), + const SizedBox(height: 8), + _buildContent(), + ], + ), + ), + () { + // Map + final btnData = { + true: { + 'btnColor': OTLColor.pinksMain, + 'onTap': () { + if (onTapPos != null) onTapPos!(); + if (type != OTLDialogType.about) + OTLNavigator.pop(context); + }, + 'btnText': type.posText.tr(), + 'textColor': OTLColor.grayF, + }, + false: { + 'btnColor': OTLColor.grayE, + 'onTap': () { + if (onTapNeg != null) onTapNeg!(); + OTLNavigator.pop(context); + }, + 'btnText': type.negText.tr(), + 'textColor': OTLColor.gray0, + } + }; + + switch (type.btnStyle) { + case BtnStyle.one: + return _buildButton(btnData[false]!); + case BtnStyle.even: + return Row( + children: [ + Expanded(child: _buildButton(btnData[false]!)), + const SizedBox(width: 10), + Expanded(child: _buildButton(btnData[true]!)), + ], + ); + case BtnStyle.uneven: + return Row( + children: [ + Expanded(child: _buildButton(btnData[true]!)), + const SizedBox(width: 10), + SizedBox( + width: 64, + child: _buildButton(btnData[false]!), + ), + ], + ); + } + }(), + ], + ), + ), + ), + ); + } + + Widget _buildContent() { + final content = type.content.tr(namedArgs: namedArgs); + switch (type) { + case OTLDialogType.addLecture: + case OTLDialogType.addLectureWithTab: + case OTLDialogType.deleteLecture: + case OTLDialogType.deleteLectureWithTab: + case OTLDialogType.deleteTab: + case OTLDialogType.deleteAccount: + case OTLDialogType.accountDeleted: + case OTLDialogType.resetSettings: + return Text( + content, + style: bodyRegular, + ); + case OTLDialogType.addOverlappingLecture: + case OTLDialogType.addOverlappingLectureWithTab: + return Text.rich(TextSpan( + children: () { + final reg = RegExp(r"'.*?'"); + final lectures = reg + .allMatches(content) + .map((e) => TextSpan(text: e[0], style: bodyBold)) + .toList(); + final nonLectures = content + .split(reg) + .map((e) => TextSpan(text: e, style: bodyRegular)) + .toList(); + final children = []; + + children.add(nonLectures.first); + + for (int i = 0; i < lectures.length; i++) { + children.add(lectures[i]); + children.add(nonLectures[i + 1]); + } + + return children; + }(), + )); + case OTLDialogType.about: + return RawResponsiveButton( + data: { + 'Text': { + 'arg': content, + 'style': bodyRegular.copyWith(color: OTLColor.pinksMain), + } + }, + onTap: onTapContent, + ); + default: + return SizedBox(); + } + } + + Widget _buildButton(Map data) { + return ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackgroundButton( + color: data['btnColor'], + onTap: data['onTap'], + child: Container( + height: 30, + alignment: Alignment.center, + child: Text( + data['btnText'], + style: bodyBold.copyWith(color: data['textColor']), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/pop_up.dart b/lib/widgets/pop_up.dart index 8380ff7d..a30337f1 100644 --- a/lib/widgets/pop_up.dart +++ b/lib/widgets/pop_up.dart @@ -100,13 +100,13 @@ Widget _build23fRecruiting() { children: [ Text( '지원하러 가기', - style: evenBodyBold.copyWith(color: Colors.black), + style: bodyBold.copyWith(color: OTLColor.gray0), textAlign: TextAlign.center, ), const SizedBox(width: 8.0), Icon( Icons.arrow_forward, - color: Colors.black, + color: OTLColor.gray0, ) ], ), diff --git a/lib/widgets/responsive_button.dart b/lib/widgets/responsive_button.dart index 77800cac..bbee5226 100644 --- a/lib/widgets/responsive_button.dart +++ b/lib/widgets/responsive_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:otlplus/constants/color.dart'; enum ButtonTapEffect { none, darken, lighten } @@ -8,7 +9,7 @@ enum ButtonDirection { row, column, rowReversed, columnReversed } class IconTextButton extends StatelessWidget { const IconTextButton({ Key? key, - this.color = const Color(0xFF000000), + this.color = OTLColor.gray0, this.icon, this.iconSize = 24, this.spaceBetween = 0, @@ -108,7 +109,7 @@ class IconTextButton extends StatelessWidget { class BackgroundButton extends StatelessWidget { const BackgroundButton( {Key? key, - this.color = const Color(0x00000000), + this.color = Colors.transparent, this.onTap, this.onLongPress, this.tapEffect = ButtonTapEffect.darken, @@ -183,13 +184,13 @@ class RawResponsiveWidget extends StatelessWidget { .toList(), ); case 'Icon': - final Color unpressedColor = args['color'] ?? const Color(0xFF000000); + final Color unpressedColor = args['color'] ?? OTLColor.gray0; final Color pressedColor = Color.lerp( unpressedColor, tapEffect == ButtonTapEffect.darken - ? const Color(0xFF000000) + ? OTLColor.gray0 : tapEffect == ButtonTapEffect.lighten - ? const Color(0xFFFFFFFF) + ? OTLColor.grayF : unpressedColor, tapEffectColorRatio)!; return ValueListenableBuilder( @@ -200,16 +201,16 @@ class RawResponsiveWidget extends StatelessWidget { color: effect ? pressedColor : unpressedColor); }); case 'SvgPicture.asset': - final ColorFilter unpressedColorFilter = ColorFilter.mode( - args['color'] ?? const Color(0xFF000000), BlendMode.srcIn); + final ColorFilter unpressedColorFilter = + ColorFilter.mode(args['color'] ?? OTLColor.gray0, BlendMode.srcIn); final ColorFilter pressedColorFilter = ColorFilter.mode( Color.lerp( - args['color'] ?? const Color(0xFF000000), + args['color'] ?? OTLColor.gray0, tapEffect == ButtonTapEffect.darken - ? const Color(0xFF000000) + ? OTLColor.gray0 : tapEffect == ButtonTapEffect.lighten - ? const Color(0xFFFFFFFF) - : args['color'] ?? const Color(0xFF000000), + ? OTLColor.grayF + : args['color'] ?? OTLColor.gray0, tapEffectColorRatio)!, BlendMode.srcIn); return ValueListenableBuilder( @@ -244,12 +245,12 @@ class RawResponsiveWidget extends StatelessWidget { } final TextStyle? pressedTextStyle = unpressedTextStyle.copyWith( color: Color.lerp( - unpressedTextStyle.color ?? const Color(0xFF000000), + unpressedTextStyle.color ?? OTLColor.gray0, tapEffect == ButtonTapEffect.darken - ? const Color(0xFF000000) + ? OTLColor.gray0 : tapEffect == ButtonTapEffect.lighten - ? const Color(0xFFFFFFFF) - : unpressedTextStyle.color ?? const Color(0xFF000000), + ? OTLColor.grayF + : unpressedTextStyle.color ?? OTLColor.gray0, tapEffectColorRatio)); return ValueListenableBuilder( valueListenable: pressedEffect, @@ -258,13 +259,13 @@ class RawResponsiveWidget extends StatelessWidget { style: effect ? pressedTextStyle : unpressedTextStyle); }); case 'ColoredBox': - final Color unpressedColor = args['color'] ?? const Color(0x00000000); + final Color unpressedColor = args['color'] ?? Colors.transparent; final Color pressedColor = Color.lerp( unpressedColor, tapEffect == ButtonTapEffect.darken - ? const Color(0xFF000000) + ? OTLColor.gray0 : tapEffect == ButtonTapEffect.lighten - ? const Color(0xFFFFFFFF) + ? OTLColor.grayF : unpressedColor, tapEffectColorRatio)!; return ValueListenableBuilder( diff --git a/lib/widgets/review_block.dart b/lib/widgets/review_block.dart index 01694436..d8c61bd0 100644 --- a/lib/widgets/review_block.dart +++ b/lib/widgets/review_block.dart @@ -5,10 +5,13 @@ import 'package:otlplus/constants/text_styles.dart'; import 'package:otlplus/constants/url.dart'; import 'package:otlplus/dio_provider.dart'; import 'package:otlplus/extensions/review.dart'; +import 'package:otlplus/extensions/semester.dart'; import 'package:otlplus/models/review.dart'; +import 'package:otlplus/models/semester.dart'; import 'package:otlplus/widgets/responsive_button.dart'; -import 'package:otlplus/utils/navigator.dart'; import 'package:otlplus/widgets/expandable_text.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:mailto/mailto.dart'; class ReviewBlock extends StatefulWidget { final Review review; @@ -186,20 +189,32 @@ class _ReviewBlockState extends State { } void _report() { - OTLNavigator.pushDialog( - context: context, - builder: (_) => AlertDialog( - title: Text('안내'), - content: Text( - '이 기능은 현재 개발중입니다. 부적절한 후기는 otlplus@sparcs.org로 신고해 주세요.'), - actions: [ - new TextButton( - child: new Text("확인"), - onPressed: () { - OTLNavigator.pop(context); - }, - ), - ], - )); + final lecture = widget.review.lecture; + final isKo = context.locale == Locale('ko'); + launchUrl( + Uri.parse( + '${Mailto( + to: [CONTACT], + subject: 'review.mailto.subject'.tr(), + body: 'review.mailto.body_reason'.tr() + + 'review.mailto.body_info'.tr( + namedArgs: { + 'title': isKo ? lecture.title : lecture.titleEn, + 'oldCode': lecture.oldCode, + 'semesterTitle': Semester( + year: lecture.year, + semester: lecture.semester, + beginning: DateTime(0), + end: DateTime(0)) + .title, + 'professors': lecture.professors + .map((e) => isKo ? e.name : e.nameEn) + .join(', '), + 'content': widget.review.content + }, + ), + )}', + ), + ); } } diff --git a/lib/widgets/review_mode_control.dart b/lib/widgets/review_mode_control.dart index ffe95697..4ab80b96 100644 --- a/lib/widgets/review_mode_control.dart +++ b/lib/widgets/review_mode_control.dart @@ -109,7 +109,7 @@ class _ReviewModeControlState extends State { widget._selectedMode == 0 ? "title.hall_of_fame".tr() : "title.latest_reviews".tr(), - style: evenTitleBold.copyWith(color: OTLColor.grayF), + style: titleBold.copyWith(color: OTLColor.grayF), ), ], ), diff --git a/lib/widgets/search_filter_panel.dart b/lib/widgets/search_filter_panel.dart index 21754b6a..5d14939c 100644 --- a/lib/widgets/search_filter_panel.dart +++ b/lib/widgets/search_filter_panel.dart @@ -338,8 +338,8 @@ class _SilderSelectionState extends State { thumbShape: CustomSliderThumbShape( outerThumbRadius: 10, innerThumbRadius: 7, - outerThumbColor: Color(0xFFF6C5CD), - innerThumbColor: Colors.white), + outerThumbColor: OTLColor.pinksSub, + innerThumbColor: OTLColor.grayF), trackHeight: 5.0, trackShape: RoundRectangularSliderTrackShape(), tickMarkShape: SliderTickMarkShape.noTickMark, @@ -349,8 +349,8 @@ class _SilderSelectionState extends State { min: 0.0, max: divisions.toDouble(), divisions: divisions, - activeColor: Color(0xFFF6C5CD), - inactiveColor: Color(0xFFEEEEEE), + activeColor: OTLColor.pinksSub, + inactiveColor: OTLColor.grayE, onChanged: (double value) { setState(() { _value = value; @@ -488,8 +488,8 @@ class CustomSliderThumbShape extends SliderComponentShape { const CustomSliderThumbShape({ this.outerThumbRadius = 10.0, this.innerThumbRadius = 10.0, - this.outerThumbColor = Colors.white, - this.innerThumbColor = Colors.white, + this.outerThumbColor = OTLColor.grayF, + this.innerThumbColor = OTLColor.grayF, this.elevation = 0.0, this.pressedElevation = 0.0, }); @@ -541,7 +541,7 @@ class CustomSliderThumbShape extends SliderComponentShape { bool paintShadows = true; if (paintShadows) { - canvas.drawShadow(path, Colors.black, evaluatedElevation, true); + canvas.drawShadow(path, OTLColor.gray0, evaluatedElevation, true); } canvas diff --git a/lib/widgets/search_textfield.dart b/lib/widgets/search_textfield.dart index cfc46d98..39e30106 100644 --- a/lib/widgets/search_textfield.dart +++ b/lib/widgets/search_textfield.dart @@ -49,7 +49,7 @@ class _SearchTextfieldState extends State { style: bodyRegular, decoration: InputDecoration( hintText: "common.search_hint".tr(), - hintStyle: evenBodyRegular.copyWith(color: OTLColor.grayA), + hintStyle: bodyRegular.copyWith(color: OTLColor.grayA), ), ), ), diff --git a/lib/widgets/timetable_mode_control.dart b/lib/widgets/timetable_mode_control.dart index 4886083e..4fb1b2bf 100644 --- a/lib/widgets/timetable_mode_control.dart +++ b/lib/widgets/timetable_mode_control.dart @@ -27,7 +27,7 @@ class _TimetableModeControlState extends State { height: 40, padding: const EdgeInsets.fromLTRB(4, 4, 16, 4), decoration: BoxDecoration( - color: Colors.white, + color: OTLColor.grayF, borderRadius: BorderRadius.horizontal(left: Radius.circular(20)), ), child: Stack( @@ -59,7 +59,7 @@ class _TimetableModeControlState extends State { child: Icon( _iconList[index], color: (index == widget.dropdownIndex) - ? Colors.white + ? OTLColor.grayF : OTLColor.pinksMain, ), ), diff --git a/lib/widgets/timetable_tabs.dart b/lib/widgets/timetable_tabs.dart index f3798c32..31b50bf8 100644 --- a/lib/widgets/timetable_tabs.dart +++ b/lib/widgets/timetable_tabs.dart @@ -72,7 +72,7 @@ class _TimetableTabsState extends State { i == 0 ? 'timetable.my_tab'.tr() : 'timetable.tab'.tr(args: [i.toString()]), - style: evenBodyBold.copyWith( + style: bodyBold.copyWith( color: i == _index ? OTLColor.grayF : OTLColor.gray0), textAlign: TextAlign.center, ); diff --git a/pubspec.lock b/pubspec.lock index 4265e5ba..02a0ee0e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -416,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + mailto: + dependency: "direct main" + description: + name: mailto + sha256: f8c5ce39e0eaa94a856795b2855af7f66aac37f7c3b70ac5c26ab00b94685445 + url: "https://pub.dev" + source: hosted + version: "2.0.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d0973986..05884dbf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_svg: ^2.0.7 open_app_file: ^4.0.2 flutter_native_splash: ^2.3.2 + mailto: ^2.0.0 # Firebase firebase_core: ^2.8.0 firebase_analytics: ^10.1.6