├── .fvmrc ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── silgam.code-workspace ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── seunghyun │ │ │ │ └── silgam │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── android12splash.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── splash.png │ │ │ ├── drawable-mdpi │ │ │ ├── android12splash.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── splash.png │ │ │ ├── drawable-night-hdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-mdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-xhdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-xxhdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-night-xxxhdpi │ │ │ └── android12splash.png │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ ├── android12splash.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── splash.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── android12splash.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── splash.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── android12splash.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── splash.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── launcher_icon.xml │ │ │ ├── mipmap-hdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-mdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xhdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── launcher_icon.png │ │ │ ├── values-night-v31 │ │ │ └── styles.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-v31 │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── app_icon │ ├── app_icon.png │ ├── app_icon.svg │ ├── app_icon_responsive.png │ ├── app_icon_transparent.png │ └── app_icon_transparent_1152.png ├── apple_icon.svg ├── custom_exam_guide_1.png ├── custom_exam_guide_2.png ├── custom_exam_guide_3.png ├── custom_exam_guide_4.png ├── facebook_icon.svg ├── google_icon.svg ├── kakao_icon.svg ├── kakao_icon_with_text.svg ├── landing_background.png ├── noise_easy.png ├── noise_hard.png ├── noise_normal.png ├── offline_guide_1.png ├── offline_guide_2.png ├── offline_guide_3.png ├── paper_texture.png ├── sns_instagram.svg ├── sns_kakaotalk.svg ├── sns_support.svg ├── star.png └── wrist_watch.svg ├── build.yaml ├── devtools_options.yaml ├── fonts ├── EstablishRetrosans.otf ├── NanumSquareB.otf ├── NanumSquareEB.otf ├── NanumSquareL.otf └── NanumSquareR.otf ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── LaunchBackground.imageset │ │ │ ├── Contents.json │ │ │ └── background.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── PrivacyInfo.xcprivacy │ ├── Runner-Bridging-Header.h │ └── Runner.entitlements └── firebase_app_id_file.json ├── lib ├── app_env.dart.example ├── main.dart ├── model │ ├── ads.dart │ ├── announcement.dart │ ├── dday.dart │ ├── exam.dart │ ├── exam_detail.dart │ ├── exam_record.dart │ ├── join_path.dart │ ├── lap_time.dart │ ├── problem.dart │ ├── product.dart │ ├── relative_time.dart │ ├── subject.dart │ ├── timetable.dart │ └── user.dart ├── presentation │ ├── announcement_setting │ │ ├── announcement_setting_page.dart │ │ └── announcement_type.dart │ ├── app │ │ ├── app.dart │ │ ├── cubit │ │ │ ├── app_cubit.dart │ │ │ ├── app_state.dart │ │ │ ├── iap_cubit.dart │ │ │ └── iap_state.dart │ │ └── initial_route_handler.dart │ ├── clock │ │ ├── breakpoint.dart │ │ ├── clock_page.dart │ │ ├── cubit │ │ │ ├── clock_cubit.dart │ │ │ └── clock_state.dart │ │ ├── noise │ │ │ ├── noise_generator.dart │ │ │ └── noise_player.dart │ │ ├── timeline.dart │ │ └── wrist_watch.dart │ ├── common │ │ ├── ad_tile.dart │ │ ├── bullet_text.dart │ │ ├── custom_card.dart │ │ ├── dialog.dart │ │ ├── empty_scroll_behavior.dart │ │ ├── filter_action_chip.dart │ │ ├── free_user_block_overlay.dart │ │ ├── login_button.dart │ │ ├── purchase_button.dart │ │ ├── review_problem_card.dart │ │ ├── scaffold_body.dart │ │ ├── search_field.dart │ │ ├── subject_filter_chip.dart │ │ ├── subtitle.dart │ │ └── timeline_marker.dart │ ├── custom_exam_edit │ │ ├── cubit │ │ │ ├── custom_exam_edit_cubit.dart │ │ │ └── custom_exam_edit_state.dart │ │ └── custom_exam_edit_page.dart │ ├── custom_exam_guide │ │ └── custom_exam_guide_page.dart │ ├── custom_exam_list │ │ └── custom_exam_list_page.dart │ ├── customize_subject_name │ │ ├── cubit │ │ │ ├── customize_subject_name_cubit.dart │ │ │ └── customize_subject_name_state.dart │ │ └── customize_subject_name_page.dart │ ├── edit_record │ │ ├── edit_record_page.dart │ │ └── widgets │ │ │ └── form_review_problems_field.dart │ ├── edit_review_problem │ │ └── edit_review_problem_page.dart │ ├── exam_overview │ │ ├── cubit │ │ │ ├── exam_overview_cubit.dart │ │ │ └── exam_overview_state.dart │ │ ├── exam_overview_messages.dart │ │ ├── exam_overview_page.dart │ │ └── example_lap_time_groups.dart │ ├── home │ │ ├── cubit │ │ │ ├── home_cubit.dart │ │ │ └── home_state.dart │ │ ├── home_page.dart │ │ ├── main │ │ │ ├── ads_card.dart │ │ │ ├── cubit │ │ │ │ ├── main_cubit.dart │ │ │ │ └── main_state.dart │ │ │ ├── d_days_card.dart │ │ │ ├── main_view.dart │ │ │ ├── quick_launcher_card.dart │ │ │ ├── silgam_now_card.dart │ │ │ ├── timetable_start_card.dart │ │ │ └── welcome_messages.dart │ │ ├── record_list │ │ │ ├── cubit │ │ │ │ ├── record_list_cubit.dart │ │ │ │ └── record_list_state.dart │ │ │ ├── record_list_view.dart │ │ │ └── record_tile.dart │ │ ├── settings │ │ │ ├── setting_tile.dart │ │ │ └── settings_view.dart │ │ └── stat │ │ │ ├── cubit │ │ │ ├── stat_cubit.dart │ │ │ └── stat_state.dart │ │ │ ├── example_records.dart │ │ │ └── stat_view.dart │ ├── login │ │ ├── cubit │ │ │ ├── login_cubit.dart │ │ │ └── login_state.dart │ │ └── login_page.dart │ ├── my │ │ └── my_page.dart │ ├── noise_setting │ │ ├── cubit │ │ │ ├── noise_setting_cubit.dart │ │ │ └── noise_setting_state.dart │ │ └── noise_setting_page.dart │ ├── notification_setting │ │ └── notification_setting_page.dart │ ├── offline │ │ └── offline_guide_page.dart │ ├── onboarding │ │ ├── cubit │ │ │ ├── onboarding_cubit.dart │ │ │ └── onboarding_state.dart │ │ ├── join_path │ │ │ └── join_path_view.dart │ │ ├── onboarding_page.dart │ │ └── welcome │ │ │ └── welcome_view.dart │ ├── products │ │ └── silgampass │ │ │ └── silgampass_page.dart │ ├── purchase │ │ ├── cubit │ │ │ ├── purchase_cubit.dart │ │ │ └── purchase_state.dart │ │ └── purchase_page.dart │ ├── record_detail │ │ └── record_detail_page.dart │ ├── review_problem_detail │ │ └── review_problem_detail_page.dart │ └── save_image │ │ └── save_image_page.dart ├── repository │ ├── ads │ │ ├── ads_api.dart │ │ └── ads_repository.dart │ ├── auth │ │ ├── auth_api.dart │ │ ├── auth_repository.dart │ │ └── dto │ │ │ └── auth_dto.dart │ ├── dday │ │ ├── dday_api.dart │ │ └── dday_repository.dart │ ├── exam │ │ └── exam_repository.dart │ ├── exam_record │ │ └── exam_record_repository.dart │ ├── feedback │ │ ├── dto │ │ │ └── send_feedback_request.dto.dart │ │ ├── feedback_api.dart │ │ └── feedback_repository.dart │ ├── noise │ │ └── noise_repository.dart │ ├── onboarding │ │ ├── dto │ │ │ └── submit_join_paths_request_dto.dart │ │ ├── onboarding_api.dart │ │ └── onboarding_repository.dart │ ├── product │ │ ├── dto │ │ │ ├── can_purchase_request.dto.dart │ │ │ ├── on_purchase_request.dto.dart │ │ │ └── start_trial_request.dto.dart │ │ ├── product_api.dart │ │ └── product_repository.dart │ └── user │ │ ├── user_api.dart │ │ └── user_repository.dart └── util │ ├── analytics_manager.dart │ ├── android_audio_manager.dart │ ├── announcement_player.dart │ ├── api_failure.dart │ ├── cache_manager.dart │ ├── color_extension.dart │ ├── connectivity_manager.dart │ ├── const.dart │ ├── date_time_extension.dart │ ├── dday_util.dart │ ├── duration_extension.dart │ ├── injection.dart │ ├── notification_manager.dart │ └── string_util.dart ├── markdown_assets ├── download_app_store.png ├── download_google_play.png ├── feature_graphic.png ├── store_1.png ├── store_2.png ├── store_3.png ├── store_4.png ├── store_5.png ├── store_6.png ├── store_7.png └── store_8.png ├── packages └── ui │ ├── .gitignore │ ├── .metadata │ ├── analysis_options.yaml │ ├── lib │ ├── src │ │ ├── custom_alert_dialog.dart │ │ ├── custom_app_bar.dart │ │ ├── custom_autocomplete.dart │ │ ├── custom_filled_button.dart │ │ ├── custom_text_button.dart │ │ ├── form │ │ │ ├── form.dart │ │ │ ├── form_date_picker.dart │ │ │ ├── form_dropdown.dart │ │ │ ├── form_item.dart │ │ │ ├── form_numbers_field.dart │ │ │ ├── form_switch.dart │ │ │ ├── form_text_field.dart │ │ │ └── form_time_picker.dart │ │ ├── page_layout.dart │ │ └── theme.dart │ └── ui.dart │ ├── pubspec.yaml │ └── test │ └── src │ ├── custom_alert_dialog_test.dart │ └── custom_text_button_test.dart ├── project_initializer.sh ├── pubspec.lock ├── pubspec.yaml ├── user_data_deletion.md └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.29.3" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | web/firebase-messaging-sw.js 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | 49 | # Custom 50 | /assets/announcements 51 | /assets/noises 52 | 53 | # generated files 54 | *.g.dart 55 | *.freezed.dart 56 | *.mocks.dart 57 | *.gr.dart 58 | *.config.dart 59 | lib/firebase_options.dart 60 | lib/app_env.dart 61 | 62 | # FVM Version Cache 63 | .fvm/ -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 18 | - platform: android 19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 21 | - platform: ios 22 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 23 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 24 | - platform: web 25 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 26 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 27 | 28 | # User provided section 29 | 30 | # List of Local paths (relative to this file) that should be 31 | # ignored by the migrate tool. 32 | # 33 | # Files that are not part of the templates will be ignored by default. 34 | unmanaged_files: 35 | - 'lib/main.dart' 36 | - 'ios/Runner.xcodeproj/project.pbxproj' 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "silgam-flutter", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "lib/main.dart" 12 | }, 13 | { 14 | "name": "silgam-flutter (profile mode)", 15 | "request": "launch", 16 | "type": "dart", 17 | "program": "lib/main.dart", 18 | "flutterMode": "profile" 19 | }, 20 | { 21 | "name": "silgam-flutter (release mode)", 22 | "request": "launch", 23 | "type": "dart", 24 | "program": "lib/main.dart", 25 | "flutterMode": "release" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/silgam.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "..", 5 | "name": "Root" 6 | }, 7 | { 8 | "path": "../packages/ui", 9 | "name": "UI" 10 | }, 11 | { 12 | "path": "../lib", 13 | "name": "Root lib" 14 | }, 15 | { 16 | "path": "../packages/ui/lib", 17 | "name": "UI lib" 18 | } 19 | ], 20 | "settings": { 21 | "dart.flutterSdkPath": "../.fvm/versions/3.29.3", 22 | "[dart]": { 23 | "editor.rulers": [100] 24 | }, 25 | "prettier.singleQuote": true, 26 | "cSpell.spellCheckOnlyWorkspaceFiles": true, 27 | "cSpell.words": [ 28 | "Admob", 29 | "ARGB", 30 | "Autovalidate", 31 | "colorsets", 32 | "Cupertino", 33 | "Datas", 34 | "dday", 35 | "ddays", 36 | "easyloading", 37 | "firestore", 38 | "kakao", 39 | "kakaotalk", 40 | "Millis", 41 | "mocktail", 42 | "Nanum", 43 | "Openchat", 44 | "paperplane", 45 | "Retrosans", 46 | "Roboto", 47 | "signin", 48 | "silgam", 49 | "silgampass", 50 | "storekit", 51 | "suneung", 52 | "unfocus", 53 | "unfreezed", 54 | "vsync", 55 | "wakelock", 56 | "writeln" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 실전 감각, 실감 2 | 3 | 실제 수능장에서의 느낌 그대로! 실감과 함께 실전감각을 키우세요. 4 | 5 | [](https://play.google.com/store/apps/details?id=com.seunghyun.silgam) 6 | [](https://apps.apple.com/us/app/%EC%8B%A4%EC%A0%84-%EA%B0%90%EA%B0%81-%EC%8B%A4%EA%B0%90/id1598576852) 7 | 8 | 9 | 10 | ## 스크린샷 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | formatter: 4 | page_width: 100 5 | 6 | linter: 7 | rules: 8 | - prefer_relative_imports 9 | - prefer_const_constructors 10 | - prefer_const_declarations 11 | - prefer_const_literals_to_create_immutables 12 | 13 | analyzer: 14 | errors: 15 | invalid_annotation_target: ignore 16 | exclude: 17 | - '**.g.dart' 18 | - '**.freezed.dart' 19 | - '**.mocks.dart' 20 | - '**.gr.dart' 21 | - '**.config.dart' 22 | - 'lib/generated_plugin_registrant.dart' 23 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | app/google-services.json 16 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | // START: FlutterFire Configuration 4 | id 'com.google.gms.google-services' 5 | id 'com.google.firebase.crashlytics' 6 | // END: FlutterFire Configuration 7 | id "kotlin-android" 8 | id "dev.flutter.flutter-gradle-plugin" 9 | } 10 | 11 | def localProperties = new Properties() 12 | def localPropertiesFile = rootProject.file('local.properties') 13 | if (localPropertiesFile.exists()) { 14 | localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) 15 | } 16 | } 17 | 18 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 19 | if (flutterVersionCode == null) { 20 | flutterVersionCode = '1' 21 | } 22 | 23 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 24 | if (flutterVersionName == null) { 25 | flutterVersionName = '1.0' 26 | } 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if (keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | compileSdkVersion 35 36 | 37 | compileOptions { 38 | coreLibraryDesugaringEnabled true 39 | sourceCompatibility JavaVersion.VERSION_17 40 | targetCompatibility JavaVersion.VERSION_17 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = 17 45 | } 46 | 47 | sourceSets { 48 | main.java.srcDirs += 'src/main/kotlin' 49 | } 50 | 51 | defaultConfig { 52 | applicationId "com.seunghyun.silgam" 53 | minSdkVersion 23 54 | targetSdkVersion 35 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | multiDexEnabled true 58 | } 59 | 60 | signingConfigs { 61 | release { 62 | keyAlias keystoreProperties['keyAlias'] 63 | keyPassword keystoreProperties['keyPassword'] 64 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 65 | storePassword keystoreProperties['storePassword'] 66 | } 67 | } 68 | 69 | buildTypes { 70 | release { 71 | signingConfig signingConfigs.release 72 | } 73 | } 74 | namespace 'com.seunghyun.silgam' 75 | } 76 | 77 | flutter { 78 | source '../..' 79 | } 80 | 81 | dependencies { 82 | implementation 'com.android.support:multidex:1.0.3' 83 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' 84 | } 85 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/seunghyun/silgam/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.seunghyun.silgam 2 | 3 | import android.media.AudioManager 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugin.common.MethodChannel 7 | 8 | class MainActivity : FlutterActivity() { 9 | private val channelId = "com.seunghyun.silgam/audio" 10 | 11 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 12 | super.configureFlutterEngine(flutterEngine) 13 | MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelId).setMethodCallHandler { call, result -> 14 | when (call.method) { 15 | "controlMediaVolume" -> { 16 | volumeControlStream = AudioManager.STREAM_MUSIC 17 | } 18 | "controlDefaultVolume" -> { 19 | volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE 20 | } 21 | else -> { 22 | result.notImplemented() 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-hdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-mdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-hdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-night-hdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-mdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-night-mdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/android12splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffffff 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 실감 4 | 457631255986966 5 | fb457631255986966 6 | ca-app-pub-5293956621132135~2623749942 7 | f3459711cfb25c80aa6abce7b815bc2c 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | subprojects { 8 | afterEvaluate { project -> 9 | if (project.hasProperty('android')) { 10 | project.android { 11 | if (namespace == null) { 12 | namespace project.group 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | project.evaluationDependsOn(':app') 24 | } 25 | 26 | tasks.register("clean", Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.6.1" apply false 22 | // START: FlutterFire Configuration 23 | id "com.google.gms.google-services" version "4.3.15" apply false 24 | id "com.google.firebase.crashlytics" version "2.9.9" apply false 25 | // END: FlutterFire Configuration 26 | id "org.jetbrains.kotlin.android" version "2.0.20" apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /assets/app_icon/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/app_icon/app_icon.png -------------------------------------------------------------------------------- /assets/app_icon/app_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/app_icon/app_icon_responsive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/app_icon/app_icon_responsive.png -------------------------------------------------------------------------------- /assets/app_icon/app_icon_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/app_icon/app_icon_transparent.png -------------------------------------------------------------------------------- /assets/app_icon/app_icon_transparent_1152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/app_icon/app_icon_transparent_1152.png -------------------------------------------------------------------------------- /assets/apple_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/custom_exam_guide_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/custom_exam_guide_1.png -------------------------------------------------------------------------------- /assets/custom_exam_guide_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/custom_exam_guide_2.png -------------------------------------------------------------------------------- /assets/custom_exam_guide_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/custom_exam_guide_3.png -------------------------------------------------------------------------------- /assets/custom_exam_guide_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/custom_exam_guide_4.png -------------------------------------------------------------------------------- /assets/facebook_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/google_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/kakao_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/kakao_icon_with_text.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/landing_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/landing_background.png -------------------------------------------------------------------------------- /assets/noise_easy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/noise_easy.png -------------------------------------------------------------------------------- /assets/noise_hard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/noise_hard.png -------------------------------------------------------------------------------- /assets/noise_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/noise_normal.png -------------------------------------------------------------------------------- /assets/offline_guide_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/offline_guide_1.png -------------------------------------------------------------------------------- /assets/offline_guide_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/offline_guide_2.png -------------------------------------------------------------------------------- /assets/offline_guide_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/offline_guide_3.png -------------------------------------------------------------------------------- /assets/paper_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/paper_texture.png -------------------------------------------------------------------------------- /assets/sns_instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/sns_kakaotalk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/sns_support.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/assets/star.png -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | json_serializable: 5 | options: 6 | explicit_to_json: true 7 | 8 | global_options: 9 | freezed: 10 | runs_before: 11 | - json_serializable 12 | json_serializable: 13 | runs_before: 14 | - retrofit_generator 15 | -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /fonts/EstablishRetrosans.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/fonts/EstablishRetrosans.otf -------------------------------------------------------------------------------- /fonts/NanumSquareB.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/fonts/NanumSquareB.otf -------------------------------------------------------------------------------- /fonts/NanumSquareEB.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/fonts/NanumSquareEB.otf -------------------------------------------------------------------------------- /fonts/NanumSquareL.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/fonts/NanumSquareL.otf -------------------------------------------------------------------------------- /fonts/NanumSquareR.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/fonts/NanumSquareR.otf -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | Runner/GoogleService-Info.plist 36 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 13.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" 3 | #include "Generated.xcconfig" 4 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '11.4.0' 36 | end 37 | 38 | post_install do |installer| 39 | installer.pods_project.targets.each do |target| 40 | flutter_additional_ios_build_settings(target) 41 | target.build_configurations.each do |config| 42 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' 43 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 44 | '$(inherited)', 45 | 'AUDIO_SESSION_MICROPHONE=0' 46 | ] 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPITypeReasons 9 | 10 | 35F9.1 11 | 12 | NSPrivacyAccessedAPIType 13 | NSPrivacyAccessedAPICategorySystemBootTime 14 | 15 | 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | 3B52.1 19 | 20 | NSPrivacyAccessedAPIType 21 | NSPrivacyAccessedAPICategoryFileTimestamp 22 | 23 | 24 | NSPrivacyAccessedAPITypeReasons 25 | 26 | CA92.1 27 | 28 | NSPrivacyAccessedAPIType 29 | NSPrivacyAccessedAPICategoryUserDefaults 30 | 31 | 32 | NSPrivacyAccessedAPITypeReasons 33 | 34 | E174.1 35 | 36 | NSPrivacyAccessedAPIType 37 | NSPrivacyAccessedAPICategoryDiskSpace 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.applesignin 8 | 9 | Default 10 | 11 | com.apple.developer.associated-domains 12 | 13 | applinks:silgam.app 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:55232180944:ios:6389f09fade70a521664d5", 5 | "FIREBASE_PROJECT_ID": "silgam-app", 6 | "GCM_SENDER_ID": "55232180944" 7 | } -------------------------------------------------------------------------------- /lib/app_env.dart.example: -------------------------------------------------------------------------------- 1 | class AppEnv { 2 | static const mixpanelToken = ""; 3 | static const kakaoNativeAppKey = ""; 4 | } 5 | -------------------------------------------------------------------------------- /lib/model/ads.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'ads.freezed.dart'; 4 | part 'ads.g.dart'; 5 | 6 | @freezed 7 | class Ads with _$Ads { 8 | const Ads._(); 9 | 10 | const factory Ads({ 11 | required String title, 12 | required String imagePath, 13 | @Default([]) List variants, 14 | required int priority, 15 | required List actions, 16 | required DateTime startDate, 17 | required DateTime expiryDate, 18 | required int minVersionNumber, 19 | int? maxVersionNumber, 20 | String? category, 21 | int? showCountInCategory, 22 | }) = _Ads; 23 | 24 | factory Ads.fromJson(Map json) => _$AdsFromJson(json); 25 | 26 | bool get isHiddenToPurchasedUser => 27 | actions.any((action) => action.intent == AdsIntent.openPurchasePage); 28 | 29 | bool get isAd => actions.any((action) => action.intent == AdsIntent.openAdUrl); 30 | } 31 | 32 | @freezed 33 | class AdsVariant with _$AdsVariant { 34 | const factory AdsVariant({required String id, required String imagePath}) = _AdsVariant; 35 | 36 | factory AdsVariant.fromJson(Map json) => _$AdsVariantFromJson(json); 37 | } 38 | 39 | @freezed 40 | class AdsAction with _$AdsAction { 41 | const factory AdsAction({ 42 | @JsonKey(unknownEnumValue: AdsIntent.unknown) required AdsIntent intent, 43 | required String data, 44 | }) = _AdsAction; 45 | 46 | factory AdsAction.fromJson(Map json) => _$AdsActionFromJson(json); 47 | } 48 | 49 | enum AdsIntent { 50 | openUrl, 51 | openAdUrl, 52 | openPurchasePage, 53 | openOfflineGuidePage, 54 | openCustomExamGuidePage, 55 | openPage, 56 | unknown, 57 | } 58 | -------------------------------------------------------------------------------- /lib/model/announcement.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'relative_time.dart'; 4 | 5 | part 'announcement.freezed.dart'; 6 | 7 | @freezed 8 | class Announcement with _$Announcement { 9 | const factory Announcement({ 10 | required String title, 11 | required RelativeTime time, 12 | required AnnouncementPurpose purpose, 13 | String? fileName, 14 | }) = _Announcement; 15 | } 16 | 17 | enum AnnouncementPurpose { 18 | preliminary, 19 | prepare, 20 | changePaper, 21 | start, 22 | listeningEnd, 23 | beforeFinish, 24 | finish, 25 | } 26 | -------------------------------------------------------------------------------- /lib/model/dday.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'dday.freezed.dart'; 4 | part 'dday.g.dart'; 5 | 6 | @freezed 7 | class DDay with _$DDay { 8 | const factory DDay({required DDayType testType, required String title, required DateTime date}) = 9 | _DDay; 10 | 11 | factory DDay.fromJson(Map json) => _$DDayFromJson(json); 12 | } 13 | 14 | enum DDayType { suneung, mockTest } 15 | -------------------------------------------------------------------------------- /lib/model/exam.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | import '../presentation/app/cubit/app_cubit.dart'; 5 | import '../util/date_time_extension.dart'; 6 | import '../util/injection.dart'; 7 | import 'announcement.dart'; 8 | import 'relative_time.dart'; 9 | import 'subject.dart'; 10 | 11 | part 'exam.freezed.dart'; 12 | part 'exam.g.dart'; 13 | 14 | @freezed 15 | class Exam with _$Exam { 16 | Exam._(); 17 | 18 | factory Exam({ 19 | required String id, 20 | String? userId, 21 | required Subject subject, 22 | required String name, 23 | required int number, 24 | @JsonKey(fromJson: timeFromJson, toJson: timeToJson) required DateTime startTime, 25 | required int durationMinutes, 26 | required int numberOfQuestions, 27 | required int perfectScore, 28 | @Default(true) bool isBeforeFinishAnnouncementEnabled, 29 | @Default(true) bool isListeningEndAnnouncementEnabled, 30 | required int color, 31 | DateTime? createdAt, 32 | }) = _Exam; 33 | 34 | factory Exam.fromJson(Map json) => _$ExamFromJson(json); 35 | 36 | factory Exam.fromId(String id) { 37 | final AppState appState = getIt.get().state; 38 | return appState.getAllExams().firstWhereOrNull((element) => element.id == id) ?? 39 | appState.getDefaultExams().first.copyWith(id: id, name: '알 수 없는 과목'); 40 | } 41 | 42 | static String toId(Exam exam) => exam.id; 43 | 44 | late final DateTime endTime = startTime.add(Duration(minutes: durationMinutes)); 45 | 46 | late final DateTime timetableStartTime = startTime.subtract( 47 | Duration(minutes: subject.minutesBeforeExamStart), 48 | ); 49 | 50 | int get timetableDurationMinutes => 51 | durationMinutes + subject.minutesBeforeExamStart + subject.minutesAfterExamFinish; 52 | 53 | bool get isCustomExam => userId != null; 54 | 55 | late final List announcements = 56 | subject.defaultAnnouncements.where((announcement) { 57 | final isOverExamDuration = 58 | (announcement.time.type == RelativeTimeType.afterStart || 59 | announcement.time.type == RelativeTimeType.beforeFinish) && 60 | announcement.time.minutes >= durationMinutes; 61 | if (isOverExamDuration) { 62 | return false; 63 | } 64 | 65 | if (!isBeforeFinishAnnouncementEnabled && 66 | announcement.purpose == AnnouncementPurpose.beforeFinish) { 67 | return false; 68 | } 69 | 70 | if (!isListeningEndAnnouncementEnabled && 71 | announcement.purpose == AnnouncementPurpose.listeningEnd) { 72 | return false; 73 | } 74 | 75 | return true; 76 | }).toList(); 77 | 78 | late final Announcement? firstAnnouncement = announcements.firstOrNull; 79 | 80 | late final Announcement? startAnnouncement = announcements.firstWhereOrNull( 81 | (announcement) => announcement.purpose == AnnouncementPurpose.start, 82 | ); 83 | 84 | late final Announcement? finishAnnouncement = announcements.lastWhereOrNull( 85 | (announcement) => announcement.purpose == AnnouncementPurpose.finish, 86 | ); 87 | } 88 | 89 | DateTime timeFromJson(String json) { 90 | final parts = json.split(':'); 91 | return DateTimeBuilder.fromHourMinute(int.parse(parts[0]), int.parse(parts[1])); 92 | } 93 | 94 | String timeToJson(DateTime dateTime) { 95 | return '${dateTime.hour}:${dateTime.minute}'; 96 | } 97 | -------------------------------------------------------------------------------- /lib/model/exam_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'exam.dart'; 4 | import 'lap_time.dart'; 5 | 6 | part 'exam_detail.freezed.dart'; 7 | 8 | @freezed 9 | class ExamDetail with _$ExamDetail { 10 | const factory ExamDetail({ 11 | required String timetableName, 12 | required List exams, 13 | required Map examStartedTimes, 14 | required Map examFinishedTimes, 15 | required List lapTimes, 16 | required DateTime timetableStartedTime, 17 | required DateTime timetableFinishedTime, 18 | }) = _ExamDetail; 19 | } 20 | -------------------------------------------------------------------------------- /lib/model/exam_record.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:uuid/uuid.dart'; 3 | 4 | import 'exam.dart'; 5 | import 'problem.dart'; 6 | 7 | part 'exam_record.freezed.dart'; 8 | part 'exam_record.g.dart'; 9 | 10 | @unfreezed 11 | class ExamRecord with _$ExamRecord { 12 | const ExamRecord._(); 13 | 14 | factory ExamRecord({ 15 | required final String id, 16 | required final String userId, 17 | required final String title, 18 | @JsonKey(name: 'subject', toJson: Exam.toId, fromJson: Exam.fromId) required final Exam exam, 19 | required final DateTime examStartedTime, 20 | final int? examDurationMinutes, 21 | final int? score, 22 | final int? grade, 23 | final int? percentile, 24 | final int? standardScore, 25 | required final List wrongProblems, 26 | required final String feedback, 27 | required final List reviewProblems, 28 | required final DateTime createdAt, 29 | }) = _ExamRecord; 30 | 31 | factory ExamRecord.create({ 32 | required final String userId, 33 | required final String title, 34 | required final Exam exam, 35 | required final DateTime examStartedTime, 36 | final int? examDurationMinutes, 37 | final int? score, 38 | final int? grade, 39 | final int? percentile, 40 | final int? standardScore, 41 | final List? wrongProblems, 42 | final String? feedback, 43 | final List? reviewProblems, 44 | }) => ExamRecord( 45 | id: '$userId-${const Uuid().v1()}', 46 | userId: userId, 47 | title: title, 48 | exam: exam, 49 | examStartedTime: examStartedTime, 50 | examDurationMinutes: examDurationMinutes, 51 | score: score, 52 | grade: grade, 53 | percentile: percentile, 54 | standardScore: standardScore, 55 | wrongProblems: wrongProblems ?? const [], 56 | feedback: feedback ?? '', 57 | reviewProblems: reviewProblems ?? const [], 58 | createdAt: DateTime.now().toUtc(), 59 | ); 60 | 61 | factory ExamRecord.fromJson(Map json) => _$ExamRecordFromJson(json); 62 | 63 | static const String autoSaveTitlePrefix = '(자동 저장됨) '; 64 | 65 | int getGradeColor() { 66 | switch (grade) { 67 | case 1: 68 | return 0xFF7900D9; 69 | case 2: 70 | return 0xFF1D82CC; 71 | case 3: 72 | return 0xFF04A80B; 73 | case 4: 74 | return 0xFFFFA700; 75 | case 5: 76 | case 6: 77 | case 7: 78 | case 8: 79 | case 9: 80 | return 0xFFD60303; 81 | default: 82 | return 0xFF000000; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/model/join_path.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'join_path.freezed.dart'; 4 | part 'join_path.g.dart'; 5 | 6 | @freezed 7 | class JoinPath with _$JoinPath { 8 | const factory JoinPath({required String id, required String text, required String sectionTitle}) = 9 | _JoinPath; 10 | 11 | factory JoinPath.fromJson(Map json) => _$JoinPathFromJson(json); 12 | } 13 | -------------------------------------------------------------------------------- /lib/model/lap_time.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import '../presentation/clock/breakpoint.dart'; 6 | import '../util/date_time_extension.dart'; 7 | import 'announcement.dart'; 8 | 9 | part 'lap_time.freezed.dart'; 10 | 11 | @freezed 12 | class LapTime with _$LapTime { 13 | const factory LapTime({required DateTime time, required Breakpoint breakpoint}) = _LapTime; 14 | } 15 | 16 | @freezed 17 | class LapTimeItem with _$LapTimeItem { 18 | const factory LapTimeItem({ 19 | required DateTime time, 20 | required Duration timeDifference, 21 | required Duration timeElapsed, 22 | }) = _LapTimeItem; 23 | } 24 | 25 | @Freezed(makeCollectionsUnmodifiable: false) 26 | class LapTimeItemGroup with _$LapTimeItemGroup { 27 | const factory LapTimeItemGroup({ 28 | required String title, 29 | required DateTime startTime, 30 | required AnnouncementPurpose announcementPurpose, 31 | required List lapTimeItems, 32 | }) = _LapTimeItemGroup; 33 | } 34 | 35 | extension LapTimeItemGroupsExtension on List { 36 | String toCopyableString({bool isExample = false}) { 37 | final buffer = StringBuffer(); 38 | if (isExample) { 39 | buffer.writeln('(예시 텍스트입니다. )'); 40 | } 41 | buffer.writeln(' | 시간 | 간격 | 누적 | 분류'); 42 | 43 | for (final group in this) { 44 | buffer.writeln('————————————————————'); 45 | buffer.writeln( 46 | ' 0 | ${DateFormat.Hms().format(group.startTime)} | 00:00 | 00:00 | ${group.title}', 47 | ); 48 | 49 | group.lapTimeItems.forEachIndexed((index, item) { 50 | buffer.writeln( 51 | '${index >= 9 ? '' : ' '}${index + 1} | ${DateFormat.Hms().format(item.time)} | ${item.timeDifference.to2DigitString()} | ${item.timeElapsed.to2DigitString()} | ', 52 | ); 53 | }); 54 | } 55 | return buffer.toString(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/model/problem.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'problem.freezed.dart'; 4 | part 'problem.g.dart'; 5 | 6 | @freezed 7 | class WrongProblem with _$WrongProblem { 8 | const factory WrongProblem(int problemNumber) = _WrongProblem; 9 | 10 | factory WrongProblem.fromJson(Map json) => _$WrongProblemFromJson(json); 11 | 12 | @override 13 | String toString() { 14 | return 'WrongProblem{problemNumber: $problemNumber}'; 15 | } 16 | } 17 | 18 | @JsonSerializable() 19 | class ReviewProblem { 20 | final String title; 21 | final String memo; 22 | final List imagePaths; 23 | 24 | ReviewProblem({required this.title, this.memo = '', this.imagePaths = const []}); 25 | 26 | factory ReviewProblem.fromJson(Map json) => _$ReviewProblemFromJson(json); 27 | 28 | Map toJson() => _$ReviewProblemToJson(this); 29 | 30 | @override 31 | String toString() { 32 | return 'ReviewProblem{title: $title, memo: $memo, imagePaths: $imagePaths}'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/model/product.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'product.freezed.dart'; 4 | part 'product.g.dart'; 5 | 6 | @freezed 7 | class Product with _$Product { 8 | factory Product({ 9 | required final String id, 10 | required final String name, 11 | required final DateTime expiryDate, 12 | required final DateTime sellingStartDate, 13 | required final DateTime sellingEndDate, 14 | required final int trialPeriod, 15 | required final int minVersionNumber, 16 | required final String stampImageUrl, 17 | required final String trialStampImageUrl, 18 | required final String pageUrl, 19 | required final String pageBackgroundColor, 20 | required final bool isPageBackgroundDark, 21 | required final ProductBenefit benefit, 22 | }) = _Product; 23 | 24 | factory Product.fromJson(Map json) => _$ProductFromJson(json); 25 | } 26 | 27 | @freezed 28 | class ProductBenefit with _$ProductBenefit { 29 | const factory ProductBenefit({ 30 | required final bool isAdsRemoved, 31 | required final bool isStatisticAvailable, 32 | required final int examRecordLimit, 33 | required final List availableNoiseIds, 34 | required final bool isCustomSubjectNameAvailable, 35 | required final bool isLapTimeAvailable, 36 | @Default(false) final bool isCustomExamAvailable, 37 | @Default(false) final bool isAllSubjectsTimetableAvailable, 38 | }) = _ProductBenefit; 39 | 40 | factory ProductBenefit.fromJson(Map json) => _$ProductBenefitFromJson(json); 41 | 42 | static const ProductBenefit initial = ProductBenefit( 43 | isAdsRemoved: false, 44 | isStatisticAvailable: false, 45 | examRecordLimit: 20, 46 | availableNoiseIds: [0, 2, 3, 10, 12], 47 | isCustomSubjectNameAvailable: false, 48 | isLapTimeAvailable: false, 49 | isCustomExamAvailable: false, 50 | isAllSubjectsTimetableAvailable: false, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /lib/model/relative_time.dart: -------------------------------------------------------------------------------- 1 | class RelativeTime { 2 | final RelativeTimeType type; 3 | final int minutes; 4 | 5 | const RelativeTime.beforeStart({required this.minutes}) : type = RelativeTimeType.beforeStart; 6 | 7 | const RelativeTime.afterStart({required this.minutes}) : type = RelativeTimeType.afterStart; 8 | 9 | const RelativeTime.beforeFinish({required this.minutes}) : type = RelativeTimeType.beforeFinish; 10 | 11 | const RelativeTime.afterFinish({required this.minutes}) : type = RelativeTimeType.afterFinish; 12 | 13 | DateTime calculateBreakpointTime(DateTime examStartTime, DateTime examEndTime) { 14 | switch (type) { 15 | case RelativeTimeType.beforeStart: 16 | return examStartTime.subtract(Duration(minutes: minutes)); 17 | case RelativeTimeType.afterStart: 18 | return examStartTime.add(Duration(minutes: minutes)); 19 | case RelativeTimeType.beforeFinish: 20 | return examEndTime.subtract(Duration(minutes: minutes)); 21 | case RelativeTimeType.afterFinish: 22 | return examEndTime.add(Duration(minutes: minutes)); 23 | } 24 | } 25 | } 26 | 27 | enum RelativeTimeType { beforeStart, afterStart, beforeFinish, afterFinish } 28 | -------------------------------------------------------------------------------- /lib/model/timetable.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'exam.dart'; 4 | 5 | part 'timetable.freezed.dart'; 6 | part 'timetable.g.dart'; 7 | 8 | @freezed 9 | class Timetable with _$Timetable { 10 | const Timetable._(); 11 | 12 | const factory Timetable({ 13 | required String name, 14 | required DateTime startTime, 15 | required List items, 16 | @JsonKey(includeFromJson: false, includeToJson: false) 17 | @Default(false) 18 | bool isAllSubjectsTimetable, 19 | }) = _Timetable; 20 | 21 | factory Timetable.fromJson(Map json) => _$TimetableFromJson(json); 22 | 23 | List get exams => items.map((e) => e.exam).toList(); 24 | 25 | Duration get duration => items.fold( 26 | const Duration(), 27 | (previousValue, item) => 28 | previousValue + 29 | Duration(minutes: item.exam.timetableDurationMinutes) + 30 | Duration(minutes: item.breakMinutesAfter), 31 | ); 32 | 33 | DateTime get endTime => startTime.add(duration); 34 | 35 | String toExamNamesString() { 36 | return items.map((e) => e.exam.name).join(', '); 37 | } 38 | 39 | String toSubjectNamesString() { 40 | return exams.map((e) => e.subject.name).join(', '); 41 | } 42 | } 43 | 44 | @freezed 45 | class TimetableItem with _$TimetableItem { 46 | const factory TimetableItem({ 47 | @JsonKey(name: 'examId', toJson: Exam.toId, fromJson: Exam.fromId) required Exam exam, 48 | @Default(0) int breakMinutesAfter, 49 | }) = _TimetableItem; 50 | 51 | factory TimetableItem.fromJson(Map json) => _$TimetableItemFromJson(json); 52 | } 53 | -------------------------------------------------------------------------------- /lib/model/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import '../util/const.dart'; 4 | import 'product.dart'; 5 | import 'subject.dart'; 6 | 7 | part 'user.freezed.dart'; 8 | part 'user.g.dart'; 9 | 10 | @freezed 11 | class User with _$User { 12 | const User._(); 13 | 14 | factory User({ 15 | required final String id, 16 | required final Product activeProduct, 17 | String? displayName, 18 | String? email, 19 | String? photoUrl, 20 | @Default([]) List providerDatas, 21 | @Default([]) List fcmTokens, 22 | @Default([]) List receipts, 23 | bool? isMarketingInfoReceivingConsented, 24 | Map? customSubjectNameMap, 25 | }) = _User; 26 | 27 | factory User.fromJson(Map json) => _$UserFromJson(json); 28 | 29 | bool get isProductTrial => activeProduct.id != ProductId.free && receipts.last.store == 'trial'; 30 | 31 | bool get isPurchasedUser => activeProduct.id != ProductId.free && !isProductTrial; 32 | } 33 | 34 | @freezed 35 | class Receipt with _$Receipt { 36 | factory Receipt({ 37 | required final String store, 38 | required final String productId, 39 | required final String token, 40 | required final DateTime createdAt, 41 | }) = _Receipt; 42 | 43 | factory Receipt.fromJson(Map json) => _$ReceiptFromJson(json); 44 | } 45 | 46 | @freezed 47 | class ProviderData with _$ProviderData { 48 | factory ProviderData({required final String providerId}) = _ProviderData; 49 | 50 | factory ProviderData.fromJson(Map json) => _$ProviderDataFromJson(json); 51 | } 52 | -------------------------------------------------------------------------------- /lib/presentation/announcement_setting/announcement_type.dart: -------------------------------------------------------------------------------- 1 | class AnnouncementType { 2 | final int id; 3 | final String title; 4 | final String description; 5 | 6 | const AnnouncementType({required this.id, required this.title, required this.description}); 7 | } 8 | 9 | const announcementTypes = [ 10 | AnnouncementType(id: 2, title: '클래식 음악', description: '2024학년도 수능에 사용된 음원'), 11 | AnnouncementType(id: 1, title: '비프음', description: '2022학년도 수능에 사용된 음원 (코로나 관련 안내 포함)'), 12 | ]; 13 | 14 | final AnnouncementType defaultAnnouncementType = announcementTypes.first; 15 | -------------------------------------------------------------------------------- /lib/presentation/app/cubit/iap_state.dart: -------------------------------------------------------------------------------- 1 | part of 'iap_cubit.dart'; 2 | 3 | @freezed 4 | class IapState with _$IapState { 5 | const IapState._(); 6 | 7 | const factory IapState({ 8 | @Default(null) final Product? sellingProduct, 9 | @Default([]) final List products, 10 | @Default([]) final List productDetails, 11 | @Default(false) final bool isStoreAvailable, 12 | @Default(false) final bool isLoading, 13 | }) = _IapState; 14 | 15 | Product? get freeProduct => products.firstWhereOrNull((product) => product.id == ProductId.free); 16 | } 17 | -------------------------------------------------------------------------------- /lib/presentation/app/initial_route_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../home/home_page.dart'; 4 | 5 | class InitialRouteHandler extends StatelessWidget { 6 | const InitialRouteHandler(this._initialRoute, {super.key}); 7 | 8 | final String _initialRoute; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | final route = 13 | _initialRoute.contains('silgam.app') ? _initialRoute.split('silgam.app')[1] : _initialRoute; 14 | Future(() { 15 | if (!context.mounted) return; 16 | 17 | if (route != HomePage.routeName) { 18 | Navigator.of(context).pushReplacementNamed(HomePage.routeName); 19 | Navigator.of(context).pushNamed(route); 20 | } else { 21 | Navigator.of(context).pushReplacementNamed(route); 22 | } 23 | }); 24 | 25 | return const Scaffold(body: Center(child: CircularProgressIndicator())); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/presentation/clock/breakpoint.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | 3 | import '../../model/announcement.dart'; 4 | import '../../model/exam.dart'; 5 | import '../../model/timetable.dart'; 6 | 7 | class Breakpoint { 8 | final String title; 9 | final DateTime time; 10 | final Announcement announcement; 11 | final Exam exam; 12 | final bool isFirstInExam; 13 | 14 | Breakpoint({ 15 | required this.title, 16 | required this.time, 17 | required this.announcement, 18 | required this.exam, 19 | required this.isFirstInExam, 20 | }); 21 | 22 | static List createBreakpointsFromTimetable(Timetable timetable) { 23 | final breakpoints = []; 24 | 25 | timetable.items.forEachIndexed((index, currentItem) { 26 | final itemStartTime = 27 | index == 0 28 | ? timetable.startTime 29 | : breakpoints.last.time.add( 30 | Duration(minutes: timetable.items[index - 1].breakMinutesAfter), 31 | ); 32 | final examStartTime = itemStartTime.add( 33 | Duration(minutes: currentItem.exam.subject.minutesBeforeExamStart), 34 | ); 35 | 36 | final currentItemBreakpoints = Breakpoint._createBreakpointsFromExam( 37 | currentItem.exam, 38 | examStartTime, 39 | ); 40 | if (breakpoints.lastOrNull?.time == currentItemBreakpoints.firstOrNull?.time) { 41 | breakpoints.removeLast(); 42 | } 43 | breakpoints.addAll(currentItemBreakpoints); 44 | }); 45 | 46 | return breakpoints; 47 | } 48 | 49 | static List _createBreakpointsFromExam(Exam exam, DateTime examStartTime) { 50 | final breakpoints = []; 51 | 52 | for (final announcement in exam.announcements) { 53 | final DateTime breakpointTime = announcement.time.calculateBreakpointTime( 54 | examStartTime, 55 | examStartTime.add(Duration(minutes: exam.durationMinutes)), 56 | ); 57 | 58 | breakpoints.add( 59 | Breakpoint( 60 | title: announcement.title, 61 | time: breakpointTime, 62 | announcement: announcement, 63 | exam: exam, 64 | isFirstInExam: breakpoints.isEmpty, 65 | ), 66 | ); 67 | } 68 | 69 | return breakpoints; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/presentation/clock/cubit/clock_state.dart: -------------------------------------------------------------------------------- 1 | part of 'clock_cubit.dart'; 2 | 3 | @freezed 4 | class ClockState with _$ClockState { 5 | const ClockState._(); 6 | 7 | const factory ClockState({ 8 | @Default(false) bool isStarted, 9 | @Default(true) bool isUiVisible, 10 | @Default(true) bool isRunning, 11 | @Default([]) List breakpoints, 12 | @Default(0) int currentBreakpointIndex, 13 | required DateTime currentTime, 14 | @Default({}) Map examStartedTimes, 15 | @Default({}) Map examFinishedTimes, 16 | @Default([]) List lapTimes, 17 | required DateTime pageOpenedTime, 18 | required DateTime timetableStartedTime, 19 | DateTime? timetableFinishedTime, 20 | }) = _ClockState; 21 | 22 | Breakpoint get currentBreakpoint => breakpoints[currentBreakpointIndex]; 23 | Exam get currentExam => currentBreakpoint.exam; 24 | 25 | bool get isFinished => currentBreakpointIndex >= breakpoints.length - 1; 26 | } 27 | -------------------------------------------------------------------------------- /lib/presentation/clock/noise/noise_player.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:just_audio/just_audio.dart'; 4 | 5 | import '../../../repository/noise/noise_repository.dart'; 6 | 7 | abstract class NoisePlayer { 8 | void playWhiteNoise(); 9 | 10 | void pauseWhiteNoise(); 11 | 12 | Future playNoise(int noiseId); 13 | 14 | Future dispose(); 15 | } 16 | 17 | class NoiseAudioPlayer implements NoisePlayer { 18 | final AudioPlayer _whiteNoisePlayer = AudioPlayer(); 19 | final Map _undisposedNoisePlayers = {}; 20 | 21 | bool _isDisposed = false; 22 | 23 | @override 24 | Future playNoise(int noiseId) async { 25 | if (_isDisposed) return; 26 | 27 | Noise noise = Noise.byId(noiseId); 28 | String noisePath = noise.getRandomNoisePath(); 29 | 30 | int playerId = DateTime.now().millisecondsSinceEpoch; 31 | _undisposedNoisePlayers[playerId] = AudioPlayer(); 32 | final AudioPlayer audioPlayer = _undisposedNoisePlayers[playerId]!; 33 | 34 | await audioPlayer.setAsset(noisePath); 35 | double volume = (Random().nextDouble() + 2) * 2; 36 | await audioPlayer.setVolume(volume); 37 | 38 | await audioPlayer.play(); 39 | 40 | await _disposeNoisePlayer(playerId); 41 | } 42 | 43 | @override 44 | void playWhiteNoise() async { 45 | await _whiteNoisePlayer.setAsset(whiteNoisePath); 46 | await _whiteNoisePlayer.setLoopMode(LoopMode.all); 47 | await _whiteNoisePlayer.setVolume(2); 48 | await _whiteNoisePlayer.play(); 49 | } 50 | 51 | @override 52 | void pauseWhiteNoise() async { 53 | await _whiteNoisePlayer.pause(); 54 | } 55 | 56 | @override 57 | Future dispose() async { 58 | _isDisposed = true; 59 | 60 | await _whiteNoisePlayer.stop(); 61 | await _whiteNoisePlayer.dispose(); 62 | 63 | await Future.wait([..._undisposedNoisePlayers.keys].map(_disposeNoisePlayer)); 64 | } 65 | 66 | Future _disposeNoisePlayer(int playerId) async { 67 | final undisposedNoisePlayer = _undisposedNoisePlayers.remove(playerId); 68 | await undisposedNoisePlayer?.stop(); 69 | await undisposedNoisePlayer?.dispose(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/presentation/common/ad_tile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:google_mobile_ads/google_mobile_ads.dart'; 5 | 6 | import '../../util/const.dart'; 7 | 8 | class AdTile extends StatefulWidget { 9 | final int width; 10 | final EdgeInsetsGeometry? margin; 11 | 12 | const AdTile({super.key, required this.width, this.margin}); 13 | 14 | @override 15 | State createState() => AdTileState(); 16 | } 17 | 18 | class AdTileState extends State { 19 | BannerAd? _bannerAd; 20 | AdSize? _adSize; 21 | bool _isLoaded = false; 22 | bool _isLoading = false; 23 | late Orientation _currentOrientation; 24 | 25 | @override 26 | void didChangeDependencies() { 27 | super.didChangeDependencies(); 28 | 29 | _currentOrientation = MediaQuery.of(context).orientation; 30 | _loadAd(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return OrientationBuilder( 36 | builder: (context, orientation) { 37 | if (_currentOrientation != orientation) { 38 | _currentOrientation = orientation; 39 | _loadAd(); 40 | return const SizedBox.shrink(); 41 | } 42 | 43 | final bannerAd = _bannerAd; 44 | final adSize = _adSize; 45 | if (bannerAd == null || adSize == null || !_isLoaded) { 46 | return const SizedBox.shrink(); 47 | } 48 | 49 | return Container( 50 | width: adSize.width.toDouble(), 51 | height: adSize.height.toDouble(), 52 | margin: widget.margin, 53 | child: AdWidget(ad: bannerAd), 54 | ); 55 | }, 56 | ); 57 | } 58 | 59 | Future _loadAd() async { 60 | if (_isLoading) return; 61 | _isLoading = true; 62 | 63 | BannerAd? bannerAd = _bannerAd; 64 | setState(() { 65 | _bannerAd = null; 66 | _adSize = null; 67 | _isLoaded = false; 68 | }); 69 | 70 | await bannerAd?.dispose(); 71 | 72 | _bannerAd = BannerAd( 73 | size: AdSize.getInlineAdaptiveBannerAdSize(widget.width, 100), 74 | adUnitId: bannerAdId, 75 | request: const AdRequest(), 76 | listener: BannerAdListener( 77 | onAdLoaded: ((Ad ad) async { 78 | final bannerAd = ad as BannerAd; 79 | 80 | final adSize = await bannerAd.getPlatformAdSize(); 81 | if (adSize == null) { 82 | log('Error: getPlatformAdSize() returned null for $bannerAd'); 83 | return; 84 | } 85 | 86 | setState(() { 87 | _adSize = adSize; 88 | _isLoaded = true; 89 | _isLoading = false; 90 | }); 91 | }), 92 | onAdFailedToLoad: ((ad, error) { 93 | log('Failed to load a banner ad: $error'); 94 | setState(() { 95 | _bannerAd = null; 96 | _adSize = null; 97 | _isLoaded = false; 98 | _isLoading = false; 99 | }); 100 | ad.dispose(); 101 | }), 102 | ), 103 | )..load(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/presentation/common/bullet_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BulletText extends StatelessWidget { 4 | const BulletText({super.key, required this.text, this.style}); 5 | 6 | final String text; 7 | final TextStyle? style; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Row( 12 | crossAxisAlignment: CrossAxisAlignment.start, 13 | mainAxisSize: MainAxisSize.min, 14 | children: [ 15 | Text( 16 | '• ', 17 | style: 18 | style?.copyWith(fontWeight: FontWeight.w300) ?? 19 | const TextStyle(fontWeight: FontWeight.w300, color: Colors.grey, height: 1.2), 20 | ), 21 | Flexible(child: Text(text, textAlign: TextAlign.start, style: style)), 22 | ], 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/presentation/common/custom_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomCard extends StatelessWidget { 4 | final Widget child; 5 | final Color backgroundColor; 6 | final EdgeInsetsGeometry? margin; 7 | final EdgeInsetsGeometry? padding; 8 | final bool isThin; 9 | final Clip clipBehavior; 10 | final double? width; 11 | 12 | const CustomCard({ 13 | super.key, 14 | required this.child, 15 | this.backgroundColor = Colors.white, 16 | this.margin, 17 | this.padding, 18 | this.isThin = false, 19 | this.clipBehavior = Clip.hardEdge, 20 | this.width = double.infinity, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | width: width, 27 | clipBehavior: clipBehavior, 28 | margin: margin, 29 | decoration: BoxDecoration(borderRadius: BorderRadius.circular(isThin ? 100 : 14)), 30 | child: Material( 31 | color: backgroundColor, 32 | borderRadius: BorderRadius.circular(isThin ? 100 : 14), 33 | child: Padding(padding: padding ?? EdgeInsets.zero, child: child), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/presentation/common/empty_scroll_behavior.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyScrollBehavior extends ScrollBehavior { 4 | Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { 5 | return child; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/presentation/common/filter_action_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FilterActionChip extends StatelessWidget { 4 | const FilterActionChip({super.key, required this.label, this.onPressed, this.tooltip}); 5 | 6 | final Widget label; 7 | final VoidCallback? onPressed; 8 | final String? tooltip; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ActionChip( 13 | label: label, 14 | onPressed: onPressed, 15 | tooltip: tooltip, 16 | pressElevation: 0, 17 | backgroundColor: Colors.grey.shade700.withAlpha(10), 18 | padding: EdgeInsets.zero, 19 | side: BorderSide(color: Colors.grey.shade700, width: 0.4), 20 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/presentation/common/free_user_block_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'purchase_button.dart'; 4 | 5 | class FreeUserBlockOverlay extends StatelessWidget { 6 | const FreeUserBlockOverlay({super.key, required this.text, this.overlayColor}); 7 | 8 | final String text; 9 | final Color? overlayColor; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Container( 14 | color: overlayColor ?? Theme.of(context).scaffoldBackgroundColor.withAlpha(160), 15 | alignment: Alignment.center, 16 | padding: const EdgeInsets.all(20), 17 | child: Column( 18 | mainAxisSize: MainAxisSize.min, 19 | children: [ 20 | Text( 21 | text, 22 | textAlign: TextAlign.center, 23 | style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 24 | ), 25 | buildPurchaseButtonOr(margin: const EdgeInsets.only(top: 20), expand: false), 26 | ], 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/presentation/common/login_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'custom_card.dart'; 4 | 5 | class LoginButton extends StatelessWidget { 6 | final GestureTapCallback onTap; 7 | final String description; 8 | 9 | const LoginButton({ 10 | super.key, 11 | required this.onTap, 12 | this.description = '로그인하면 실감의 더 많은 기능들을 누릴 수 있어요!', 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return CustomCard( 18 | backgroundColor: Theme.of(context).primaryColor, 19 | child: InkWell( 20 | onTap: onTap, 21 | splashColor: Colors.transparent, 22 | highlightColor: Colors.white10, 23 | child: Padding( 24 | padding: const EdgeInsets.all(16), 25 | child: Row( 26 | children: [ 27 | const Icon(Icons.login, color: Colors.white), 28 | const SizedBox(width: 18), 29 | Flexible( 30 | child: Column( 31 | mainAxisSize: MainAxisSize.min, 32 | crossAxisAlignment: CrossAxisAlignment.start, 33 | children: [ 34 | const Text( 35 | '로그인', 36 | style: TextStyle( 37 | fontWeight: FontWeight.w600, 38 | fontSize: 18, 39 | color: Colors.white, 40 | ), 41 | ), 42 | const SizedBox(height: 4), 43 | Text( 44 | description, 45 | style: TextStyle(fontSize: 12, color: Colors.white.withAlpha(200)), 46 | ), 47 | ], 48 | ), 49 | ), 50 | ], 51 | ), 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/presentation/common/purchase_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | import '../../model/product.dart'; 6 | import '../app/cubit/iap_cubit.dart'; 7 | import '../purchase/purchase_page.dart'; 8 | import 'custom_card.dart'; 9 | 10 | Widget buildPurchaseButtonOr({EdgeInsetsGeometry? margin, bool expand = true}) { 11 | return BlocBuilder( 12 | buildWhen: (previous, current) => previous.sellingProduct != current.sellingProduct, 13 | builder: (context, state) { 14 | final sellingProduct = state.sellingProduct; 15 | if (sellingProduct == null) { 16 | return const SizedBox.shrink(); 17 | } 18 | return _PurchaseButton(product: sellingProduct, margin: margin, expand: expand); 19 | }, 20 | ); 21 | } 22 | 23 | class _PurchaseButton extends StatelessWidget { 24 | const _PurchaseButton({required this.product, this.margin, this.expand = true}); 25 | 26 | final Product product; 27 | final EdgeInsetsGeometry? margin; 28 | final bool expand; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return CustomCard( 33 | margin: margin, 34 | width: expand ? double.infinity : null, 35 | backgroundColor: Theme.of(context).primaryColor, 36 | isThin: true, 37 | child: InkWell( 38 | onTap: () { 39 | Navigator.pushNamed( 40 | context, 41 | PurchasePage.routeName, 42 | arguments: PurchasePageArguments(product: product), 43 | ); 44 | }, 45 | splashColor: Colors.transparent, 46 | highlightColor: Colors.grey.withAlpha(60), 47 | child: Padding( 48 | padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), 49 | child: Row( 50 | mainAxisSize: MainAxisSize.min, 51 | children: [ 52 | const SizedBox(width: 12), 53 | Text( 54 | '${product.name} 확인하기', 55 | style: const TextStyle(fontSize: 14, color: Colors.white), 56 | ), 57 | if (expand) const Spacer(), 58 | if (!expand) const SizedBox(width: 12), 59 | Icon(CupertinoIcons.chevron_right, color: Colors.white.withAlpha(150), size: 18), 60 | ], 61 | ), 62 | ), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/presentation/common/search_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SearchField extends StatelessWidget { 4 | const SearchField({super.key, this.onChanged, this.hintText}); 5 | 6 | final ValueChanged? onChanged; 7 | final String? hintText; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return TextField( 12 | onChanged: onChanged, 13 | cursorWidth: 1, 14 | cursorColor: Colors.grey.shade700, 15 | decoration: InputDecoration( 16 | contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), 17 | isCollapsed: true, 18 | filled: true, 19 | fillColor: Colors.grey.shade200, 20 | hintText: hintText, 21 | hintStyle: const TextStyle(fontWeight: FontWeight.w300), 22 | enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), 23 | focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/presentation/common/subject_filter_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../model/exam.dart'; 4 | import '../../util/color_extension.dart'; 5 | 6 | class ExamFilterChip extends StatelessWidget { 7 | ExamFilterChip({ 8 | super.key, 9 | required this.exam, 10 | required this.isSelected, 11 | required this.onSelected, 12 | }) : _darkColor = Color(exam.color).darken(0.1); 13 | 14 | final Exam exam; 15 | final bool isSelected; 16 | final Function() onSelected; 17 | final Color _darkColor; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Padding( 22 | padding: const EdgeInsets.symmetric(horizontal: 3), 23 | child: AnimatedSwitcher( 24 | duration: const Duration(milliseconds: 150), 25 | child: FilterChip( 26 | key: ValueKey('${exam.id} $isSelected'), 27 | label: Text( 28 | exam.name, 29 | style: TextStyle( 30 | color: isSelected ? Colors.white : _darkColor, 31 | fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, 32 | ), 33 | ), 34 | onSelected: (_) => onSelected(), 35 | selected: false, 36 | side: BorderSide(color: _darkColor, width: 0.4), 37 | backgroundColor: Color(exam.color).withAlpha(isSelected ? 255 : 10), 38 | pressElevation: 0, 39 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/presentation/common/subtitle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Subtitle extends StatelessWidget { 4 | const Subtitle({ 5 | super.key, 6 | required this.text, 7 | this.margin = const EdgeInsets.symmetric(horizontal: 16), 8 | }); 9 | 10 | final String text; 11 | final EdgeInsets margin; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | width: double.infinity, 17 | margin: margin, 18 | padding: const EdgeInsets.only(left: 8, right: 8, top: 12, bottom: 4), 19 | decoration: BoxDecoration( 20 | border: Border(top: BorderSide(color: Colors.grey.shade300, width: 1)), 21 | ), 22 | child: Text(text, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w900)), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/presentation/common/timeline_marker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TimelineMarker extends StatelessWidget { 4 | final double width; 5 | final double height; 6 | final Color color; 7 | 8 | const TimelineMarker({super.key, this.width = 8, this.height = 12, this.color = Colors.grey}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Center( 13 | child: ClipPath( 14 | clipper: TimelineMarkerClipper(), 15 | child: Container(width: width, height: height, color: color), 16 | ), 17 | ); 18 | } 19 | } 20 | 21 | class TimelineMarkerClipper extends CustomClipper { 22 | @override 23 | Path getClip(Size size) { 24 | final path = Path(); 25 | path.moveTo(0, 0); 26 | path.lineTo(size.width, 0); 27 | path.lineTo(size.width, size.height * 3 / 5); 28 | path.lineTo(size.width / 2, size.height); 29 | path.lineTo(0, size.height * 3 / 5); 30 | path.close(); 31 | return path; 32 | } 33 | 34 | @override 35 | bool shouldReclip(TimelineMarkerClipper oldClipper) => false; 36 | } 37 | -------------------------------------------------------------------------------- /lib/presentation/custom_exam_edit/cubit/custom_exam_edit_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../../model/exam.dart'; 7 | import '../../../model/subject.dart'; 8 | import '../../../repository/exam/exam_repository.dart'; 9 | import '../../../util/date_time_extension.dart'; 10 | import '../../app/cubit/app_cubit.dart'; 11 | 12 | part 'custom_exam_edit_cubit.freezed.dart'; 13 | part 'custom_exam_edit_state.dart'; 14 | 15 | @injectable 16 | class CustomExamEditCubit extends Cubit { 17 | CustomExamEditCubit(this._examRepository, this._appCubit) : super(const CustomExamEditState()); 18 | 19 | final ExamRepository _examRepository; 20 | final AppCubit _appCubit; 21 | 22 | void onBaseExamChanged(Exam exam) { 23 | emit(state.copyWith(showListeningEndAnnouncementEnabledField: exam.subject == Subject.english)); 24 | } 25 | 26 | void save({ 27 | required Exam? examToEdit, 28 | required String examName, 29 | required Exam baseExam, 30 | required TimeOfDay startTime, 31 | required int duration, 32 | required int numberOfQuestions, 33 | required int perfectScore, 34 | required bool isBeforeFinishAnnouncementEnabled, 35 | required bool isListeningEndAnnouncementEnabled, 36 | }) { 37 | final userId = _appCubit.state.me!.id; 38 | final newExam = Exam( 39 | id: examToEdit?.id ?? '$userId-${DateTime.now().millisecondsSinceEpoch}', 40 | userId: userId, 41 | subject: baseExam.subject, 42 | name: examName, 43 | number: baseExam.number, 44 | startTime: startTime.toDateTime(), 45 | durationMinutes: duration, 46 | numberOfQuestions: numberOfQuestions, 47 | perfectScore: perfectScore, 48 | isBeforeFinishAnnouncementEnabled: isBeforeFinishAnnouncementEnabled, 49 | isListeningEndAnnouncementEnabled: isListeningEndAnnouncementEnabled, 50 | color: baseExam.color, 51 | createdAt: examToEdit?.createdAt ?? DateTime.now(), 52 | ); 53 | 54 | if (examToEdit == null) { 55 | _examRepository.addExam(newExam); 56 | } else { 57 | _examRepository.updateExam(newExam); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/presentation/custom_exam_edit/cubit/custom_exam_edit_state.dart: -------------------------------------------------------------------------------- 1 | part of 'custom_exam_edit_cubit.dart'; 2 | 3 | @freezed 4 | class CustomExamEditState with _$CustomExamEditState { 5 | const factory CustomExamEditState({ 6 | @Default(false) final bool showListeningEndAnnouncementEnabledField, 7 | }) = _CustomExamEditState; 8 | } 9 | -------------------------------------------------------------------------------- /lib/presentation/customize_subject_name/cubit/customize_subject_name_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | 6 | import '../../../model/subject.dart'; 7 | import '../../../repository/user/user_repository.dart'; 8 | import '../../../util/analytics_manager.dart'; 9 | import '../../app/cubit/app_cubit.dart'; 10 | 11 | part 'customize_subject_name_cubit.freezed.dart'; 12 | part 'customize_subject_name_state.dart'; 13 | 14 | @injectable 15 | class CustomizeSubjectNameCubit extends Cubit { 16 | CustomizeSubjectNameCubit(this._appCubit, this._userRepository) 17 | : super(const CustomizeSubjectNameState()); 18 | 19 | final AppCubit _appCubit; 20 | final UserRepository _userRepository; 21 | 22 | void onFormChanged() { 23 | emit(state.copyWith(isFormChanged: true)); 24 | } 25 | 26 | Future save({required Map subjectNames}) async { 27 | if (_appCubit.state.isOffline) { 28 | EasyLoading.showToast('오프라인 상태에서는 수정할 수 없어요.', dismissOnTap: true); 29 | return; 30 | } 31 | 32 | final me = _appCubit.state.me; 33 | if (me == null) { 34 | EasyLoading.showError('로그인이 필요합니다.', dismissOnTap: true); 35 | return; 36 | } 37 | 38 | emit(state.copyWith(status: CustomizeSubjectNameStatus.saving)); 39 | 40 | await _userRepository.updateCustomSubjectNameMap(userId: me.id, subjectNameMap: subjectNames); 41 | await _appCubit.onUserChange(); 42 | 43 | emit(state.copyWith(status: CustomizeSubjectNameStatus.saved)); 44 | 45 | AnalyticsManager.logEvent( 46 | name: '[CustomizeSubjectNamePage] Subject Name Saved', 47 | properties: {'subjectNameMap': subjectNames.toString()}, 48 | ); 49 | AnalyticsManager.setPeopleProperty('Customized Subject Names', subjectNames.toString()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/presentation/customize_subject_name/cubit/customize_subject_name_state.dart: -------------------------------------------------------------------------------- 1 | part of 'customize_subject_name_cubit.dart'; 2 | 3 | enum CustomizeSubjectNameStatus { initial, saving, saved } 4 | 5 | @freezed 6 | class CustomizeSubjectNameState with _$CustomizeSubjectNameState { 7 | const factory CustomizeSubjectNameState({ 8 | @Default(CustomizeSubjectNameStatus.initial) CustomizeSubjectNameStatus status, 9 | @Default(false) bool isFormChanged, 10 | }) = _CustomizeSubjectNameState; 11 | } 12 | -------------------------------------------------------------------------------- /lib/presentation/exam_overview/cubit/exam_overview_state.dart: -------------------------------------------------------------------------------- 1 | part of 'exam_overview_cubit.dart'; 2 | 3 | @freezed 4 | class ExamOverviewState with _$ExamOverviewState { 5 | const ExamOverviewState._(); 6 | 7 | const factory ExamOverviewState({ 8 | @Default({}) Map> examToLapTimeItemGroups, 9 | @Default(false) bool isUsingExampleLapTimeItemGroups, 10 | @Default({}) Map examToRecordIds, 11 | @Default(false) bool isAutoSavingRecords, 12 | }) = _ExamOverviewState; 13 | 14 | String? getPrefillFeedbackForExamRecord(Exam exam) { 15 | final lapTimeItemGroups = examToLapTimeItemGroups[exam] ?? []; 16 | 17 | return (lapTimeItemGroups.isEmpty || isUsingExampleLapTimeItemGroups) 18 | ? null 19 | : lapTimeItemGroups.toCopyableString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/presentation/exam_overview/exam_overview_messages.dart: -------------------------------------------------------------------------------- 1 | part of 'exam_overview_page.dart'; 2 | 3 | const List _examOverviewMessages = [ 4 | '시험 끝!\n오늘도 목표에 한 발짝 더 다가갔어요 😆', 5 | '지금의 노력이 수능날의 1점!\n잘하고 있어요 🥳', 6 | '잘될 때는 겸손하게,\n안될 때는 자신있게! 💪', 7 | '올해가 내 인생의 마지막 수능 👨‍🎓\n오늘도 파이팅!', 8 | '하루에 1점씩만 올리자!\n오늘도 빡공하세요 🔥', 9 | ]; 10 | -------------------------------------------------------------------------------- /lib/presentation/exam_overview/example_lap_time_groups.dart: -------------------------------------------------------------------------------- 1 | import '../../model/announcement.dart'; 2 | import '../../model/lap_time.dart'; 3 | 4 | List getExampleLapTimeGroups({required DateTime startTime}) => [ 5 | LapTimeItemGroup( 6 | title: '예비령', 7 | startTime: startTime, 8 | announcementPurpose: AnnouncementPurpose.preliminary, 9 | lapTimeItems: [ 10 | LapTimeItem( 11 | time: startTime.add(const Duration(minutes: 1, seconds: 15)), 12 | timeDifference: const Duration(minutes: 1, seconds: 15), 13 | timeElapsed: const Duration(minutes: 1, seconds: 15), 14 | ), 15 | LapTimeItem( 16 | time: startTime.add(const Duration(minutes: 7, seconds: 33)), 17 | timeDifference: const Duration(minutes: 6, seconds: 18), 18 | timeElapsed: const Duration(minutes: 7, seconds: 33), 19 | ), 20 | ], 21 | ), 22 | LapTimeItemGroup( 23 | title: '본령', 24 | startTime: startTime.add(const Duration(minutes: 10)), 25 | announcementPurpose: AnnouncementPurpose.start, 26 | lapTimeItems: [ 27 | LapTimeItem( 28 | time: startTime.add(const Duration(minutes: 12, seconds: 5)), 29 | timeDifference: const Duration(minutes: 2, seconds: 5), 30 | timeElapsed: const Duration(minutes: 2, seconds: 5), 31 | ), 32 | LapTimeItem( 33 | time: startTime.add(const Duration(minutes: 18, seconds: 12)), 34 | timeDifference: const Duration(minutes: 6, seconds: 7), 35 | timeElapsed: const Duration(minutes: 8, seconds: 12), 36 | ), 37 | LapTimeItem( 38 | time: startTime.add(const Duration(minutes: 20, seconds: 44)), 39 | timeDifference: const Duration(minutes: 2, seconds: 32), 40 | timeElapsed: const Duration(minutes: 10, seconds: 44), 41 | ), 42 | LapTimeItem( 43 | time: startTime.add(const Duration(minutes: 25, seconds: 17)), 44 | timeDifference: const Duration(minutes: 4, seconds: 33), 45 | timeElapsed: const Duration(minutes: 15, seconds: 17), 46 | ), 47 | LapTimeItem( 48 | time: startTime.add(const Duration(minutes: 26, seconds: 48)), 49 | timeDifference: const Duration(minutes: 1, seconds: 31), 50 | timeElapsed: const Duration(minutes: 16, seconds: 48), 51 | ), 52 | LapTimeItem( 53 | time: startTime.add(const Duration(minutes: 29, seconds: 51)), 54 | timeDifference: const Duration(minutes: 3, seconds: 3), 55 | timeElapsed: const Duration(minutes: 19, seconds: 51), 56 | ), 57 | LapTimeItem( 58 | time: startTime.add(const Duration(minutes: 41, seconds: 27)), 59 | timeDifference: const Duration(minutes: 11, seconds: 36), 60 | timeElapsed: const Duration(minutes: 31, seconds: 27), 61 | ), 62 | LapTimeItem( 63 | time: startTime.add(const Duration(minutes: 56, seconds: 4)), 64 | timeDifference: const Duration(minutes: 14, seconds: 37), 65 | timeElapsed: const Duration(minutes: 46, seconds: 4), 66 | ), 67 | ], 68 | ), 69 | ]; 70 | -------------------------------------------------------------------------------- /lib/presentation/home/cubit/home_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | import '../home_page.dart'; 6 | 7 | part 'home_cubit.freezed.dart'; 8 | part 'home_state.dart'; 9 | 10 | @lazySingleton 11 | class HomeCubit extends Cubit { 12 | HomeCubit() : super(const HomeState()); 13 | 14 | static const defaultTabIndex = 0; 15 | 16 | void changeTab(int tabIndex) { 17 | emit(state.copyWith(tabIndex: tabIndex)); 18 | } 19 | 20 | void changeTabByTitle(String title) { 21 | final tabIndex = HomePage.views.keys.toList().indexOf(title); 22 | changeTab(tabIndex); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/presentation/home/cubit/home_state.dart: -------------------------------------------------------------------------------- 1 | part of 'home_cubit.dart'; 2 | 3 | @freezed 4 | class HomeState with _$HomeState { 5 | const factory HomeState({@Default(HomeCubit.defaultTabIndex) int tabIndex}) = _HomeState; 6 | } 7 | -------------------------------------------------------------------------------- /lib/presentation/home/main/cubit/main_state.dart: -------------------------------------------------------------------------------- 1 | part of 'main_cubit.dart'; 2 | 3 | @freezed 4 | class MainState with _$MainState { 5 | const factory MainState({ 6 | @Default([]) List ads, 7 | @Default([]) List dDayItems, 8 | @Default({}) Map adsShownLoggedMap, 9 | }) = _MainState; 10 | } 11 | -------------------------------------------------------------------------------- /lib/presentation/home/main/d_days_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import '../../common/custom_card.dart'; 6 | import 'cubit/main_cubit.dart'; 7 | 8 | class DDaysCard extends StatelessWidget { 9 | const DDaysCard({super.key}); 10 | 11 | Widget _buildDDayWidget(DDayItem dDayItem, Color primaryColor) { 12 | return Column( 13 | children: [ 14 | const SizedBox(height: 8), 15 | Row( 16 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 17 | crossAxisAlignment: CrossAxisAlignment.end, 18 | children: [ 19 | Column( 20 | crossAxisAlignment: CrossAxisAlignment.start, 21 | children: [ 22 | Text( 23 | DateFormat.yMEd('ko_KR').format(dDayItem.date), 24 | style: const TextStyle(fontWeight: FontWeight.w300, fontSize: 12), 25 | ), 26 | const SizedBox(height: 2), 27 | Text(dDayItem.title, style: const TextStyle(fontWeight: FontWeight.w700)), 28 | ], 29 | ), 30 | Text( 31 | 'D-${dDayItem.remainingDays == 0 ? 'Day' : dDayItem.remainingDays}', 32 | style: TextStyle( 33 | height: 0.4, 34 | color: primaryColor, 35 | fontWeight: FontWeight.w700, 36 | fontSize: 24, 37 | ), 38 | ), 39 | ], 40 | ), 41 | const SizedBox(height: 6), 42 | Container( 43 | height: 2, 44 | decoration: ShapeDecoration( 45 | shape: const StadiumBorder(), 46 | gradient: LinearGradient( 47 | colors: [primaryColor, primaryColor.withAlpha(30)], 48 | stops: [dDayItem.progress, dDayItem.progress], 49 | ), 50 | ), 51 | ), 52 | const SizedBox(height: 8), 53 | ], 54 | ); 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return BlocBuilder( 60 | buildWhen: (previous, current) => previous.dDayItems != current.dDayItems, 61 | builder: (context, state) { 62 | if (state.dDayItems.isEmpty) { 63 | return const SizedBox.shrink(); 64 | } 65 | 66 | return CustomCard( 67 | margin: const EdgeInsets.symmetric(vertical: 8), 68 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 69 | child: Column( 70 | children: [ 71 | for (DDayItem item in state.dDayItems) 72 | _buildDDayWidget(item, Theme.of(context).primaryColor), 73 | ], 74 | ), 75 | ); 76 | }, 77 | ); 78 | } 79 | } 80 | 81 | class DDayItem { 82 | final String title; 83 | final DateTime date; 84 | final int remainingDays; 85 | final double progress; 86 | 87 | const DDayItem({ 88 | required this.title, 89 | required this.date, 90 | required this.remainingDays, 91 | required this.progress, 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /lib/presentation/home/main/welcome_messages.dart: -------------------------------------------------------------------------------- 1 | const List welcomeMessages = [ 2 | '오늘도 빡공하세요! 🔥', 3 | '오늘도 파이팅! 🔥', 4 | '오늘의 노력 = 수능 날의 1점 💯', 5 | '하루에 1점씩만 올리자! 💯', 6 | '잘 하고 있어! 😊', 7 | '조금만 더 버티자! 🔥', 8 | '내 인생 마지막 수능 💯', 9 | ]; 10 | -------------------------------------------------------------------------------- /lib/presentation/home/record_list/cubit/record_list_state.dart: -------------------------------------------------------------------------------- 1 | part of 'record_list_cubit.dart'; 2 | 3 | @freezed 4 | class RecordListState with _$RecordListState { 5 | const RecordListState._(); 6 | 7 | const factory RecordListState({ 8 | @Default(false) bool isLoading, 9 | @Default([]) List originalRecords, 10 | @Default([]) List records, 11 | @Default('') String searchQuery, 12 | @Default(RecordSortType.dateDesc) RecordSortType sortType, 13 | @Default([]) List selectedExamIds, 14 | }) = _RecordListState; 15 | 16 | factory RecordListState.initial() { 17 | return const RecordListState(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/presentation/home/stat/cubit/stat_state.dart: -------------------------------------------------------------------------------- 1 | part of 'stat_cubit.dart'; 2 | 3 | @freezed 4 | class StatState with _$StatState { 5 | const StatState._(); 6 | 7 | const factory StatState({ 8 | @Default([]) List originalRecords, 9 | @Default({}) Map> records, 10 | @Default(false) bool isLoading, 11 | @Default('') String searchQuery, 12 | @Default([]) List selectedExamIds, 13 | @Default(false) bool isDateRangeSet, 14 | required DateTimeRange dateRange, 15 | required ExamValueType selectedExamValueType, 16 | }) = _StatState; 17 | 18 | factory StatState.initial() { 19 | return StatState( 20 | dateRange: DateTimeRange( 21 | start: DateTime.now().subtract(const Duration(days: 365)), 22 | end: DateTime.now(), 23 | ), 24 | selectedExamValueType: ExamValueType.values.first, 25 | ); 26 | } 27 | 28 | DateTime getDefaultStartDate({List? records}) { 29 | records ??= originalRecords; 30 | return (records.isEmpty 31 | ? DateTime.now().subtract(const Duration(days: 365)) 32 | : records.sortedBy((r) => r.examStartedTime).first.examStartedTime) 33 | .toDate(); 34 | } 35 | 36 | DateTime getDefaultEndDate({List? records}) { 37 | records ??= originalRecords; 38 | if (records.isEmpty) { 39 | return DateTime.now(); 40 | } 41 | 42 | final DateTime lastTime = records.sortedBy((r) => r.examStartedTime).last.examStartedTime; 43 | return lastTime.isAfter(DateTime.now()) ? lastTime.toDate() : DateTime.now().toDate(); 44 | } 45 | 46 | DateTimeRange get defaultDateRange => 47 | DateTimeRange(start: getDefaultStartDate(), end: getDefaultEndDate()); 48 | } 49 | -------------------------------------------------------------------------------- /lib/presentation/login/cubit/login_state.dart: -------------------------------------------------------------------------------- 1 | part of 'login_cubit.dart'; 2 | 3 | @freezed 4 | class LoginState with _$LoginState { 5 | const factory LoginState({@Default(false) bool isLoading}) = _LoginState; 6 | } 7 | -------------------------------------------------------------------------------- /lib/presentation/noise_setting/cubit/noise_setting_state.dart: -------------------------------------------------------------------------------- 1 | part of 'noise_setting_cubit.dart'; 2 | 3 | @freezed 4 | class NoiseSettingState with _$NoiseSettingState { 5 | const factory NoiseSettingState({ 6 | @Default(NoisePreset.disabled) NoisePreset selectedNoisePreset, 7 | @Default(false) bool useWhiteNoise, 8 | @Default({}) Map noiseLevels, 9 | }) = _NoiseSettingState; 10 | } 11 | -------------------------------------------------------------------------------- /lib/presentation/notification_setting/notification_setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 4 | import 'package:ui/ui.dart'; 5 | 6 | import '../app/cubit/app_cubit.dart'; 7 | import '../common/dialog.dart'; 8 | import '../home/settings/settings_view.dart'; 9 | 10 | class NotificationSettingPage extends StatelessWidget { 11 | const NotificationSettingPage({super.key}); 12 | 13 | static const routeName = '/notification_setting'; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return PageLayout( 18 | title: '알림 설정', 19 | onBackPressed: () => Navigator.of(context).pop(), 20 | child: BlocConsumer( 21 | listenWhen: 22 | (previous, current) => 23 | previous.me?.isMarketingInfoReceivingConsented != 24 | current.me?.isMarketingInfoReceivingConsented || 25 | previous.isOffline != current.isOffline, 26 | listener: (context, appState) { 27 | EasyLoading.dismiss(); 28 | }, 29 | builder: (context, appState) { 30 | final me = appState.me; 31 | 32 | if (me == null) { 33 | return const SizedBox.shrink(); 34 | } 35 | 36 | return Column( 37 | children: [ 38 | _buildSwitchTile( 39 | title: '마케팅 정보 수신 동의', 40 | description: '실감의 상품 소개, 이벤트 등 유용한 정보들을 푸시알림으로 받아보실 수 있어요.', 41 | value: me.isMarketingInfoReceivingConsented ?? false, 42 | onChanged: (value) => _onMarketingInfoReceivingConsentChanged(context, value), 43 | ), 44 | const SettingDivider(), 45 | ], 46 | ); 47 | }, 48 | ), 49 | ); 50 | } 51 | 52 | void _onMarketingInfoReceivingConsentChanged(BuildContext context, bool value) async { 53 | EasyLoading.show(); 54 | await changeMarketingInfoReceivingConsentStatus(context, value); 55 | } 56 | 57 | Widget _buildSwitchTile({ 58 | required String title, 59 | required String description, 60 | required bool value, 61 | required ValueChanged onChanged, 62 | }) { 63 | return InkWell( 64 | onTap: () => onChanged(!value), 65 | splashColor: Colors.transparent, 66 | child: Padding( 67 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), 68 | child: Row( 69 | children: [ 70 | Expanded( 71 | child: Column( 72 | crossAxisAlignment: CrossAxisAlignment.start, 73 | children: [ 74 | Text(title), 75 | const SizedBox(height: 4), 76 | Text( 77 | description, 78 | style: const TextStyle(fontSize: 12, color: Colors.grey, height: 1.4), 79 | ), 80 | ], 81 | ), 82 | ), 83 | const SizedBox(width: 16), 84 | Switch( 85 | value: value, 86 | onChanged: onChanged, 87 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 88 | ), 89 | ], 90 | ), 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/presentation/offline/offline_guide_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:ui/ui.dart'; 5 | 6 | class OfflineGuidePage extends StatelessWidget { 7 | const OfflineGuidePage({super.key}); 8 | 9 | static const routeName = '/offline_guide'; 10 | static const _maxWidth = 550.0; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final screenWidth = MediaQuery.of(context).size.width; 15 | return PageLayout( 16 | title: '오프라인 모드 이용 안내', 17 | onBackPressed: () => Navigator.pop(context), 18 | backgroundColor: Theme.of(context).primaryColor, 19 | textBrightness: Brightness.light, 20 | child: SingleChildScrollView( 21 | padding: EdgeInsets.symmetric(horizontal: max(0, (screenWidth - _maxWidth) / 2)), 22 | child: Column( 23 | children: [ 24 | Image.asset('assets/offline_guide_1.png', fit: BoxFit.contain), 25 | Image.asset('assets/offline_guide_2.png', fit: BoxFit.contain), 26 | Image.asset('assets/offline_guide_3.png', fit: BoxFit.contain), 27 | const SizedBox(height: 100), 28 | ], 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/presentation/onboarding/cubit/onboarding_state.dart: -------------------------------------------------------------------------------- 1 | part of 'onboarding_cubit.dart'; 2 | 3 | @freezed 4 | class OnboardingState with _$OnboardingState { 5 | const factory OnboardingState({ 6 | @Default(OnboardingStep.welcome) OnboardingStep step, 7 | @Default([]) List joinPaths, 8 | @Default([]) List selectedJoinPathIds, 9 | }) = _OnboardingState; 10 | } 11 | 12 | enum OnboardingStep { welcome, joinPath, finished } 13 | -------------------------------------------------------------------------------- /lib/presentation/onboarding/onboarding_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | import '../../util/injection.dart'; 5 | import '../home/home_page.dart'; 6 | import 'cubit/onboarding_cubit.dart'; 7 | import 'join_path/join_path_view.dart'; 8 | import 'welcome/welcome_view.dart'; 9 | 10 | class OnboardingPage extends StatelessWidget { 11 | OnboardingPage({super.key}); 12 | 13 | static const routeName = 'onboarding'; 14 | final OnboardingCubit _cubit = getIt.get(); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return BlocProvider.value( 19 | value: _cubit, 20 | child: BlocConsumer( 21 | listener: (context, state) { 22 | if (state.step == OnboardingStep.finished) { 23 | Navigator.pushReplacementNamed(context, HomePage.routeName); 24 | } 25 | }, 26 | buildWhen: (previous, current) => current.step != OnboardingStep.finished, 27 | builder: (context, state) { 28 | Widget child; 29 | switch (state.step) { 30 | case OnboardingStep.welcome: 31 | child = WelcomeView(key: const ValueKey(OnboardingStep.welcome)); 32 | case OnboardingStep.joinPath: 33 | child = const JoinPathView(key: ValueKey(OnboardingStep.joinPath)); 34 | case OnboardingStep.finished: 35 | child = Container(); 36 | } 37 | return AnimatedSwitcher(duration: const Duration(milliseconds: 400), child: child); 38 | }, 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/presentation/products/silgampass/silgampass_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:ui/ui.dart'; 4 | 5 | import '../../../util/injection.dart'; 6 | import '../../app/cubit/iap_cubit.dart'; 7 | import '../../purchase/purchase_page.dart'; 8 | 9 | class SilgampassPage extends StatelessWidget { 10 | const SilgampassPage({super.key}); 11 | 12 | static const routeName = '/products/silgampass'; 13 | 14 | void _onIapStateChanged(BuildContext context, IapState state) { 15 | final product = state.sellingProduct; 16 | if (product != null) { 17 | Future(() { 18 | if (!context.mounted) return; 19 | 20 | Navigator.of(context).pushReplacementNamed( 21 | PurchasePage.routeName, 22 | arguments: PurchasePageArguments(product: product), 23 | ); 24 | }); 25 | } 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | _onIapStateChanged(context, getIt.get().state); 31 | 32 | return BlocListener( 33 | listener: _onIapStateChanged, 34 | child: PageLayout( 35 | title: '실감패스', 36 | onBackPressed: () => Navigator.pop(context), 37 | backgroundColor: Theme.of(context).primaryColor, 38 | textBrightness: Brightness.light, 39 | child: const Center(child: CircularProgressIndicator(strokeWidth: 3, color: Colors.white)), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/presentation/purchase/cubit/purchase_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:injectable/injectable.dart'; 4 | 5 | part 'purchase_cubit.freezed.dart'; 6 | part 'purchase_state.dart'; 7 | 8 | @injectable 9 | class PurchaseCubit extends Cubit { 10 | PurchaseCubit() : super(const PurchaseState()); 11 | 12 | void onWebviewProgressChanged(int progress) { 13 | if (isClosed) return; 14 | emit(state.copyWith(isWebviewLoading: progress < 100)); 15 | } 16 | 17 | void purchaseSectionShown() { 18 | if (isClosed) return; 19 | emit(state.copyWith(isPurchaseSectionShown: true)); 20 | } 21 | 22 | void purchaseSectionHidden() { 23 | if (isClosed) return; 24 | emit(state.copyWith(isPurchaseSectionShown: false)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/presentation/purchase/cubit/purchase_state.dart: -------------------------------------------------------------------------------- 1 | part of 'purchase_cubit.dart'; 2 | 3 | @freezed 4 | class PurchaseState with _$PurchaseState { 5 | const factory PurchaseState({ 6 | @Default(true) bool isWebviewLoading, 7 | @Default(false) bool isPurchaseSectionShown, 8 | }) = _PurchaseState; 9 | } 10 | -------------------------------------------------------------------------------- /lib/repository/ads/ads_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | import '../../model/ads.dart'; 6 | 7 | part 'ads_api.g.dart'; 8 | 9 | @lazySingleton 10 | @RestApi() 11 | abstract class AdsApi { 12 | @factoryMethod 13 | factory AdsApi(Dio dio) = _AdsApi; 14 | 15 | @GET('/ads') 16 | Future> getAllAds(); 17 | } 18 | -------------------------------------------------------------------------------- /lib/repository/ads/ads_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | import 'package:multiple_result/multiple_result.dart'; 6 | 7 | import '../../model/ads.dart'; 8 | import '../../util/api_failure.dart'; 9 | import 'ads_api.dart'; 10 | 11 | @lazySingleton 12 | class AdsRepository { 13 | const AdsRepository(this._adsApi); 14 | 15 | final AdsApi _adsApi; 16 | 17 | Future, ApiFailure>> getAllAds() async { 18 | try { 19 | var ads = await _adsApi.getAllAds(); 20 | return Result.success(ads); 21 | } on DioException catch (e) { 22 | log(e.toString(), name: 'AdsRepository.getAllAds'); 23 | return Result.error(e.error as ApiFailure); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/repository/auth/auth_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | import 'dto/auth_dto.dart'; 6 | 7 | part 'auth_api.g.dart'; 8 | 9 | @lazySingleton 10 | @RestApi() 11 | abstract class AuthApi { 12 | @factoryMethod 13 | factory AuthApi(Dio dio) = _AuthApi; 14 | 15 | @POST('/auth/kakao') 16 | Future authKakao(@Body() AuthRequest body); 17 | } 18 | -------------------------------------------------------------------------------- /lib/repository/auth/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | import 'package:kakao_flutter_sdk/kakao_flutter_sdk.dart' as kakao; 6 | import 'package:multiple_result/multiple_result.dart'; 7 | 8 | import '../../util/api_failure.dart'; 9 | import 'auth_api.dart'; 10 | import 'dto/auth_dto.dart'; 11 | 12 | @lazySingleton 13 | class AuthRepository { 14 | const AuthRepository(this._authApi); 15 | 16 | final AuthApi _authApi; 17 | 18 | Future> authKakao(kakao.OAuthToken kakaoOAuthToken) async { 19 | final requestBody = AuthRequest(token: kakaoOAuthToken.accessToken); 20 | try { 21 | final response = await _authApi.authKakao(requestBody); 22 | return Result.success(response.firebaseToken); 23 | } on DioException catch (e) { 24 | log(e.toString(), name: 'AuthRepository.authKakao'); 25 | return Result.error(e.error as ApiFailure); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/repository/auth/dto/auth_dto.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | 5 | part 'auth_dto.freezed.dart'; 6 | part 'auth_dto.g.dart'; 7 | 8 | @freezed 9 | class AuthRequest with _$AuthRequest { 10 | const factory AuthRequest({required String token}) = _AuthRequest; 11 | 12 | factory AuthRequest.fromJson(Map json) => _$AuthRequestFromJson(json); 13 | 14 | factory AuthRequest.fromJsonString(String json) => AuthRequest.fromJson(jsonDecode(json)); 15 | } 16 | 17 | @freezed 18 | class AuthResponse with _$AuthResponse { 19 | const factory AuthResponse({required String firebaseToken}) = _AuthResponse; 20 | 21 | factory AuthResponse.fromJson(Map json) => _$AuthResponseFromJson(json); 22 | 23 | factory AuthResponse.fromJsonString(String json) => AuthResponse.fromJson(jsonDecode(json)); 24 | } 25 | -------------------------------------------------------------------------------- /lib/repository/dday/dday_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | import '../../model/dday.dart'; 6 | 7 | part 'dday_api.g.dart'; 8 | 9 | @lazySingleton 10 | @RestApi() 11 | abstract class DDayApi { 12 | @factoryMethod 13 | factory DDayApi(Dio dio) = _DDayApi; 14 | 15 | @GET('/ddays') 16 | Future> getAllDDays(); 17 | } 18 | -------------------------------------------------------------------------------- /lib/repository/dday/dday_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | import 'package:multiple_result/multiple_result.dart'; 6 | 7 | import '../../model/dday.dart'; 8 | import '../../util/api_failure.dart'; 9 | import 'dday_api.dart'; 10 | 11 | @lazySingleton 12 | class DDayRepository { 13 | const DDayRepository(this._ddayApi); 14 | 15 | final DDayApi _ddayApi; 16 | 17 | Future, ApiFailure>> getAllDDays() async { 18 | try { 19 | final ddays = await _ddayApi.getAllDDays(); 20 | final ddaysLocal = ddays.map((dday) => dday.copyWith(date: dday.date.toLocal())).toList(); 21 | return Result.success(ddaysLocal); 22 | } on DioException catch (e) { 23 | log(e.toString(), name: 'DDayRepository.getAllDDays'); 24 | return Result.error(e.error as ApiFailure); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/repository/exam/exam_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | 4 | import '../../model/exam.dart'; 5 | 6 | @lazySingleton 7 | class ExamRepository { 8 | final CollectionReference _examsCollection = FirebaseFirestore.instance 9 | .collection('exams') 10 | .withConverter( 11 | fromFirestore: (snapshot, _) => Exam.fromJson(snapshot.data()!), 12 | toFirestore: (exam, _) => exam.toJson(), 13 | ); 14 | 15 | void addExam(Exam exam) { 16 | _examsCollection.doc(exam.id).set(exam); 17 | } 18 | 19 | Future> getMyExams(String userId) async { 20 | final exams = await _examsCollection.where('userId', isEqualTo: userId).get(); 21 | return exams.docs.map((snapshot) => snapshot.data()).toList(); 22 | } 23 | 24 | void updateExam(Exam exam) { 25 | _examsCollection.doc(exam.id).update(exam.toJson()); 26 | } 27 | 28 | void deleteExam(String examId) { 29 | _examsCollection.doc(examId).delete(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/repository/feedback/dto/send_feedback_request.dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'send_feedback_request.dto.freezed.dart'; 4 | part 'send_feedback_request.dto.g.dart'; 5 | 6 | @freezed 7 | class SendFeedbackRequestDto with _$SendFeedbackRequestDto { 8 | const factory SendFeedbackRequestDto({ 9 | required String? userId, 10 | required String feedback, 11 | required String appVersion, 12 | required String os, 13 | required String osVersion, 14 | }) = _SendFeedbackRequestDto; 15 | 16 | factory SendFeedbackRequestDto.fromJson(Map json) => 17 | _$SendFeedbackRequestDtoFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/repository/feedback/feedback_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | import 'dto/send_feedback_request.dto.dart'; 6 | 7 | part 'feedback_api.g.dart'; 8 | 9 | @lazySingleton 10 | @RestApi() 11 | abstract class FeedbackApi { 12 | @factoryMethod 13 | factory FeedbackApi(Dio dio) = _FeedbackApi; 14 | 15 | @POST('/feedback') 16 | Future sendFeedback(@Body() SendFeedbackRequestDto request); 17 | } 18 | -------------------------------------------------------------------------------- /lib/repository/feedback/feedback_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'dart:io'; 3 | 4 | import 'package:dio/dio.dart'; 5 | import 'package:firebase_auth/firebase_auth.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | import 'package:multiple_result/multiple_result.dart'; 8 | import 'package:package_info_plus/package_info_plus.dart'; 9 | 10 | import '../../util/api_failure.dart'; 11 | import 'dto/send_feedback_request.dto.dart'; 12 | import 'feedback_api.dart'; 13 | 14 | @lazySingleton 15 | class FeedbackRepository { 16 | const FeedbackRepository(this._feedbackApi); 17 | 18 | final FeedbackApi _feedbackApi; 19 | 20 | Future> sendFeedback({required String feedback}) async { 21 | final request = SendFeedbackRequestDto( 22 | userId: FirebaseAuth.instance.currentUser?.uid, 23 | feedback: feedback, 24 | appVersion: await _getAppVersion(), 25 | os: Platform.operatingSystem, 26 | osVersion: Platform.operatingSystemVersion, 27 | ); 28 | try { 29 | await _feedbackApi.sendFeedback(request); 30 | return const Result.success(unit); 31 | } on DioException catch (e) { 32 | log(e.toString(), name: 'FeedbackRepository.sendFeedback'); 33 | return Result.error(e.error as ApiFailure); 34 | } 35 | } 36 | 37 | Future _getAppVersion() async { 38 | final packageInfo = await PackageInfo.fromPlatform(); 39 | return packageInfo.version; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/repository/onboarding/dto/submit_join_paths_request_dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'submit_join_paths_request_dto.freezed.dart'; 4 | part 'submit_join_paths_request_dto.g.dart'; 5 | 6 | @freezed 7 | class SubmitJoinPathsRequestDto with _$SubmitJoinPathsRequestDto { 8 | const factory SubmitJoinPathsRequestDto({ 9 | required List joinPathIds, 10 | required String? userId, 11 | required String? otherJoinPath, 12 | required bool isSkipped, 13 | }) = _SubmitJoinPathsRequestDto; 14 | 15 | factory SubmitJoinPathsRequestDto.fromJson(Map json) => 16 | _$SubmitJoinPathsRequestDtoFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/repository/onboarding/onboarding_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | import '../../model/join_path.dart'; 6 | import 'dto/submit_join_paths_request_dto.dart'; 7 | 8 | part 'onboarding_api.g.dart'; 9 | 10 | @lazySingleton 11 | @RestApi() 12 | abstract class OnboardingApi { 13 | @factoryMethod 14 | factory OnboardingApi(Dio dio) = _OnboardingApi; 15 | 16 | @GET('/join_paths') 17 | Future> getAllJoinPaths(); 18 | 19 | @POST('/onboarding/join_paths') 20 | Future submitJoinPaths(@Body() SubmitJoinPathsRequestDto request); 21 | } 22 | -------------------------------------------------------------------------------- /lib/repository/onboarding/onboarding_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:firebase_auth/firebase_auth.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | import 'package:multiple_result/multiple_result.dart'; 7 | 8 | import '../../model/join_path.dart'; 9 | import '../../util/api_failure.dart'; 10 | import 'dto/submit_join_paths_request_dto.dart'; 11 | import 'onboarding_api.dart'; 12 | 13 | @lazySingleton 14 | class OnboardingRepository { 15 | const OnboardingRepository(this._onboardingApi); 16 | 17 | final OnboardingApi _onboardingApi; 18 | 19 | Future, ApiFailure>> getAllJoinPaths() async { 20 | try { 21 | final joinPaths = await _onboardingApi.getAllJoinPaths(); 22 | return Result.success(joinPaths); 23 | } on DioException catch (e) { 24 | log(e.toString(), name: 'OnboardingRepository.getAllJoinPaths'); 25 | return Result.error(e.error as ApiFailure); 26 | } 27 | } 28 | 29 | Future> submitJoinPaths({ 30 | required List joinPathIds, 31 | required String? otherJoinPath, 32 | required bool isSkipped, 33 | }) async { 34 | final request = SubmitJoinPathsRequestDto( 35 | joinPathIds: joinPathIds, 36 | userId: FirebaseAuth.instance.currentUser?.uid, 37 | otherJoinPath: otherJoinPath, 38 | isSkipped: isSkipped, 39 | ); 40 | try { 41 | await _onboardingApi.submitJoinPaths(request); 42 | return const Result.success(unit); 43 | } on DioException catch (e) { 44 | log(e.toString(), name: 'OnboardingRepository.submitJoinPaths'); 45 | return Result.error(e.error as ApiFailure); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/repository/product/dto/can_purchase_request.dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'can_purchase_request.dto.freezed.dart'; 4 | part 'can_purchase_request.dto.g.dart'; 5 | 6 | @freezed 7 | class CanPurchaseRequestDto with _$CanPurchaseRequestDto { 8 | const factory CanPurchaseRequestDto({required String productId, required String store}) = 9 | _CanPurchaseRequestDto; 10 | 11 | factory CanPurchaseRequestDto.fromJson(Map json) => 12 | _$CanPurchaseRequestDtoFromJson(json); 13 | } 14 | -------------------------------------------------------------------------------- /lib/repository/product/dto/on_purchase_request.dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'on_purchase_request.dto.freezed.dart'; 4 | part 'on_purchase_request.dto.g.dart'; 5 | 6 | @freezed 7 | class OnPurchaseRequestDto with _$OnPurchaseRequestDto { 8 | const factory OnPurchaseRequestDto({ 9 | required String productId, 10 | required String store, 11 | required String verificationToken, 12 | }) = _OnPurchaseRequestDto; 13 | 14 | factory OnPurchaseRequestDto.fromJson(Map json) => 15 | _$OnPurchaseRequestDtoFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /lib/repository/product/dto/start_trial_request.dto.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'start_trial_request.dto.freezed.dart'; 4 | part 'start_trial_request.dto.g.dart'; 5 | 6 | @freezed 7 | class StartTrialRequestDto with _$StartTrialRequestDto { 8 | const factory StartTrialRequestDto({required String productId}) = _StartTrialRequestDto; 9 | 10 | factory StartTrialRequestDto.fromJson(Map json) => 11 | _$StartTrialRequestDtoFromJson(json); 12 | } 13 | -------------------------------------------------------------------------------- /lib/repository/product/product_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | import '../../model/product.dart'; 6 | import 'dto/can_purchase_request.dto.dart'; 7 | import 'dto/on_purchase_request.dto.dart'; 8 | import 'dto/start_trial_request.dto.dart'; 9 | 10 | part 'product_api.g.dart'; 11 | 12 | @lazySingleton 13 | @RestApi() 14 | abstract class ProductApi { 15 | @factoryMethod 16 | factory ProductApi(Dio dio) = _ProductApi; 17 | 18 | @GET('/products') 19 | Future> getAllProducts(); 20 | 21 | @POST('/iap/on-purchase') 22 | Future onPurchase( 23 | @Header('Authorization') String bearerToken, 24 | @Body() OnPurchaseRequestDto request, 25 | ); 26 | 27 | @POST('/iap/start-trial') 28 | Future startTrial( 29 | @Header('Authorization') String bearerToken, 30 | @Body() StartTrialRequestDto request, 31 | ); 32 | 33 | @POST('/iap/can-purchase') 34 | Future canPurchase( 35 | @Header('Authorization') String bearerToken, 36 | @Body() CanPurchaseRequestDto request, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /lib/repository/product/product_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:firebase_auth/firebase_auth.dart'; 5 | import 'package:injectable/injectable.dart'; 6 | import 'package:multiple_result/multiple_result.dart'; 7 | 8 | import '../../model/product.dart'; 9 | import '../../util/api_failure.dart'; 10 | import 'dto/can_purchase_request.dto.dart'; 11 | import 'dto/on_purchase_request.dto.dart'; 12 | import 'dto/start_trial_request.dto.dart'; 13 | import 'product_api.dart'; 14 | 15 | @lazySingleton 16 | class ProductRepository { 17 | const ProductRepository(this._productApi); 18 | 19 | final ProductApi _productApi; 20 | 21 | Future, ApiFailure>> getAllProducts() async { 22 | try { 23 | final products = await _productApi.getAllProducts(); 24 | final productsLocal = []; 25 | for (final product in products) { 26 | productsLocal.add( 27 | product.copyWith( 28 | expiryDate: product.expiryDate.toLocal(), 29 | sellingStartDate: product.sellingStartDate.toLocal(), 30 | sellingEndDate: product.sellingEndDate.toLocal(), 31 | ), 32 | ); 33 | } 34 | return Result.success(productsLocal); 35 | } on DioException catch (e) { 36 | log(e.toString(), name: 'ProductRepository.getActiveProducts'); 37 | return Result.error(e.error as ApiFailure); 38 | } 39 | } 40 | 41 | Future> onPurchase({ 42 | required String productId, 43 | required String store, 44 | required String verificationToken, 45 | }) async { 46 | final authToken = await FirebaseAuth.instance.currentUser?.getIdToken(); 47 | final request = OnPurchaseRequestDto( 48 | productId: productId, 49 | store: store, 50 | verificationToken: verificationToken, 51 | ); 52 | try { 53 | await _productApi.onPurchase('Bearer $authToken', request); 54 | return const Result.success(unit); 55 | } on DioException catch (e) { 56 | log(e.toString(), name: 'ProductRepository.onPurchase'); 57 | return Result.error(e.error as ApiFailure); 58 | } 59 | } 60 | 61 | Future> startTrial({required String productId}) async { 62 | final authToken = await FirebaseAuth.instance.currentUser?.getIdToken(); 63 | final request = StartTrialRequestDto(productId: productId); 64 | try { 65 | await _productApi.startTrial('Bearer $authToken', request); 66 | return const Result.success(unit); 67 | } on DioException catch (e) { 68 | log(e.toString(), name: 'ProductRepository.startTrial'); 69 | return Result.error(e.error as ApiFailure); 70 | } 71 | } 72 | 73 | Future> canPurchase({ 74 | required String productId, 75 | required String store, 76 | }) async { 77 | final authToken = await FirebaseAuth.instance.currentUser?.getIdToken(); 78 | final request = CanPurchaseRequestDto(productId: productId, store: store); 79 | try { 80 | await _productApi.canPurchase('Bearer $authToken', request); 81 | return const Result.success(unit); 82 | } on DioException catch (e) { 83 | log(e.toString(), name: 'ProductRepository.canPurchase'); 84 | return Result.error(e.error as ApiFailure); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/repository/user/user_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:injectable/injectable.dart'; 3 | import 'package:retrofit/retrofit.dart'; 4 | 5 | import '../../model/user.dart'; 6 | 7 | part 'user_api.g.dart'; 8 | 9 | @lazySingleton 10 | @RestApi() 11 | abstract class UserApi { 12 | @factoryMethod 13 | factory UserApi(Dio dio) = _UserApi; 14 | 15 | @GET('/users/me') 16 | Future getMe(@Header('Authorization') String bearerToken); 17 | } 18 | -------------------------------------------------------------------------------- /lib/util/android_audio_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart'; 5 | 6 | class AndroidAudioManager { 7 | static const _platform = MethodChannel('com.seunghyun.silgam/audio'); 8 | 9 | static Future controlMediaVolume() async { 10 | if (!kIsWeb && Platform.isAndroid) { 11 | await _platform.invokeMethod('controlMediaVolume'); 12 | } 13 | } 14 | 15 | static Future controlDefaultVolume() async { 16 | if (!kIsWeb && Platform.isAndroid) { 17 | await _platform.invokeMethod('controlDefaultVolume'); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/util/announcement_player.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:injectable/injectable.dart'; 5 | import 'package:just_audio/just_audio.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | import '../presentation/announcement_setting/announcement_type.dart'; 9 | import 'const.dart'; 10 | 11 | const _announcementsAssetPath = 'assets/announcements'; 12 | 13 | @injectable 14 | class AnnouncementPlayer extends AudioPlayer { 15 | final SharedPreferences _sharedPreferences; 16 | 17 | late final int _announcementTypeId = 18 | _sharedPreferences.getInt(PreferenceKey.announcementTypeId) ?? defaultAnnouncementType.id; 19 | 20 | AnnouncementPlayer(this._sharedPreferences) { 21 | if (!kIsWeb && Platform.isAndroid) setVolume(0.4); 22 | } 23 | 24 | Future setAnnouncement(String fileName, {int? announcementTypeId}) { 25 | return setAsset( 26 | '$_announcementsAssetPath/${announcementTypeId ?? _announcementTypeId}_$fileName', 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/util/api_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api_failure.freezed.dart'; 4 | 5 | @freezed 6 | class ApiFailure with _$ApiFailure implements Exception { 7 | const factory ApiFailure({required ApiFailureType type, required String message}) = _ApiFailure; 8 | 9 | factory ApiFailure.from(ApiFailureType type) => ApiFailure(type: type, message: type.message); 10 | 11 | factory ApiFailure.unknown() => ApiFailure.from(ApiFailureType.unknown); 12 | factory ApiFailure.unauthorized() => ApiFailure.from(ApiFailureType.unauthorized); 13 | factory ApiFailure.noNetwork() => ApiFailure.from(ApiFailureType.noNetwork); 14 | } 15 | 16 | enum ApiFailureType { 17 | unknown(message: '알 수 없는 오류가 발생했습니다.'), 18 | unauthorized(message: '인증에 실패했습니다. 다시 로그인해주세요.'), 19 | noNetwork(message: '인터넷 연결을 확인해주세요.'); 20 | 21 | const ApiFailureType({required this.message}); 22 | 23 | final String message; 24 | } 25 | -------------------------------------------------------------------------------- /lib/util/color_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension ColorExtension on Color { 4 | Color darken([double amount = .1]) { 5 | assert(amount >= 0 && amount <= 1); 6 | 7 | final hsl = HSLColor.fromColor(this); 8 | final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); 9 | 10 | return hslDark.toColor(); 11 | } 12 | 13 | Color lighten([double amount = .1]) { 14 | assert(amount >= 0 && amount <= 1); 15 | 16 | final hsl = HSLColor.fromColor(this); 17 | final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); 18 | 19 | return hslLight.toColor(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/util/connectivity_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | 4 | import 'package:connectivity_plus/connectivity_plus.dart'; 5 | import 'package:firebase_database/firebase_database.dart'; 6 | import 'package:injectable/injectable.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | 9 | import '../presentation/app/cubit/app_cubit.dart'; 10 | import '../presentation/app/cubit/iap_cubit.dart'; 11 | import 'injection.dart'; 12 | 13 | @lazySingleton 14 | class ConnectivityManger { 15 | late final AppCubit _appCubit = getIt.get(); 16 | 17 | StreamSubscription? _connectivityListener; 18 | 19 | String _sessionId = const Uuid().v1(); 20 | StreamSubscription? _realtimeDatabaseListener; 21 | DatabaseReference? _connectedAtRef; 22 | 23 | Future updateConnectivityListener() async { 24 | final connectivityResults = await Connectivity().checkConnectivity(); 25 | _onConnectivityChanged(connectivityResults); 26 | 27 | _connectivityListener?.cancel(); 28 | _connectivityListener = Connectivity().onConnectivityChanged.listen(_onConnectivityChanged); 29 | } 30 | 31 | void updateRealtimeDatabaseListener({required String? currentUserId}) { 32 | _realtimeDatabaseListener?.cancel(); 33 | _connectedAtRef 34 | ?..remove() 35 | ..onDisconnect().cancel(); 36 | 37 | _sessionId = const Uuid().v1(); 38 | final db = FirebaseDatabase.instance; 39 | _realtimeDatabaseListener = db.ref('.info/connected').onValue.skip(1).listen((event) { 40 | // ca0acf561495e6e8647f9b60cc911c3581d6de40 커밋 이후로 iOS에서 결제 시작 직후에 41 | // event.snapshot.value가 잠깐동안 false에서 true로 바뀌는 현상이 있어 결제 오류가 발생함 42 | // 따라서 결제 중에는 아래의 코드 무시 43 | // 슬랙: https://silgam.slack.com/archives/C04LTRHMD1R/p1724317524215279?thread_ts=1723657786.199189&cid=C04LTRHMD1R 44 | final isPurchasing = getIt.get().state.isLoading; 45 | if (isPurchasing) return; 46 | 47 | final connected = event.snapshot.value == true; 48 | log('Realtime database connected: $connected', name: runtimeType.toString()); 49 | _appCubit.updateIsOffline(!connected); 50 | 51 | if (connected) { 52 | final userId = currentUserId ?? 'anonymous'; 53 | _connectedAtRef = 54 | db.ref('users/$userId/sessions/$_sessionId/connectedAt') 55 | ..onDisconnect().remove() 56 | ..set(ServerValue.timestamp); 57 | } else { 58 | _sessionId = const Uuid().v1(); 59 | } 60 | }); 61 | } 62 | 63 | void _onConnectivityChanged(List connectivityResults) { 64 | log('Connectivity changed: $connectivityResults', name: 'AppCubit'); 65 | final isOffline = connectivityResults.contains(ConnectivityResult.none); 66 | _appCubit.updateIsOffline(isOffline); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/util/const.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | const double tabletScreenWidth = 840; 6 | const double maxWidth = 500; 7 | const double maxWidthForTablet = 1000; 8 | 9 | const String urlInstagram = "https://silgam.app/instagram"; 10 | const String urlSupport = "https://silgam.app/support"; 11 | const String urlOpenchat = "https://silgam.app/openchat"; 12 | const String urlPrivacy = "https://silgam.app/privacy"; 13 | const String urlTerms = "https://silgam.app/terms"; 14 | final String urlSilgamApi = 15 | useFirebaseEmulator 16 | ? "http://${Platform.isAndroid ? '10.0.2.2' : 'localhost'}:5001/silgam-app/asia-northeast3/api" 17 | : "https://api.silgam.app"; 18 | 19 | const isAdmobDisabled = false || kIsWeb; 20 | final String bannerAdId = 21 | Platform.isAndroid 22 | ? "ca-app-pub-5293956621132135/7574334463" 23 | : "ca-app-pub-5293956621132135/7145274842"; 24 | final String interstitialAdId = 25 | Platform.isAndroid 26 | ? "ca-app-pub-5293956621132135/1155168299" 27 | : "ca-app-pub-5293956621132135/5094413305"; 28 | 29 | abstract class PreferenceKey { 30 | static const useAutoSaveRecords = 'useAutoSaveRecords'; 31 | static const useLapTime = 'useLapTime'; 32 | static const noisePreset = 'noisePreset'; 33 | static const useWhiteNoise = 'whiteNoise'; 34 | static const cacheMe = 'me'; 35 | static const cacheProducts = 'products'; 36 | static const cacheAds = 'ads'; 37 | static const cacheDDays = 'ddays'; 38 | static const isOnboardingFinished = 'isOnboardingFinished'; 39 | static const announcementTypeId = 'announcementTypeId'; 40 | static const selectedAdsVariantIds = 'selectedAdsVariantIds'; 41 | } 42 | 43 | abstract class ProductId { 44 | static const free = 'free'; 45 | } 46 | 47 | const bool useFirebaseEmulator = false; 48 | -------------------------------------------------------------------------------- /lib/util/date_time_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension DateTimeBuilder on DateTime { 4 | static DateTime fromHourMinute(int hour, int minute) => DateTime(0, 1, 1, hour, minute); 5 | } 6 | 7 | extension DateTimeUtil on DateTime { 8 | DateTime toDate() { 9 | return DateTime(year, month, day); 10 | } 11 | 12 | bool isSameOrAfter(DateTime other) { 13 | return isAfter(other) || isAtSameMomentAs(other); 14 | } 15 | } 16 | 17 | extension DurationExtension on Duration { 18 | String to2DigitString() { 19 | final minutes = inMinutes.toString().padLeft(2, '0'); 20 | final seconds = (inSeconds % 60).toString().padLeft(2, '0'); 21 | return '$minutes:$seconds'; 22 | } 23 | } 24 | 25 | extension TimeOfDayExtension on TimeOfDay { 26 | DateTime toDateTime() { 27 | return DateTime(0, 1, 1, hour, minute); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/util/dday_util.dart: -------------------------------------------------------------------------------- 1 | import '../model/dday.dart'; 2 | import '../presentation/home/main/d_days_card.dart'; 3 | 4 | class DDayUtil { 5 | DDayUtil(this._dDays); 6 | 7 | final List _dDays; 8 | 9 | List getItemsToShow(DateTime today) { 10 | today = DateTime(today.year, today.month, today.day); 11 | DDayItem? suneungDDay = _getSuneungDDay(today); 12 | DDayItem? mockTestDDay = _getMockTestDDay(today); 13 | return [if (suneungDDay != null) suneungDDay, if (mockTestDDay != null) mockTestDDay]; 14 | } 15 | 16 | DDayItem? _getSuneungDDay(DateTime today) { 17 | final List suneungs = _dDays.where((test) => test.testType == DDayType.suneung).toList(); 18 | final DDay? previousSuneung = _getPreviousTest(today, suneungs); 19 | final DDay? nextSuneung = _getNextTest(today, suneungs); 20 | if (previousSuneung == null || nextSuneung == null) return null; 21 | 22 | final remainingDays = nextSuneung.date.difference(today).inDays; 23 | final totalDays = nextSuneung.date.difference(previousSuneung.date).inDays; 24 | return DDayItem( 25 | title: nextSuneung.title, 26 | date: nextSuneung.date, 27 | remainingDays: remainingDays, 28 | progress: 1 - remainingDays / totalDays, 29 | ); 30 | } 31 | 32 | DDayItem? _getMockTestDDay(DateTime today) { 33 | final DDay? previousMockTest = _getPreviousTest(today, _dDays); 34 | final DDay? nextMockTest = _getNextTest(today, _dDays); 35 | if (previousMockTest == null || 36 | nextMockTest == null || 37 | nextMockTest.testType == DDayType.suneung) { 38 | return null; 39 | } 40 | 41 | final remainingDays = nextMockTest.date.difference(today).inDays; 42 | final totalDays = nextMockTest.date.difference(previousMockTest.date).inDays; 43 | return DDayItem( 44 | title: nextMockTest.title, 45 | date: nextMockTest.date, 46 | remainingDays: remainingDays, 47 | progress: 1 - remainingDays / totalDays, 48 | ); 49 | } 50 | 51 | DDay? _getPreviousTest(DateTime today, List tests) { 52 | final previousTests = tests.where( 53 | (test) => test.date.add(const Duration(seconds: 1)).isBefore(today), 54 | ); 55 | return previousTests.isEmpty ? null : previousTests.last; 56 | } 57 | 58 | DDay? _getNextTest(DateTime today, List tests) { 59 | final nextTests = tests.where( 60 | (test) => test.date.add(const Duration(seconds: 1)).isAfter(today), 61 | ); 62 | return nextTests.isEmpty ? null : nextTests.first; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/util/duration_extension.dart: -------------------------------------------------------------------------------- 1 | extension DurationExtension on Duration { 2 | String toKoreanString() { 3 | final hours = inHours; 4 | final minutes = inMinutes % 60; 5 | if (hours == 0) { 6 | return '$minutes분'; 7 | } else { 8 | return '$hours시간 $minutes분'; 9 | } 10 | } 11 | 12 | /// 실제로는 60분이지만 미세한 차이로 59분 59초인 경우가 있기 때문에 1초를 더해서 이를 방지 13 | /// 14 | /// [관련 슬랙 메시지](https://silgam.slack.com/archives/C038LL94EUR/p1728268361059709?thread_ts=1727766981.046459&cid=C038LL94EUR) 15 | int get inMinutesWithCorrection => (this + const Duration(seconds: 1)).inMinutes; 16 | } 17 | -------------------------------------------------------------------------------- /lib/util/injection.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | import 'dart:io'; 4 | 5 | import 'package:dio/dio.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:get_it/get_it.dart'; 8 | import 'package:injectable/injectable.dart'; 9 | import 'package:pretty_dio_logger/pretty_dio_logger.dart'; 10 | import 'package:shared_preferences/shared_preferences.dart'; 11 | 12 | import '../presentation/app/cubit/app_cubit.dart'; 13 | import 'api_failure.dart'; 14 | import 'const.dart'; 15 | import 'injection.config.dart'; 16 | 17 | final getIt = GetIt.instance; 18 | 19 | @InjectableInit() 20 | Future configureDependencies() => getIt.init(); 21 | 22 | @module 23 | abstract class RegisterModule { 24 | @singleton 25 | @preResolve 26 | Future get sharedPreferences => SharedPreferences.getInstance(); 27 | 28 | @singleton 29 | Dio get dio => 30 | Dio( 31 | BaseOptions( 32 | baseUrl: urlSilgamApi, 33 | contentType: Headers.jsonContentType, 34 | sendTimeout: kIsWeb ? null : const Duration(seconds: 20), 35 | receiveTimeout: const Duration(seconds: 20), 36 | connectTimeout: const Duration(seconds: 20), 37 | ), 38 | ) 39 | ..interceptors.add(PrettyDioLogger(responseBody: false)) 40 | ..interceptors.add( 41 | InterceptorsWrapper( 42 | onResponse: (response, handler) { 43 | if (response.statusCode == 200) { 44 | getIt.get().updateIsOffline(false); 45 | } 46 | handler.next(response); 47 | }, 48 | onError: (e, handler) { 49 | log('Dio error: ${e.error}, type: ${e.type}', name: 'DioInterceptor'); 50 | 51 | final body = e.response?.data; 52 | ApiFailure failure; 53 | if (e.error is SocketException || 54 | e.type == DioExceptionType.connectionTimeout || 55 | e.type == DioExceptionType.sendTimeout || 56 | e.type == DioExceptionType.receiveTimeout) { 57 | failure = ApiFailure.noNetwork(); 58 | getIt.get().updateIsOffline(true); 59 | } else if (body is Map) { 60 | final message = body['message'] as String?; 61 | failure = ApiFailure( 62 | type: ApiFailureType.unknown, 63 | message: message ?? ApiFailureType.unknown.message, 64 | ); 65 | } else { 66 | failure = ApiFailure.unknown(); 67 | } 68 | 69 | handler.next(e.copyWith(error: failure)); 70 | }, 71 | ), 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /lib/util/string_util.dart: -------------------------------------------------------------------------------- 1 | String keepWord(text) { 2 | final RegExp emoji = RegExp( 3 | r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])', 4 | ); 5 | String fullText = ''; 6 | List words = text.split(' '); 7 | for (var i = 0; i < words.length; i++) { 8 | fullText += 9 | emoji.hasMatch(words[i]) 10 | ? words[i] 11 | : words[i].replaceAllMapped(RegExp(r'(\S)(?=\S)'), (m) => '${m[1]}\u200D'); 12 | if (i < words.length - 1) fullText += ' '; 13 | } 14 | return fullText; 15 | } 16 | -------------------------------------------------------------------------------- /markdown_assets/download_app_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/download_app_store.png -------------------------------------------------------------------------------- /markdown_assets/download_google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/download_google_play.png -------------------------------------------------------------------------------- /markdown_assets/feature_graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/feature_graphic.png -------------------------------------------------------------------------------- /markdown_assets/store_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_1.png -------------------------------------------------------------------------------- /markdown_assets/store_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_2.png -------------------------------------------------------------------------------- /markdown_assets/store_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_3.png -------------------------------------------------------------------------------- /markdown_assets/store_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_4.png -------------------------------------------------------------------------------- /markdown_assets/store_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_5.png -------------------------------------------------------------------------------- /markdown_assets/store_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_6.png -------------------------------------------------------------------------------- /markdown_assets/store_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_7.png -------------------------------------------------------------------------------- /markdown_assets/store_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/markdown_assets/store_8.png -------------------------------------------------------------------------------- /packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | build/ 32 | -------------------------------------------------------------------------------- /packages/ui/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /packages/ui/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | formatter: 4 | page_width: 100 5 | 6 | linter: 7 | rules: 8 | - prefer_relative_imports 9 | - prefer_const_constructors 10 | - prefer_const_declarations 11 | - prefer_const_literals_to_create_immutables 12 | -------------------------------------------------------------------------------- /packages/ui/lib/src/custom_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomAlertDialog extends StatelessWidget { 4 | CustomAlertDialog({ 5 | super.key, 6 | this.title, 7 | String? content, 8 | this.actions, 9 | this.scrollable = false, 10 | this.dimmedBackground = false, 11 | }) : content = content != null ? Text(content) : null; 12 | 13 | const CustomAlertDialog.customContent({ 14 | super.key, 15 | this.title, 16 | this.content, 17 | this.actions, 18 | this.scrollable = false, 19 | this.dimmedBackground = false, 20 | }); 21 | 22 | final String? title; 23 | final Widget? content; 24 | final List? actions; 25 | final bool scrollable; 26 | final bool dimmedBackground; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final title = this.title; 31 | final content = this.content; 32 | 33 | return AlertDialog( 34 | title: title != null ? Text(title, style: const TextStyle(fontWeight: FontWeight.w700)) : null, 35 | content: content, 36 | actions: actions, 37 | scrollable: scrollable, 38 | backgroundColor: dimmedBackground ? Theme.of(context).scaffoldBackgroundColor : null, 39 | elevation: 0, 40 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/ui/lib/src/custom_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppBarAction { 4 | final Key? key; 5 | final IconData iconData; 6 | final String tooltip; 7 | final VoidCallback? onPressed; 8 | 9 | const AppBarAction({this.key, required this.iconData, required this.tooltip, this.onPressed}); 10 | } 11 | 12 | class CustomAppBar extends StatelessWidget { 13 | const CustomAppBar({ 14 | super.key, 15 | this.title, 16 | this.onBackPressed, 17 | this.actions = const [], 18 | this.ignoreButtonPress = false, 19 | this.textBrightness = Brightness.dark, 20 | }); 21 | 22 | final String? title; 23 | final VoidCallback? onBackPressed; 24 | final List actions; 25 | final bool ignoreButtonPress; 26 | final Brightness textBrightness; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final Color textColor = textBrightness == Brightness.dark ? Colors.black : Colors.white; 31 | 32 | final title = this.title; 33 | 34 | return Padding( 35 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 36 | child: Row( 37 | children: [ 38 | Material( 39 | color: Colors.transparent, 40 | child: IconButton( 41 | onPressed: ignoreButtonPress ? () {} : onBackPressed, 42 | tooltip: '뒤로가기', 43 | splashRadius: 20, 44 | color: textColor, 45 | icon: const Icon(Icons.arrow_back), 46 | ), 47 | ), 48 | if (title != null) 49 | Expanded( 50 | child: Text( 51 | title, 52 | maxLines: 1, 53 | overflow: TextOverflow.ellipsis, 54 | style: TextStyle(color: textColor, fontSize: 20, fontWeight: FontWeight.w700), 55 | ), 56 | ) 57 | else 58 | const Spacer(), 59 | for (AppBarAction action in actions) 60 | Material( 61 | color: Colors.transparent, 62 | child: IconButton( 63 | key: action.key, 64 | onPressed: ignoreButtonPress ? () {} : action.onPressed, 65 | tooltip: action.tooltip, 66 | splashRadius: 20, 67 | color: textColor, 68 | icon: Icon(action.iconData), 69 | ), 70 | ), 71 | ], 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/ui/lib/src/custom_autocomplete.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomAutocomplete extends StatelessWidget { 4 | const CustomAutocomplete({ 5 | super.key, 6 | required this.optionsBuilder, 7 | required this.fieldViewBuilder, 8 | this.displayStringForOption = RawAutocomplete.defaultStringForOption, 9 | this.onSelected, 10 | this.initialValue, 11 | }); 12 | 13 | final AutocompleteOptionsBuilder optionsBuilder; 14 | final AutocompleteFieldViewBuilder fieldViewBuilder; 15 | final AutocompleteOptionToString displayStringForOption; 16 | final AutocompleteOnSelected? onSelected; 17 | final TextEditingValue? initialValue; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return LayoutBuilder( 22 | builder: (context, constraints) { 23 | return Autocomplete( 24 | optionsBuilder: optionsBuilder, 25 | fieldViewBuilder: fieldViewBuilder, 26 | displayStringForOption: displayStringForOption, 27 | onSelected: onSelected, 28 | initialValue: initialValue, 29 | optionsViewBuilder: (context, onSelected, options) { 30 | return Align( 31 | alignment: Alignment.topLeft, 32 | child: Container( 33 | margin: const EdgeInsets.symmetric(vertical: 4), 34 | decoration: BoxDecoration( 35 | color: Colors.white, 36 | borderRadius: BorderRadius.circular(8), 37 | boxShadow: [ 38 | BoxShadow( 39 | color: Colors.black.withAlpha(13), 40 | blurRadius: 6, 41 | offset: const Offset(0, 2), 42 | ), 43 | ], 44 | ), 45 | constraints: BoxConstraints(maxHeight: 200, maxWidth: constraints.maxWidth), 46 | child: ListView.separated( 47 | shrinkWrap: true, 48 | padding: EdgeInsets.zero, 49 | itemCount: options.length, 50 | itemBuilder: (context, index) { 51 | final option = options.elementAt(index); 52 | 53 | return ListTile( 54 | title: Text(displayStringForOption(option)), 55 | onTap: () { 56 | onSelected(option); 57 | }, 58 | ); 59 | }, 60 | separatorBuilder: (context, index) { 61 | return const Divider(height: 1); 62 | }, 63 | ), 64 | ), 65 | ); 66 | }, 67 | ); 68 | }, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/ui/lib/src/custom_filled_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomFilledButton extends StatelessWidget { 4 | const CustomFilledButton({ 5 | super.key, 6 | required this.label, 7 | this.isLoading = false, 8 | this.onPressed, 9 | this.borderRadius = 12, 10 | }); 11 | 12 | final String label; 13 | final bool isLoading; 14 | final VoidCallback? onPressed; 15 | final double borderRadius; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return FilledButton( 20 | onPressed: isLoading ? null : onPressed, 21 | style: FilledButton.styleFrom( 22 | tapTargetSize: MaterialTapTargetSize.shrinkWrap, 23 | padding: const EdgeInsets.symmetric(vertical: 16), 24 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius)), 25 | textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), 26 | disabledBackgroundColor: isLoading ? Theme.of(context).primaryColor.withAlpha(180) : null, 27 | disabledForegroundColor: isLoading ? Colors.white : null, 28 | ), 29 | child: 30 | isLoading 31 | ? const Padding( 32 | padding: EdgeInsets.only(right: 4), 33 | child: SizedBox( 34 | width: 16, 35 | height: 16, 36 | child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), 37 | ), 38 | ) 39 | : Text(label), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/ui/lib/src/custom_text_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum TextButtonVariant { primary, secondary, destructive } 4 | 5 | class CustomTextButton extends StatelessWidget { 6 | const CustomTextButton._({super.key, required this.variant, required this.text, this.onPressed}); 7 | 8 | factory CustomTextButton.primary({Key? key, required String text, VoidCallback? onPressed}) => 9 | CustomTextButton._( 10 | key: key, 11 | variant: TextButtonVariant.primary, 12 | text: text, 13 | onPressed: onPressed, 14 | ); 15 | 16 | factory CustomTextButton.secondary({Key? key, required String text, VoidCallback? onPressed}) => 17 | CustomTextButton._( 18 | key: key, 19 | variant: TextButtonVariant.secondary, 20 | text: text, 21 | onPressed: onPressed, 22 | ); 23 | 24 | factory CustomTextButton.destructive({Key? key, required String text, VoidCallback? onPressed}) => 25 | CustomTextButton._( 26 | key: key, 27 | variant: TextButtonVariant.destructive, 28 | text: text, 29 | onPressed: onPressed, 30 | ); 31 | 32 | final TextButtonVariant variant; 33 | final String text; 34 | final VoidCallback? onPressed; 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return TextButton(onPressed: onPressed, style: _getButtonStyle(), child: Text(text)); 39 | } 40 | 41 | ButtonStyle? _getButtonStyle() { 42 | return switch (variant) { 43 | TextButtonVariant.primary => null, 44 | TextButtonVariant.secondary => TextButton.styleFrom(foregroundColor: Colors.grey.shade600), 45 | TextButtonVariant.destructive => TextButton.styleFrom(foregroundColor: Colors.red), 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/ui/lib/src/form/form.dart: -------------------------------------------------------------------------------- 1 | export 'form_date_picker.dart'; 2 | export 'form_dropdown.dart'; 3 | export 'form_item.dart'; 4 | export 'form_numbers_field.dart'; 5 | export 'form_switch.dart'; 6 | export 'form_text_field.dart'; 7 | export 'form_time_picker.dart'; 8 | -------------------------------------------------------------------------------- /packages/ui/lib/src/form/form_date_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_form_builder/flutter_form_builder.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | class FormDatePicker extends StatelessWidget { 6 | const FormDatePicker({ 7 | super.key, 8 | required this.name, 9 | this.initialValue, 10 | required this.firstDate, 11 | required this.lastDate, 12 | this.autoWidth = false, 13 | }); 14 | 15 | final String name; 16 | final DateTime? initialValue; 17 | final DateTime firstDate; 18 | final DateTime lastDate; 19 | final bool autoWidth; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final fieldWidget = FormBuilderField( 24 | name: name, 25 | initialValue: initialValue, 26 | builder: (field) { 27 | final value = field.value; 28 | final state = field as FormBuilderFieldState, DateTime>; 29 | 30 | return GestureDetector( 31 | onTap: 32 | state.enabled 33 | ? () async { 34 | final date = await showDatePicker( 35 | context: context, 36 | initialDate: value, 37 | firstDate: firstDate, 38 | lastDate: lastDate, 39 | ); 40 | if (date == null) return; 41 | 42 | field.didChange(date); 43 | } 44 | : null, 45 | child: InputDecorator( 46 | decoration: InputDecoration( 47 | contentPadding: const EdgeInsets.all(12), 48 | isCollapsed: true, 49 | filled: true, 50 | fillColor: Colors.white, 51 | enabledBorder: OutlineInputBorder( 52 | borderSide: BorderSide(width: 0.5, color: Colors.grey.shade300), 53 | borderRadius: const BorderRadius.all(Radius.circular(6)), 54 | ), 55 | ), 56 | child: Text( 57 | value != null ? DateFormat.yMEd('ko_KR').format(value) : '', 58 | style: TextTheme.of(context).titleMedium?.copyWith( 59 | color: state.enabled ? null : Theme.of(context).disabledColor, 60 | ), 61 | ), 62 | ), 63 | ); 64 | }, 65 | ); 66 | 67 | if (autoWidth) { 68 | return IntrinsicWidth(child: fieldWidget); 69 | } 70 | 71 | return fieldWidget; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/ui/lib/src/form/form_dropdown.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_form_builder/flutter_form_builder.dart'; 3 | 4 | class FormDropdown extends StatelessWidget { 5 | const FormDropdown({ 6 | super.key, 7 | required this.name, 8 | required this.items, 9 | this.initialValue, 10 | this.onChanged, 11 | }); 12 | 13 | final String name; 14 | final List> items; 15 | final T? initialValue; 16 | final ValueChanged? onChanged; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return FormBuilderDropdown( 21 | name: name, 22 | items: items, 23 | initialValue: initialValue, 24 | onChanged: onChanged, 25 | style: TextTheme.of(context).titleMedium?.copyWith(overflow: TextOverflow.ellipsis), 26 | decoration: InputDecoration( 27 | hintStyle: TextStyle(color: Colors.grey.shade500), 28 | contentPadding: const EdgeInsets.all(12), 29 | isCollapsed: true, 30 | filled: true, 31 | fillColor: Colors.white, 32 | enabledBorder: OutlineInputBorder( 33 | borderSide: BorderSide(width: 0.5, color: Colors.grey.shade300), 34 | borderRadius: const BorderRadius.all(Radius.circular(6)), 35 | ), 36 | focusedBorder: OutlineInputBorder( 37 | borderSide: BorderSide(width: 0.5, color: Theme.of(context).primaryColor), 38 | borderRadius: const BorderRadius.all(Radius.circular(6)), 39 | ), 40 | errorBorder: const OutlineInputBorder( 41 | borderSide: BorderSide(width: 0.5, color: Colors.red), 42 | borderRadius: BorderRadius.all(Radius.circular(6)), 43 | ), 44 | focusedErrorBorder: const OutlineInputBorder( 45 | borderSide: BorderSide(width: 0.5, color: Colors.red), 46 | borderRadius: BorderRadius.all(Radius.circular(6)), 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/ui/lib/src/form/form_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FormItem extends StatelessWidget { 4 | const FormItem({ 5 | super.key, 6 | required this.label, 7 | required this.child, 8 | this.isRequired = false, 9 | this.description, 10 | }); 11 | 12 | final String label; 13 | final Widget child; 14 | final bool isRequired; 15 | final String? description; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final description = this.description; 20 | 21 | return Column( 22 | crossAxisAlignment: CrossAxisAlignment.start, 23 | spacing: 6, 24 | children: [ 25 | _FormLabel(label: label, isRequired: isRequired), 26 | if (description != null) 27 | Text(description, style: const TextStyle(color: Colors.grey, fontSize: 12)), 28 | child, 29 | ], 30 | ); 31 | } 32 | } 33 | 34 | class _FormLabel extends StatelessWidget { 35 | const _FormLabel({required this.label, this.isRequired = false}); 36 | 37 | final String label; 38 | final bool isRequired; 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | final labelWidget = Text( 43 | label, 44 | style: TextStyle(color: Colors.grey.shade900, fontWeight: FontWeight.w500, fontSize: 15), 45 | ); 46 | 47 | if (isRequired) { 48 | return Row( 49 | spacing: 2, 50 | children: [labelWidget, const Text('*', style: TextStyle(color: Colors.red))], 51 | ); 52 | } 53 | 54 | return labelWidget; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/ui/lib/src/form/form_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_form_builder/flutter_form_builder.dart'; 3 | 4 | class FormSwitch extends StatelessWidget { 5 | const FormSwitch({ 6 | super.key, 7 | required this.name, 8 | this.initialValue, 9 | required this.title, 10 | this.subtitle, 11 | }); 12 | 13 | final String name; 14 | final bool? initialValue; 15 | final String title; 16 | final String? subtitle; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final subtitle = this.subtitle; 21 | 22 | return FormBuilderSwitch( 23 | name: name, 24 | initialValue: initialValue, 25 | title: Text(title, style: const TextStyle(fontSize: 14)), 26 | subtitle: 27 | subtitle != null 28 | ? Text( 29 | subtitle, 30 | style: const TextStyle(fontSize: 12, height: 1.4, color: Colors.grey), 31 | ) 32 | : null, 33 | contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), 34 | decoration: const InputDecoration(border: InputBorder.none, contentPadding: EdgeInsets.zero), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/ui/lib/src/form/form_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_form_builder/flutter_form_builder.dart'; 4 | 5 | class FormTextField extends StatelessWidget { 6 | const FormTextField({ 7 | super.key, 8 | required this.name, 9 | this.initialValue, 10 | this.hintText, 11 | this.suffixText, 12 | this.validator, 13 | this.inputFormatters, 14 | this.textInputAction, 15 | this.keyboardType, 16 | this.hideError = false, 17 | this.autoWidth = false, 18 | this.controller, 19 | this.focusNode, 20 | this.onSubmitted, 21 | this.minLines, 22 | this.maxLines = 1, 23 | }); 24 | 25 | final String name; 26 | final String? initialValue; 27 | final String? hintText; 28 | final String? suffixText; 29 | final FormFieldValidator? validator; 30 | final List? inputFormatters; 31 | final TextInputAction? textInputAction; 32 | final TextInputType? keyboardType; 33 | final bool hideError; 34 | final bool autoWidth; 35 | final TextEditingController? controller; 36 | final FocusNode? focusNode; 37 | final ValueChanged? onSubmitted; 38 | final int? minLines; 39 | final int? maxLines; 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | final fieldWidget = FormBuilderTextField( 44 | name: name, 45 | initialValue: initialValue, 46 | validator: validator, 47 | inputFormatters: inputFormatters, 48 | textInputAction: textInputAction, 49 | keyboardType: keyboardType, 50 | controller: controller, 51 | focusNode: focusNode, 52 | onSubmitted: onSubmitted, 53 | minLines: minLines, 54 | maxLines: maxLines, 55 | autovalidateMode: AutovalidateMode.onUserInteraction, 56 | decoration: InputDecoration( 57 | hintText: hintText, 58 | suffixText: suffixText, 59 | errorStyle: hideError ? const TextStyle(height: 0.001) : null, 60 | hintStyle: TextStyle(color: Colors.grey.shade500), 61 | contentPadding: const EdgeInsets.all(12), 62 | isCollapsed: true, 63 | filled: true, 64 | fillColor: Colors.white, 65 | enabledBorder: OutlineInputBorder( 66 | borderSide: BorderSide(width: 0.5, color: Colors.grey.shade300), 67 | borderRadius: const BorderRadius.all(Radius.circular(6)), 68 | ), 69 | disabledBorder: OutlineInputBorder( 70 | borderSide: BorderSide(width: 0.5, color: Colors.grey.shade300), 71 | borderRadius: const BorderRadius.all(Radius.circular(6)), 72 | ), 73 | focusedBorder: OutlineInputBorder( 74 | borderSide: BorderSide(width: 0.5, color: Theme.of(context).primaryColor), 75 | borderRadius: const BorderRadius.all(Radius.circular(6)), 76 | ), 77 | errorBorder: const OutlineInputBorder( 78 | borderSide: BorderSide(width: 0.5, color: Colors.red), 79 | borderRadius: BorderRadius.all(Radius.circular(6)), 80 | ), 81 | focusedErrorBorder: const OutlineInputBorder( 82 | borderSide: BorderSide(width: 0.5, color: Colors.red), 83 | borderRadius: BorderRadius.all(Radius.circular(6)), 84 | ), 85 | ), 86 | ); 87 | 88 | if (autoWidth) { 89 | return IntrinsicWidth(child: fieldWidget); 90 | } 91 | 92 | return fieldWidget; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/ui/lib/src/form/form_time_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_form_builder/flutter_form_builder.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | class FormTimePicker extends StatelessWidget { 6 | const FormTimePicker({super.key, required this.name, this.initialValue, this.autoWidth = false}); 7 | 8 | final String name; 9 | final TimeOfDay? initialValue; 10 | final bool autoWidth; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final fieldWidget = FormBuilderField( 15 | name: name, 16 | initialValue: initialValue, 17 | builder: (field) { 18 | final value = field.value; 19 | final state = field as FormBuilderFieldState, TimeOfDay>; 20 | 21 | return GestureDetector( 22 | onTap: 23 | state.enabled 24 | ? () async { 25 | final time = await showTimePicker( 26 | context: context, 27 | initialTime: value ?? TimeOfDay.now(), 28 | ); 29 | if (time == null) return; 30 | 31 | field.didChange(time); 32 | } 33 | : null, 34 | child: InputDecorator( 35 | decoration: InputDecoration( 36 | contentPadding: const EdgeInsets.all(12), 37 | isCollapsed: true, 38 | filled: true, 39 | fillColor: Colors.white, 40 | enabledBorder: OutlineInputBorder( 41 | borderSide: BorderSide(width: 0.5, color: Colors.grey.shade300), 42 | borderRadius: const BorderRadius.all(Radius.circular(6)), 43 | ), 44 | ), 45 | child: Text( 46 | value != null 47 | ? DateFormat.jm('ko_KR').format(DateTime(0, 0, 0, value.hour, value.minute)) 48 | : '', 49 | style: TextTheme.of(context).titleMedium?.copyWith( 50 | color: state.enabled ? null : Theme.of(context).disabledColor, 51 | ), 52 | ), 53 | ), 54 | ); 55 | }, 56 | ); 57 | 58 | if (autoWidth) { 59 | return IntrinsicWidth(child: fieldWidget); 60 | } 61 | 62 | return fieldWidget; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/ui/lib/src/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const String defaultFontFamily = 'NanumSquare'; 4 | 5 | final textButtonTheme = TextButtonThemeData( 6 | style: TextButton.styleFrom( 7 | textStyle: const TextStyle(fontFamily: defaultFontFamily, fontWeight: FontWeight.w700), 8 | ), 9 | ); 10 | 11 | final outlinedButtonTheme = OutlinedButtonThemeData( 12 | style: OutlinedButton.styleFrom( 13 | textStyle: const TextStyle(fontFamily: defaultFontFamily, fontWeight: FontWeight.w700), 14 | ), 15 | ); 16 | 17 | SliderThemeData getSliderTheme(BuildContext context) => SliderTheme.of(context).copyWith( 18 | trackHeight: 3, 19 | trackShape: const RectangularSliderTrackShape(), 20 | overlayShape: const RoundSliderOverlayShape(overlayRadius: 12), 21 | overlayColor: Colors.transparent, 22 | thumbShape: SliderComponentShape.noThumb, 23 | showValueIndicator: ShowValueIndicator.always, 24 | ); 25 | -------------------------------------------------------------------------------- /packages/ui/lib/ui.dart: -------------------------------------------------------------------------------- 1 | export 'src/custom_alert_dialog.dart'; 2 | export 'src/custom_app_bar.dart'; 3 | export 'src/custom_autocomplete.dart'; 4 | export 'src/custom_filled_button.dart'; 5 | export 'src/custom_text_button.dart'; 6 | export 'src/form/form.dart'; 7 | export 'src/page_layout.dart'; 8 | export 'src/theme.dart'; 9 | -------------------------------------------------------------------------------- /packages/ui/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ui 2 | version: 0.0.1 3 | publish_to: none 4 | 5 | environment: 6 | sdk: 3.7.2 7 | flutter: 3.29.3 8 | 9 | resolution: workspace 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | flutter_form_builder: ^9.7.0 15 | intl: ^0.19.0 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | flutter_lints: ^5.0.0 21 | mocktail: ^1.0.4 22 | 23 | flutter: 24 | uses-material-design: true 25 | -------------------------------------------------------------------------------- /packages/ui/test/src/custom_text_button_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mocktail/mocktail.dart'; 4 | import 'package:ui/ui.dart'; 5 | 6 | abstract class VoidCallback { 7 | void call(); 8 | } 9 | 10 | class MockVoidCallback extends Mock implements VoidCallback {} 11 | 12 | void main() { 13 | group('CustomTextButton', () { 14 | late MockVoidCallback mockCallback; 15 | 16 | setUp(() { 17 | mockCallback = MockVoidCallback(); 18 | }); 19 | 20 | testWidgets('renders text correctly', (tester) async { 21 | await tester.pumpWidget( 22 | MaterialApp(home: CustomTextButton.primary(text: 'Button Text', onPressed: () {})), 23 | ); 24 | 25 | expect(find.text('Button Text'), findsOneWidget); 26 | }); 27 | 28 | testWidgets('calls onPressed when tapped', (tester) async { 29 | await tester.pumpWidget( 30 | MaterialApp(home: CustomTextButton.primary(text: 'Button', onPressed: mockCallback.call)), 31 | ); 32 | 33 | verifyNever(() => mockCallback.call()); 34 | 35 | await tester.tap(find.byType(TextButton)); 36 | await tester.pump(); 37 | 38 | verify(() => mockCallback.call()).called(1); 39 | }); 40 | 41 | testWidgets('primary variant uses default style', (tester) async { 42 | await tester.pumpWidget( 43 | MaterialApp(home: CustomTextButton.primary(text: 'Primary', onPressed: () {})), 44 | ); 45 | 46 | final TextButton button = tester.widget(find.byType(TextButton)); 47 | expect(button.style, isNull); 48 | }); 49 | 50 | testWidgets('secondary variant has grey foreground color', (tester) async { 51 | await tester.pumpWidget( 52 | MaterialApp(home: CustomTextButton.secondary(text: 'Secondary', onPressed: () {})), 53 | ); 54 | 55 | final TextButton button = tester.widget(find.byType(TextButton)); 56 | expect(button.style?.foregroundColor?.resolve({}), Colors.grey.shade600); 57 | }); 58 | 59 | testWidgets('destructive variant has red foreground color', (tester) async { 60 | await tester.pumpWidget( 61 | MaterialApp(home: CustomTextButton.destructive(text: 'Delete', onPressed: () {})), 62 | ); 63 | 64 | final TextButton button = tester.widget(find.byType(TextButton)); 65 | 66 | expect(button.style?.foregroundColor?.resolve({}), Colors.red); 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /project_initializer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -x 5 | 6 | curl -o ./silgam_private_files.zip $1 7 | unzip silgam_private_files.zip 8 | 9 | mv silgam_private_files/announcements ./assets/announcements 10 | mv silgam_private_files/noises ./assets/noises 11 | mv silgam_private_files/app_env.dart ./lib/app_env.dart 12 | mv silgam_private_files/firebase_options.dart ./lib/firebase_options.dart 13 | mv silgam_private_files/GoogleService-Info.plist ./ios/Runner/GoogleService-Info.plist 14 | mv silgam_private_files/google-services.json ./android/app/google-services.json 15 | mv silgam_private_files/firebase-messaging-sw.js ./web/firebase-messaging-sw.js 16 | 17 | flutter pub run build_runner build --delete-conflicting-outputs 18 | -------------------------------------------------------------------------------- /user_data_deletion.md: -------------------------------------------------------------------------------- 1 | # 사용자 데이터 삭제 안내 2 | 3 | 데이터 삭제를 원하시면 아래 주소로 연락 바랍니다. 4 | 5 | 카카오톡 문의: [https://silgam.app/kakaotalk](https://silgam.app/kakaotalk) 6 | 이메일: [i@seunghyun.in](mailto:i@seunghyun.in) 7 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silgam/silgam-flutter/251be3af36840dceeaf892d0468f740462c06c1a/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | silgam 33 | 34 | 35 | 39 | 40 | 41 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silgam", 3 | "short_name": "silgam", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------