├── .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 |
--------------------------------------------------------------------------------