├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── basic_template.md │ └── release_template.md ├── actions │ ├── get-app-version │ │ └── action.yml │ ├── setup-development-environment │ │ └── action.yml │ ├── setup-key-environment │ │ └── action.yml │ └── update-app-version │ │ └── action.yml ├── auto_assign_config.yml └── workflows │ ├── deploy-new-version-play-store.yml │ ├── deploy_firebase-distribution_debug.yml │ ├── deploy_firebase-distribution_release.yml │ ├── pr_auto_assign.yml │ ├── pr_template.yml │ ├── verify_debug_build_test.yml │ └── verify_release_build_test.yml ├── .gitignore ├── README.md ├── app-version.json ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ ├── drawable │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── ic_launcher_background.xml │ │ └── strings.xml │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── pomonyang │ │ └── mohanyang │ │ ├── MainActivity.kt │ │ ├── MainElements.kt │ │ ├── MainViewModel.kt │ │ ├── MohaNyangApplication.kt │ │ ├── di │ │ ├── ManagerModule.kt │ │ └── ServiceModule.kt │ │ ├── navigation │ │ └── MohaNyangNavHost.kt │ │ ├── notification │ │ ├── FocusNotificationService.kt │ │ ├── LocalNotificationReceiver.kt │ │ ├── MnAlarmManager.kt │ │ ├── MnFirebaseMessagingService.kt │ │ └── util │ │ │ └── NotificationUtils.kt │ │ ├── ui │ │ ├── BottomNavItem.kt │ │ ├── MohaNyangApp.kt │ │ ├── MohaNyangAppState.kt │ │ ├── ServiceHelper.kt │ │ └── component │ │ │ └── MohaNyangBottomBar.kt │ │ └── util │ │ └── DebugTimberTree.kt │ └── res │ ├── drawable │ ├── app_splash.xml │ └── ic_app_notification.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── mipmap-hdpi │ └── ic_launcher_foreground.webp │ ├── mipmap-mdpi │ └── ic_launcher_foreground.webp │ ├── mipmap-xhdpi │ └── ic_launcher_foreground.webp │ ├── mipmap-xxhdpi │ └── ic_launcher_foreground.webp │ ├── mipmap-xxxhdpi │ └── ic_launcher_foreground.webp │ ├── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build-logic ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── java │ └── com │ └── pomonyang │ └── mohanyang │ ├── AndroidApplicationComposeConventionPlugin.kt │ ├── AndroidApplicationConventionPlugin.kt │ ├── AndroidApplicationFirebaseConventionPlugin.kt │ ├── AndroidDatadogConventionPlugin.kt │ ├── AndroidHiltConventionPlugin.kt │ ├── AndroidLibraryComposeConventionPlugin.kt │ ├── AndroidLibraryConventionPlugin.kt │ ├── AppVersionPlugin.kt │ └── convention │ ├── AndroidCompose.kt │ ├── DependencyHandlerExtensions.kt │ ├── GithubUtils.kt │ ├── KotlinAndroid.kt │ ├── ProjectConfigurations.kt │ ├── ProjectExtensions.kt │ └── Secrets.kt ├── build.gradle.kts ├── data ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── pomonyang │ └── mohanyang │ └── data │ ├── local │ ├── datastore │ │ ├── datasource │ │ │ ├── deviceid │ │ │ │ ├── DeviceIdLocalDataSource.kt │ │ │ │ └── DeviceIdLocalDataSourceImpl.kt │ │ │ ├── notification │ │ │ │ ├── NotificationLocalDataSource.kt │ │ │ │ └── NotificationLocalDataSourceImpl.kt │ │ │ ├── token │ │ │ │ ├── TokenLocalDataSource.kt │ │ │ │ └── TokenLocalDataSourceImpl.kt │ │ │ └── user │ │ │ │ ├── UserLocalDataSource.kt │ │ │ │ └── UserLocalDataSourceImpl.kt │ │ ├── di │ │ │ ├── DataStoreModule.kt │ │ │ ├── DataStoreQualifier.kt │ │ │ └── LocalDataSourceModule.kt │ │ ├── model │ │ │ └── .gitkeep │ │ └── util │ │ │ └── .gitkeep │ ├── device │ │ ├── receiver │ │ │ └── LockScreenBroadcastReceiver.kt │ │ └── util │ │ │ └── LockScreenUtils.kt │ └── room │ │ ├── dao │ │ ├── PomodoroSettingDao.kt │ │ └── PomodoroTimerDao.kt │ │ ├── database │ │ ├── PomodoroSettingDataBase.kt │ │ └── PomodoroTimerDataBase.kt │ │ ├── di │ │ └── RoomModule.kt │ │ ├── enitity │ │ ├── PomodoroSettingEntity.kt │ │ └── PomodoroTimerEntity.kt │ │ └── util │ │ └── TimeUtils.kt │ ├── remote │ ├── datasource │ │ ├── auth │ │ │ ├── AuthRemoteDataSource.kt │ │ │ └── AuthRemoteDataSourceImpl.kt │ │ └── pomodoro │ │ │ ├── PomodoroSettingRemoteDataSource.kt │ │ │ └── PomodoroSettingRemoteDataSourceImpl.kt │ ├── di │ │ ├── ClientQualifier.kt │ │ ├── NetworkModule.kt │ │ └── RemoteDataSourceModule.kt │ ├── interceptor │ │ ├── HttpRequestInterceptor.kt │ │ └── TokenRefreshInterceptor.kt │ ├── model │ │ ├── request │ │ │ ├── AddCategoryRequest.kt │ │ │ ├── DeleteCategoryRequest.kt │ │ │ ├── PomodoroTimerRequest.kt │ │ │ ├── RefreshTokenRequest.kt │ │ │ ├── RegisterPushTokenRequest.kt │ │ │ ├── TokenRequest.kt │ │ │ ├── UpdateCatInfoRequest.kt │ │ │ ├── UpdateCatTypeRequest.kt │ │ │ └── UpdateCategoryInfoRequest.kt │ │ └── response │ │ │ ├── CatTypeResponse.kt │ │ │ ├── ErrorResponse.kt │ │ │ ├── PomodoroSettingResponse.kt │ │ │ ├── StatisticsResponse.kt │ │ │ ├── TokenResponse.kt │ │ │ └── UserInfoResponse.kt │ ├── service │ │ ├── AuthService.kt │ │ └── MohaNyangService.kt │ └── util │ │ ├── NetworkException.kt │ │ ├── NetworkMonitor.kt │ │ ├── NetworkResultCall.kt │ │ ├── NetworkResultCallAdapter.kt │ │ └── NetworkResultCallAdapterFactory.kt │ └── repository │ ├── cat │ ├── CatSettingRepository.kt │ └── CatSettingRepositoryImpl.kt │ ├── di │ └── RepositoryModule.kt │ ├── pomodoro │ ├── PomodoroSettingRepository.kt │ ├── PomodoroSettingRepositoryImpl.kt │ ├── PomodoroTimerRepository.kt │ └── PomodoroTimerRepositoryImpl.kt │ ├── push │ ├── PushAlarmRepository.kt │ └── PushAlarmRepositoryImpl.kt │ ├── statistics │ ├── StatisticsRepository.kt │ └── StatisticsRepositoryImpl.kt │ ├── user │ ├── UserRepository.kt │ └── UserRepositoryImpl.kt │ └── util │ └── TimerUtil.kt ├── domain ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ └── java │ └── com │ └── pomonyang │ └── mohanyang │ └── domain │ └── usecase │ ├── AdjustPomodoroTimeUseCase.kt │ ├── GetSelectedPomodoroSettingUseCase.kt │ ├── GetTokenByDeviceIdUseCase.kt │ └── InsertPomodoroInitialDataUseCase.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── presentation ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro └── src │ └── main │ ├── java │ └── com │ │ └── pomonyang │ │ └── mohanyang │ │ └── presentation │ │ ├── base │ │ ├── BaseViewElements.kt │ │ └── BaseViewModel.kt │ │ ├── component │ │ ├── CatRive.kt │ │ ├── CategoryBox.kt │ │ ├── FocusTimerSelecterButtons.kt │ │ ├── Timer.kt │ │ └── TimerType.kt │ │ ├── designsystem │ │ ├── bottomsheet │ │ │ ├── MnBottomSheet.kt │ │ │ └── MnBottomSheetDefaults.kt │ │ ├── button │ │ │ ├── box │ │ │ │ ├── MnBoxButton.kt │ │ │ │ ├── MnBoxButtonColorType.kt │ │ │ │ ├── MnBoxButtonColors.kt │ │ │ │ └── MnBoxButtonStyles.kt │ │ │ ├── common │ │ │ │ ├── MnButtonStyleProperties.kt │ │ │ │ └── MnPressableWrapper.kt │ │ │ ├── icon │ │ │ │ └── MnIconButton.kt │ │ │ ├── round │ │ │ │ ├── MnRoundButton.kt │ │ │ │ ├── MnRoundButtonColorType.kt │ │ │ │ └── MnRoundButtonColors.kt │ │ │ ├── select │ │ │ │ ├── MnSelectButton.kt │ │ │ │ ├── MnSelectButtonSelector.kt │ │ │ │ ├── MnSelectList.kt │ │ │ │ └── MnSelectListDefaults.kt │ │ │ ├── text │ │ │ │ ├── MnTextButton.kt │ │ │ │ └── MnTextButtonStyles.kt │ │ │ └── toggle │ │ │ │ ├── MnToggleButton.kt │ │ │ │ └── MnToggleButtonSize.kt │ │ ├── datepicker │ │ │ ├── MnDatePickerDefaults.kt │ │ │ └── MnDatePickerDialog.kt │ │ ├── dialog │ │ │ ├── MnDialog.kt │ │ │ └── MnDialogDefaults.kt │ │ ├── icon │ │ │ ├── MnLargeIcon.kt │ │ │ ├── MnMediumIcon.kt │ │ │ ├── MnSmallIcon.kt │ │ │ ├── MnXLargeIcon.kt │ │ │ └── MnXSmallIcon.kt │ │ ├── picker │ │ │ ├── MnWheelMinutePicker.kt │ │ │ └── MnWheelPickerDefaults.kt │ │ ├── spinner │ │ │ └── MnSpinner.kt │ │ ├── textfield │ │ │ └── MnTextField.kt │ │ ├── toast │ │ │ └── MnToastSnackbarHost.kt │ │ ├── token │ │ │ ├── MnColor.kt │ │ │ ├── MnIconSize.kt │ │ │ ├── MnInteraction.kt │ │ │ ├── MnRadius.kt │ │ │ ├── MnSpacing.kt │ │ │ ├── MnStroke.kt │ │ │ └── MnTypography.kt │ │ ├── tooltip │ │ │ ├── MnTooltip.kt │ │ │ └── MnTooltipDefaults.kt │ │ └── topappbar │ │ │ ├── MnTopAppBar.kt │ │ │ └── MnTopAppBarDefaults.kt │ │ ├── di │ │ ├── MohanyangLoggerModule.kt │ │ ├── PomodoroModule.kt │ │ └── Qualifier.kt │ │ ├── model │ │ ├── cat │ │ │ ├── CatInfoModel.kt │ │ │ └── CatType.kt │ │ ├── category │ │ │ └── PomodoroCategoryModel.kt │ │ ├── setting │ │ │ └── PomodoroSettingModel.kt │ │ └── user │ │ │ └── UserInfoModel.kt │ │ ├── noti │ │ ├── PomodoroNotificationBitmapGenerator.kt │ │ ├── PomodoroNotificationContentFactory.kt │ │ └── PomodoroNotificationManager.kt │ │ ├── screen │ │ ├── PomodoroConstants.kt │ │ ├── common │ │ │ ├── LoadingScreen.kt │ │ │ ├── NetworkErrorDialog.kt │ │ │ ├── NetworkErrorScreen.kt │ │ │ └── ServerErrorScreen.kt │ │ ├── home │ │ │ ├── HomeNavigator.kt │ │ │ ├── category │ │ │ │ ├── CategoryIconBottomSheet.kt │ │ │ │ ├── CategoryNameVerifier.kt │ │ │ │ ├── CategorySettingElements.kt │ │ │ │ ├── CategorySettingScreen.kt │ │ │ │ ├── CategorySettingViewModel.kt │ │ │ │ ├── PomodoroCategoryBottomSheet.kt │ │ │ │ ├── component │ │ │ │ │ ├── CategoryActionMoreMenuList.kt │ │ │ │ │ ├── CategoryBottomSheetContents.kt │ │ │ │ │ └── CategoryBottomSheetHeaderContents.kt │ │ │ │ └── model │ │ │ │ │ ├── CategoryIcon.kt │ │ │ │ │ ├── CategoryManageState.kt │ │ │ │ │ └── CategoryModel.kt │ │ │ ├── setting │ │ │ │ ├── PomodoroSettingElements.kt │ │ │ │ ├── PomodoroSettingScreen.kt │ │ │ │ └── PomodoroSettingViewModel.kt │ │ │ └── time │ │ │ │ ├── PomodoroTimeSettingElements.kt │ │ │ │ ├── PomodoroTimeSettingScreen.kt │ │ │ │ └── PomodoroTimeSettingViewModel.kt │ │ ├── mypage │ │ │ ├── MyPageElements.kt │ │ │ ├── MyPageNavigator.kt │ │ │ ├── MyPageScreen.kt │ │ │ ├── MyPageViewModel.kt │ │ │ ├── component │ │ │ │ └── FocusStatisticBox.kt │ │ │ └── profile │ │ │ │ ├── CatProfileScreen.kt │ │ │ │ └── CatProfileViewModel.kt │ │ ├── onboarding │ │ │ ├── OnboardingNavigator.kt │ │ │ ├── guide │ │ │ │ ├── OnboardingGuideScreen.kt │ │ │ │ └── OnboardingGuideViewModel.kt │ │ │ ├── model │ │ │ │ └── OnboardingGuideContent.kt │ │ │ ├── naming │ │ │ │ ├── CatNameVerifier.kt │ │ │ │ ├── OnboardingNamingCatScreen.kt │ │ │ │ ├── OnboardingNamingCatViewModel.kt │ │ │ │ └── OnboardingNamingElements.kt │ │ │ └── select │ │ │ │ ├── OnboardingSelectCatElements.kt │ │ │ │ ├── OnboardingSelectCatScreen.kt │ │ │ │ └── OnboardingSelectCatViewModel.kt │ │ ├── pomodoro │ │ │ ├── PomodoroNavigator.kt │ │ │ ├── focus │ │ │ │ ├── PomodoroFocusElements.kt │ │ │ │ ├── PomodoroFocusScreen.kt │ │ │ │ └── PomodoroFocusViewModel.kt │ │ │ ├── rest │ │ │ │ ├── PomodoroRestElements.kt │ │ │ │ ├── PomodoroRestScreen.kt │ │ │ │ └── PomodoroRestViewModel.kt │ │ │ └── waiting │ │ │ │ ├── PomodoroBreakReadyElements.kt │ │ │ │ ├── PomodoroBreakReadyScreen.kt │ │ │ │ └── PomodoroBreakReadyViewModel.kt │ │ └── statistics │ │ │ ├── StaticsNavigator.kt │ │ │ ├── StatisticsElements.kt │ │ │ ├── StatisticsScreen.kt │ │ │ ├── StatisticsViewModel.kt │ │ │ ├── component │ │ │ ├── Dot.kt │ │ │ ├── FocusTimeListItem.kt │ │ │ ├── StatisticsContentHeader.kt │ │ │ ├── StatisticsTopBar.kt │ │ │ ├── TotalFocusTimeContent.kt │ │ │ ├── datepicker │ │ │ │ ├── StatisticDatePickerModal.kt │ │ │ │ └── StatisticDateState.kt │ │ │ └── graph │ │ │ │ ├── FocusGraphConfigure.kt │ │ │ │ └── FocusTimeGrpahBox.kt │ │ │ ├── model │ │ │ ├── StatisticsModel.kt │ │ │ └── mapper │ │ │ │ └── StatisticsMapper.kt │ │ │ └── widget │ │ │ └── StatisticsContent.kt │ │ ├── service │ │ ├── BasePomodoroTimer.kt │ │ ├── PomodoroTimer.kt │ │ ├── PomodoroTimerEventHandler.kt │ │ ├── PomodoroTimerServiceExtras.kt │ │ ├── focus │ │ │ ├── FocusTimer.kt │ │ │ └── PomodoroFocusTimerService.kt │ │ └── rest │ │ │ ├── PomodoroRestTimerService.kt │ │ │ └── RestTimer.kt │ │ ├── theme │ │ └── MnTheme.kt │ │ └── util │ │ ├── DpPxSpConversionUtils.kt │ │ ├── FlowUtils.kt │ │ ├── IntentUtils.kt │ │ ├── MnNotificationManager.kt │ │ ├── ModifierUtils.kt │ │ ├── MohanyangEventLogger.kt │ │ ├── NavigationUtils.kt │ │ ├── PreviewUtils.kt │ │ ├── TimeUtils.kt │ │ └── TimerUtils.kt │ └── res │ ├── drawable │ ├── ic_alert.xml │ ├── ic_app.xml │ ├── ic_arrow_down.xml │ ├── ic_arrow_left.xml │ ├── ic_arrow_right.xml │ ├── ic_arrow_up.xml │ ├── ic_asterisk.xml │ ├── ic_box_pen.xml │ ├── ic_brifecase.xml │ ├── ic_bubble_ellipses.xml │ ├── ic_category_default.xml │ ├── ic_chart_bar.xml │ ├── ic_chart_bar_fill.xml │ ├── ic_check.xml │ ├── ic_check_32.xml │ ├── ic_check_circle.xml │ ├── ic_chevron_down.xml │ ├── ic_chevron_left.xml │ ├── ic_chevron_right.xml │ ├── ic_chevron_up.xml │ ├── ic_circle.xml │ ├── ic_clock.xml │ ├── ic_close.xml │ ├── ic_dumbbell.xml │ ├── ic_ellipsis.xml │ ├── ic_error.xml │ ├── ic_feedback.xml │ ├── ic_fire.xml │ ├── ic_heart.xml │ ├── ic_house.xml │ ├── ic_house_fill.xml │ ├── ic_internet.xml │ ├── ic_lightning.xml │ ├── ic_lock.xml │ ├── ic_menu.xml │ ├── ic_minus.xml │ ├── ic_monitor.xml │ ├── ic_moon.xml │ ├── ic_null.xml │ ├── ic_open_book.xml │ ├── ic_pen.xml │ ├── ic_play.xml │ ├── ic_play_32.xml │ ├── ic_plus.xml │ ├── ic_sort_offline.xml │ ├── ic_static_ready.xml │ ├── ic_sun.xml │ ├── ic_trashcan.xml │ ├── ic_user.xml │ ├── ic_user_fill.xml │ ├── img_touch_hair_ball.png │ ├── onboarding_contents_1.xml │ ├── onboarding_contents_2.xml │ └── onboarding_contents_3.xml │ ├── font │ ├── pretendard_bold.ttf │ ├── pretendard_regular.ttf │ └── pretendard_semibold.ttf │ ├── layout │ ├── notification_pomodoro_expand.xml │ └── notification_pomodoro_standard.xml │ ├── raw │ ├── alarm.mp3 │ ├── cat_focus.riv │ ├── cat_home.riv │ ├── cat_rename_2.riv │ ├── cat_rest.riv │ ├── cat_select_2.riv │ ├── loti_rest_waiting.json │ ├── loti_rest_waiting_complete_focus.json │ └── spinner.json │ └── values │ ├── arrays.xml │ ├── colors.xml │ └── strings.xml └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | end_of_line = lf 3 | ij_kotlin_allow_trailing_comma = true 4 | ij_kotlin_allow_trailing_comma_on_call_site = true 5 | ij_kotlin_imports_layout = * 6 | ij_kotlin_line_break_after_multiline_when_entry = true 7 | ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.** 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | ktlint_code_style = android_studio 12 | ktlint_function_naming_ignore_when_annotated_with = Composable, Test 13 | ktlint_function_signature_body_expression_wrapping = default 14 | ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset 15 | ktlint_ignore_back_ticked_identifier = true 16 | ktlint_standard_value-parameter-comment = disabled 17 | ktlint_standard_filename = disabled 18 | max_line_length = off -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/basic_template.md: -------------------------------------------------------------------------------- 1 | ## 작업 내용 2 | 3 | ## 체크리스트 4 | - [ ] 빌드 확인 5 | 6 | ## 동작 화면 7 | 8 | ## 살려주세요 9 | 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/release_template.md: -------------------------------------------------------------------------------- 1 | ## RELEASE 모하냥 2 | 3 | > 해당하는 업데이트 사항을 선택해주세요: 4 | 5 | - [ ] Major 6 | - [ ] Minor 7 | - [ ] Patch 8 | 9 | ## 설명 10 | 11 | 이 PR에 포함된 변경 사항을 간략히 설명해주세요. 12 | 13 | ## 체크리스트 14 | 15 | - [ ] 체크 해야 하는 내용 16 | -------------------------------------------------------------------------------- /.github/actions/get-app-version/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Get App Version' 2 | description: 'Get App Version' 3 | 4 | outputs: 5 | major: 6 | description: "앱 버전 중 major" 7 | value: ${{ steps.get-app-version.outputs.major }} 8 | minor: 9 | description: "앱 버전 중 minor" 10 | value: ${{ steps.get-app-version.outputs.minor }} 11 | patch: 12 | description: "앱 버전 중 patch" 13 | value: ${{ steps.get-app-version.outputs.patch }} 14 | code: 15 | description: "앱 버전 코드" 16 | value: ${{ steps.get-app-version.outputs.code }} 17 | version_name: 18 | description: "앱 버전 이름" 19 | value: ${{ steps.get-app-version.outputs.version_name }} 20 | 21 | runs: 22 | using: "composite" 23 | steps: 24 | - name: Get App Version 25 | id: get-app-version 26 | shell: bash 27 | run: | 28 | major=$(jq .major app-version.json) 29 | minor=$(jq .minor app-version.json) 30 | patch=$(jq .patch app-version.json) 31 | code=$(jq .code app-version.json) 32 | 33 | { 34 | echo "major=$major" 35 | echo "minor=$minor" 36 | echo "patch=$patch" 37 | echo "code=$code" 38 | echo "version_name=$major.$minor.$patch" 39 | } >> $GITHUB_OUTPUT -------------------------------------------------------------------------------- /.github/actions/setup-development-environment/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Development Environment" 2 | 3 | inputs: 4 | google-services: 5 | description: 'Google Services Json' 6 | required: true 7 | test-mode: 8 | description: 'Test debug or release mode' 9 | required: false 10 | debug-properties: 11 | description: 'Secret Debug Properties' 12 | required: false 13 | release-properties: 14 | description: 'Secret Release Properties' 15 | required: false 16 | 17 | runs: 18 | using: "composite" 19 | steps: 20 | - name: Setup JDK 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: 'zulu' 24 | java-version: '21' 25 | 26 | - name: Setup Gradle 27 | uses: gradle/actions/setup-gradle@v3 28 | 29 | - name: Create google-services.json 30 | shell: bash 31 | run: echo -n "${{ inputs.google-services }}" | base64 --decode > ./app/google-services.json 32 | 33 | - name: Create Debug properties file 34 | shell: bash 35 | run: echo -n "${{ inputs.debug-properties }}" > ./debug.secrets.properties 36 | 37 | - name: Create Release properties file 38 | shell: bash 39 | run: echo -n "${{ inputs.release-properties }}" > ./release.secrets.properties 40 | -------------------------------------------------------------------------------- /.github/actions/setup-key-environment/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Release Environment" 2 | 3 | inputs: 4 | key-properties: 5 | description: 'Secret Key Properties' 6 | required: true 7 | key-file: 8 | description: 'Signed Key' 9 | required: true 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: Setup JDK 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'zulu' 18 | java-version: '21' 19 | 20 | - name: Setup Gradle 21 | uses: gradle/actions/setup-gradle@v3 22 | 23 | - name: Create Key Properties 24 | shell: bash 25 | run: echo -n "${{ inputs.key-properties }}" > ./key.secrets.properties 26 | 27 | - name: Create Signed Key 28 | shell: bash 29 | run: echo -n "${{ inputs.key-file }}" | base64 --decode > ./key-release -------------------------------------------------------------------------------- /.github/actions/update-app-version/action.yml: -------------------------------------------------------------------------------- 1 | name: "Update App Version" 2 | 3 | inputs: 4 | major: 5 | description: "업데이트 할 앱 major" 6 | required: true 7 | minor: 8 | description: "업데이트 할 앱 버전 minor" 9 | required: true 10 | patch: 11 | description: "업데이트 할 앱 버전 patch" 12 | required: true 13 | code: 14 | description: "업데이트 할 앱 버전 code" 15 | required: true 16 | file: 17 | description: "업데이트 할 파일 경로" 18 | required: true 19 | 20 | runs: 21 | using: "composite" 22 | steps: 23 | - name: Update app version 24 | shell: bash 25 | if: success() 26 | run: | 27 | VERSION_FILE="${{ inputs.file }}" 28 | if command -v jq &> /dev/null; then 29 | # jq가 설치되어 있는 경우 30 | jq '.major = $major | .minor = $minor | .patch = $patch | .code = $code' \ 31 | --argjson major "${{ inputs.major }}" \ 32 | --argjson minor "${{ inputs.minor }}" \ 33 | --argjson patch "${{ inputs.patch }}" \ 34 | --argjson code "${{ inputs.code }}" \ 35 | $VERSION_FILE > tmp.$$.json && mv tmp.$$.json $VERSION_FILE 36 | else 37 | # jq가 설치되어 있지 않은 경우 38 | sed -i "s/\"major\": [0-9]\+/\"major\": ${{ inputs.major }}/" $VERSION_FILE 39 | sed -i "s/\"minor\": [0-9]\+/\"minor\": ${{ inputs.minor }}/" $VERSION_FILE 40 | sed -i "s/\"patch\": [0-9]\+/\"patch\": ${{ inputs.patch }}/" $VERSION_FILE 41 | sed -i "s/\"code\": [0-9]\+/\"code\": ${{ inputs.code }}/" $VERSION_FILE 42 | fi 43 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 44 | git config --local user.name "github-actions[bot]" 45 | git add $VERSION_FILE 46 | git commit -m "Update app version to ${{ inputs.major }}.${{ inputs.minor }}.${{ inputs.patch }} [${{ inputs.code }}]" 47 | git push -------------------------------------------------------------------------------- /.github/auto_assign_config.yml: -------------------------------------------------------------------------------- 1 | addAssignees: author 2 | addReviewers: true 3 | reviewers: 4 | - HyomK 5 | - lee-ji-hoon 6 | -------------------------------------------------------------------------------- /.github/workflows/deploy_firebase-distribution_debug.yml: -------------------------------------------------------------------------------- 1 | name: "[CD] Debug to Firebase Distribution" 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | deploy_debug: 10 | environment: Debug 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Development Environment 17 | uses: ./.github/actions/setup-development-environment 18 | with: 19 | google-services: ${{ secrets.GOOGLE_SERVICES }} 20 | test-mode: debug 21 | debug-properties: ${{ secrets.DEBUG_PROPERTIES }} 22 | 23 | - name: Setup Signed Key Environment 24 | uses: ./.github/actions/setup-key-environment 25 | with: 26 | key-properties: ${{ secrets.KEY_PROPERTIES }} 27 | key-file: ${{ secrets.SIGNED_KEY }} 28 | 29 | - name: Build Debug APK 30 | run: ./gradlew :app:assembleDebug 31 | 32 | - name: Check if APK exists 33 | run: ls -la app/build/outputs/apk/debug/ 34 | 35 | - name: Upload to Firebase App Distribution 36 | uses: wzieba/Firebase-Distribution-Github-Action@v1 37 | with: 38 | appId: ${{secrets.FIREBASE_APP_ID}} 39 | serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIALS }} 40 | groups: 뽀모냥 41 | file: app/build/outputs/apk/debug/app-debug.apk -------------------------------------------------------------------------------- /.github/workflows/deploy_firebase-distribution_release.yml: -------------------------------------------------------------------------------- 1 | name: "[CD] Release to Firebase Distribution" 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | deploy_release: 10 | environment: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Development Environment 17 | uses: ./.github/actions/setup-development-environment 18 | with: 19 | google-services: ${{ secrets.GOOGLE_SERVICES }} 20 | test-mode: release 21 | release-properties: ${{ secrets.RELEASE_PROPERTIES }} 22 | 23 | - name: Setup Signed Key Environment 24 | uses: ./.github/actions/setup-key-environment 25 | with: 26 | key-properties: ${{ secrets.KEY_PROPERTIES }} 27 | key-file: ${{ secrets.SIGNED_KEY }} 28 | 29 | - name: Build Release APK 30 | run: ./gradlew :app:assembleRelease 31 | 32 | - name: Check if APK exists 33 | run: ls -la app/build/outputs/apk/release/ 34 | 35 | - name: Upload to Firebase App Distribution 36 | uses: wzieba/Firebase-Distribution-Github-Action@v1 37 | with: 38 | appId: ${{secrets.FIREBASE_APP_ID}} 39 | serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIALS }} 40 | groups: 뽀모냥 41 | file: app/build/outputs/apk/release/app-release.apk -------------------------------------------------------------------------------- /.github/workflows/pr_auto_assign.yml: -------------------------------------------------------------------------------- 1 | name: 'PR Auto Assign' 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review] 5 | 6 | permissions: 7 | contents: read 8 | pull-requests: write 9 | 10 | jobs: 11 | add-reviewers-and-assignee: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: kentaro-m/auto-assign-action@v1.2.5 15 | with: 16 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 17 | configuration-path: '.github/auto_assign_config.yml' 18 | -------------------------------------------------------------------------------- /.github/workflows/pr_template.yml: -------------------------------------------------------------------------------- 1 | name: PR Template Selector 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | apply-template: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check out the repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '16' 19 | 20 | - name: Check Branch and Apply Template 21 | uses: actions/github-script@v6 22 | with: 23 | script: | 24 | const fs = require('fs'); 25 | const prBranch = context.payload.pull_request.head.ref; 26 | const baseBranch = context.payload.pull_request.base.ref; 27 | 28 | let templatePath = '.github/PULL_REQUEST_TEMPLATE/basic_template.md'; 29 | 30 | if (prBranch.startsWith('release/') || prBranch.startsWith('hotfix/')) { 31 | templatePath = '.github/PULL_REQUEST_TEMPLATE/release_template.md'; 32 | } else if (baseBranch === 'develop') { 33 | templatePath = '.github/PULL_REQUEST_TEMPLATE/basic_template.md'; 34 | } 35 | 36 | const template = fs.readFileSync(templatePath, 'utf8'); 37 | await github.rest.pulls.update({ 38 | ...context.repo, 39 | pull_number: context.payload.pull_request.number, 40 | body: context.payload.pull_request.body ? `${template}\n\n---\n\n${context.payload.pull_request.body}` : template 41 | }); 42 | -------------------------------------------------------------------------------- /.github/workflows/verify_debug_build_test.yml: -------------------------------------------------------------------------------- 1 | name: "[CI] verify debug build test" 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | push: 7 | branches: 8 | - main 9 | - develop 10 | 11 | jobs: 12 | build_test: 13 | environment: Debug 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | gradle_command: 18 | - :app:assembleDebug 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Development Environment 24 | uses: ./.github/actions/setup-development-environment 25 | with: 26 | google-services: ${{ secrets.GOOGLE_SERVICES }} 27 | test-mode: debug 28 | debug-properties: ${{ secrets.DEBUG_PROPERTIES }} 29 | 30 | - name: Setup Signed Key Environment 31 | uses: ./.github/actions/setup-key-environment 32 | with: 33 | key-properties: ${{ secrets.KEY_PROPERTIES }} 34 | key-file: ${{ secrets.SIGNED_KEY }} 35 | 36 | - name: build test 37 | run: ./gradlew ${{ matrix.gradle_command }} -------------------------------------------------------------------------------- /.github/workflows/verify_release_build_test.yml: -------------------------------------------------------------------------------- 1 | name: "[CI] verify release build test" 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build_test: 14 | environment: Release 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | gradle_command: 19 | - :app:assembleRelease 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Development Environment 25 | uses: ./.github/actions/setup-development-environment 26 | with: 27 | google-services: ${{ secrets.GOOGLE_SERVICES }} 28 | test-mode: release 29 | release-properties: ${{ secrets.RELEASE_PROPERTIES }} 30 | 31 | - name: Setup Signed Key Environment 32 | uses: ./.github/actions/setup-key-environment 33 | with: 34 | key-properties: ${{ secrets.KEY_PROPERTIES }} 35 | key-file: ${{ secrets.SIGNED_KEY }} 36 | 37 | - name: build test 38 | run: ./gradlew ${{ matrix.gradle_command }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .externalNativeBuild 8 | .cxx 9 | local.properties 10 | .idea/ 11 | /core/ui/build/ 12 | /library/network/build/ 13 | /.kotlin/ 14 | debug.secrets.properties 15 | release.secrets.properties 16 | /app/google-services.json 17 | key-release 18 | key.secrets.properties 19 | -------------------------------------------------------------------------------- /app-version.json: -------------------------------------------------------------------------------- 1 | { 2 | "major": 1, 3 | "minor": 0, 4 | "patch": 4, 5 | "code": 32 6 | } 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/debug/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFE9BF 4 | -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 모하냥 debug 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/pomonyang/mohanyang/MainElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang 2 | 3 | import com.pomonyang.mohanyang.presentation.base.NetworkViewState 4 | import com.pomonyang.mohanyang.presentation.base.ViewEvent 5 | import com.pomonyang.mohanyang.presentation.base.ViewSideEffect 6 | 7 | data class MainState( 8 | override val isLoading: Boolean = true, 9 | override val isInternalError: Boolean = false, 10 | override val isInvalidError: Boolean = false, 11 | override val lastRequestAction: MainEvent? = null, 12 | ) : NetworkViewState() 13 | 14 | sealed interface MainEvent : ViewEvent { 15 | data object Init : MainEvent 16 | data object ClickRefresh : MainEvent 17 | data object ClickClose : MainEvent 18 | data object ClickRetry : MainEvent 19 | } 20 | 21 | sealed interface MainEffect : ViewSideEffect { 22 | data object ShowDialog : MainEffect 23 | data object DismissDialog : MainEffect 24 | data object GoToOnBoarding : MainEffect 25 | data object GoToTimer : MainEffect 26 | data object ExitApp : MainEffect 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/pomonyang/mohanyang/di/ManagerModule.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.di 2 | 3 | import android.app.AlarmManager 4 | import android.content.Context 5 | import com.pomonyang.mohanyang.notification.MnAlarmManager 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | internal object ManagerModule { 15 | @Provides 16 | internal fun provideMnAlarmManager( 17 | @ApplicationContext context: Context, 18 | ): MnAlarmManager { 19 | val alarmManager = 20 | context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 21 | return MnAlarmManager(context, alarmManager) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/pomonyang/mohanyang/di/ServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.di 2 | 3 | import android.app.NotificationManager 4 | import android.content.Context 5 | import androidx.core.app.NotificationCompat 6 | import com.pomonyang.mohanyang.R 7 | import com.pomonyang.mohanyang.presentation.di.PomodoroNotification 8 | import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.POMODORO_NOTIFICATION_CHANNEL_ID 9 | import com.pomonyang.mohanyang.presentation.screen.PomodoroConstants.POMODORO_NOTIFICATION_ID 10 | import com.pomonyang.mohanyang.ui.ServiceHelper 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.android.components.ServiceComponent 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.android.scopes.ServiceScoped 17 | 18 | @Module 19 | @InstallIn(ServiceComponent::class) 20 | internal object ServiceModule { 21 | 22 | @Provides 23 | @PomodoroNotification 24 | @ServiceScoped 25 | fun provideNotificationBuilder( 26 | @ApplicationContext context: Context, 27 | ): NotificationCompat.Builder = NotificationCompat.Builder(context, POMODORO_NOTIFICATION_CHANNEL_ID) 28 | .setContentTitle(context.getString(R.string.app_name)) 29 | .setSmallIcon(R.drawable.ic_app_notification) 30 | .setContentIntent(ServiceHelper.clickPendingIntent(context, POMODORO_NOTIFICATION_ID)) 31 | 32 | @Provides 33 | @ServiceScoped 34 | fun provideNotificationManager( 35 | @ApplicationContext context: Context, 36 | ): NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/pomonyang/mohanyang/ui/BottomNavItem.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.ui 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | data class BottomNavItem( 6 | val route: Any, 7 | @DrawableRes val iconRes: Int, 8 | @DrawableRes val selectedIconRes: Int, 9 | val label: String, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/pomonyang/mohanyang/ui/ServiceHelper.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.ui 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | object ServiceHelper { 8 | 9 | private const val FLAG = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 10 | 11 | fun clickPendingIntent( 12 | context: Context, 13 | requestCode: Int, 14 | ): PendingIntent { 15 | val notificationIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { 16 | flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP 17 | } 18 | return PendingIntent.getActivity( 19 | context, 20 | requestCode, 21 | notificationIntent, 22 | FLAG, 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/pomonyang/mohanyang/util/DebugTimberTree.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.util 2 | 3 | import timber.log.Timber 4 | 5 | internal class DebugTimberTree : Timber.DebugTree() { 6 | override fun createStackElementTag(element: StackTraceElement): String = "${super.createStackElementTag(element)}" 7 | 8 | override fun log( 9 | priority: Int, 10 | tag: String?, 11 | message: String, 12 | t: Throwable?, 13 | ) { 14 | val element = 15 | Throwable().stackTrace.first { stackElement -> 16 | stackElement.className.run { 17 | startsWith("timber.log.").not() && contains("Timber").not() 18 | } 19 | } 20 | val prefix = "[${element.fileName}:${element.lineNumber}]" 21 | super.log(priority, tag, "$prefix$message", t) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #FFFFE9BF 11 | #FFFAF6F3 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFE9BF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 모하냥 3 | moha-nyang-channel-v2 4 | moha-nyang 5 | moha-nyang-pomodoro 6 | local_notification_channel_id 7 | local_notification_id 8 | local_notification_title 9 | local_notification_message 10 | 오프라인 모드 11 | 집중을 끝내고 돌아왔어요 12 | 너무 오랜 시간동안 대기화면에 머물러서 홈화면으로 이동되었어요. 13 | moha-nyang-channel 14 | pomodoro_notification_channel_id 15 | interrupt_notification_channel_id 16 | interrupt_notification_channel_name 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build-logic/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | dependencyResolutionManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | 9 | versionCatalogs { 10 | create("libs") { 11 | from(files("../gradle/libs.versions.toml")) 12 | } 13 | } 14 | } 15 | 16 | rootProject.name = "build-logic" 17 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AndroidApplicationComposeConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ApplicationExtension 2 | import com.pomonyang.mohanyang.convention.configureAndroidCompose 3 | import com.pomonyang.mohanyang.convention.findPluginId 4 | import com.pomonyang.mohanyang.convention.libs 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.getByType 8 | 9 | class AndroidApplicationComposeConventionPlugin : Plugin { 10 | override fun apply(target: Project) { 11 | with(target) { 12 | with(pluginManager) { 13 | apply(libs.findPluginId("android.application")) 14 | apply(libs.findPluginId("compose.compiler")) 15 | } 16 | 17 | val extension = extensions.getByType() 18 | configureAndroidCompose(extension) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AndroidApplicationConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ApplicationExtension 2 | import com.pomonyang.mohanyang.convention.ProjectConfigurations 3 | import com.pomonyang.mohanyang.convention.configureKotlinAndroid 4 | import com.pomonyang.mohanyang.convention.configureSecret 5 | import com.pomonyang.mohanyang.convention.findPluginId 6 | import com.pomonyang.mohanyang.convention.libs 7 | import org.gradle.api.Plugin 8 | import org.gradle.api.Project 9 | import org.gradle.kotlin.dsl.configure 10 | 11 | class AndroidApplicationConventionPlugin : Plugin { 12 | override fun apply(target: Project) { 13 | with(target) { 14 | with(pluginManager) { 15 | apply(libs.findPluginId("android.application")) 16 | apply(libs.findPluginId("kotlin.android")) 17 | apply(libs.findPluginId("kotlin.serialization")) 18 | apply(libs.findPluginId("gradle.secrets")) 19 | } 20 | 21 | extensions.configure { 22 | configureKotlinAndroid(this) 23 | configureSecret() 24 | defaultConfig.targetSdk = ProjectConfigurations.TARGET_SDK 25 | 26 | packaging { 27 | resources { 28 | excludes.add("/META-INF/{AL2.0,LGPL2.1}") 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AndroidApplicationFirebaseConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ApplicationExtension 2 | import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension 3 | import com.pomonyang.mohanyang.convention.findPluginId 4 | import com.pomonyang.mohanyang.convention.libs 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.configure 8 | import org.gradle.kotlin.dsl.dependencies 9 | 10 | class AndroidApplicationFirebaseConventionPlugin : Plugin { 11 | override fun apply(target: Project) { 12 | with(target) { 13 | with(pluginManager) { 14 | apply(libs.findPluginId("google.services")) 15 | apply(libs.findPluginId("firebase.pref")) 16 | apply(libs.findPluginId("firebase.crashlytics")) 17 | apply(libs.findPluginId("firebase.appdistribution")) 18 | } 19 | 20 | dependencies { 21 | add("implementation", libs.findBundle("firebase").get()) 22 | add("implementation", platform(libs.findLibrary("firebase-bom").get())) 23 | } 24 | 25 | extensions.configure { 26 | buildTypes.configureEach { 27 | if (name == "release") { 28 | configure { 29 | mappingFileUploadEnabled = true 30 | } 31 | } else { 32 | configure { 33 | mappingFileUploadEnabled = false 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AndroidDatadogConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.pomonyang.mohanyang.convention.findPluginId 2 | import com.pomonyang.mohanyang.convention.implementation 3 | import com.pomonyang.mohanyang.convention.libs 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.kotlin.dsl.dependencies 7 | 8 | class AndroidDatadogConventionPlugin : Plugin { 9 | override fun apply(target: Project) { 10 | with(target) { 11 | with(pluginManager) { 12 | apply(libs.findPluginId("datadog")) 13 | } 14 | 15 | dependencies { 16 | implementation(libs.findLibrary("datadog-rum")) 17 | implementation(libs.findLibrary("datadog-compose")) 18 | implementation(libs.findLibrary("datadog-okhttp")) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AndroidHiltConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.pomonyang.mohanyang.convention.findPluginId 2 | import com.pomonyang.mohanyang.convention.implementation 3 | import com.pomonyang.mohanyang.convention.ksp 4 | import com.pomonyang.mohanyang.convention.libs 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.dependencies 8 | 9 | class AndroidHiltConventionPlugin : Plugin { 10 | override fun apply(target: Project) { 11 | with(target) { 12 | with(pluginManager) { 13 | apply(libs.findPluginId("hilt")) 14 | apply(libs.findPluginId("ksp")) 15 | } 16 | 17 | dependencies { 18 | implementation(libs.findLibrary("dagger-hilt-android")) 19 | ksp(libs.findLibrary("dagger-hilt-android-compiler")) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AndroidLibraryComposeConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.LibraryExtension 2 | import com.pomonyang.mohanyang.convention.configureAndroidCompose 3 | import com.pomonyang.mohanyang.convention.findPluginId 4 | import com.pomonyang.mohanyang.convention.libs 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.configure 8 | 9 | class AndroidLibraryComposeConventionPlugin : Plugin { 10 | override fun apply(target: Project) { 11 | with(target) { 12 | with(pluginManager) { 13 | apply(libs.findPluginId("compose.compiler")) 14 | apply(libs.findPluginId("android.library")) 15 | } 16 | 17 | extensions.configure { 18 | configureAndroidCompose(this) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AndroidLibraryConventionPlugin.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.LibraryExtension 2 | import com.pomonyang.mohanyang.convention.configureKotlinAndroid 3 | import com.pomonyang.mohanyang.convention.configureSecret 4 | import com.pomonyang.mohanyang.convention.findPluginId 5 | import com.pomonyang.mohanyang.convention.libs 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.kotlin.dsl.configure 9 | 10 | class AndroidLibraryConventionPlugin : Plugin { 11 | override fun apply(target: Project) { 12 | with(target) { 13 | with(pluginManager) { 14 | apply(libs.findPluginId("android.library")) 15 | apply(libs.findPluginId("kotlin.android")) 16 | apply(libs.findPluginId("kotlin.serialization")) 17 | apply(libs.findPluginId("gradle.secrets")) 18 | } 19 | 20 | extensions.configure { 21 | configureKotlinAndroid(this) 22 | configureSecret() 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/AppVersionPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang 2 | 3 | import groovy.json.JsonSlurper 4 | import java.io.File 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | 8 | open class AppVersionExtension(val name: String, val code: Int) 9 | 10 | data class AppVersion( 11 | val major: Int, 12 | val minor: Int, 13 | val patch: Int, 14 | val code: Int, 15 | ) { 16 | fun getName() = "$major.$minor.$patch" 17 | fun getVersionCode() = code 18 | } 19 | 20 | class AppVersionPlugin : Plugin { 21 | override fun apply(project: Project) { 22 | val versionFile = getAppVersionFile(project) 23 | val appVersion = getAppVersion(versionFile) 24 | setupAppVersionExtension(project, appVersion) 25 | } 26 | 27 | private fun getAppVersionFile(project: Project): File { 28 | val file = File(project.rootDir, "app-version.json") 29 | if (!file.exists()) { 30 | throw IllegalStateException("app-version.json file not found") 31 | } 32 | return file 33 | } 34 | 35 | private fun getAppVersion(versionFile: File): AppVersion { 36 | val appVersionJson = JsonSlurper().parseText(versionFile.readText()) as Map<*, *> 37 | return AppVersion( 38 | (appVersionJson["major"] as Number).toInt(), 39 | (appVersionJson["minor"] as Number).toInt(), 40 | (appVersionJson["patch"] as Number).toInt(), 41 | (appVersionJson["code"] as Number).toInt(), 42 | ) 43 | } 44 | 45 | private fun setupAppVersionExtension(project: Project, appVersion: AppVersion) { 46 | project.extensions.create( 47 | "appVersion", 48 | AppVersionExtension::class.java, 49 | appVersion.getName(), 50 | appVersion.getVersionCode(), 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/convention/DependencyHandlerExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.convention 2 | 3 | import java.util.Optional 4 | import org.gradle.api.artifacts.Dependency 5 | import org.gradle.api.artifacts.dsl.DependencyHandler 6 | import org.gradle.api.provider.Provider 7 | 8 | internal fun DependencyHandler.implementation(dependencyNotation: Optional>): Dependency? = add("implementation", dependencyNotation.get()) 9 | 10 | internal fun DependencyHandler.debugImplementation(dependencyNotation: Optional>): Dependency? = add("debugImplementation", dependencyNotation.get()) 11 | 12 | internal fun DependencyHandler.ksp(dependencyNotation: Optional>): Dependency? = add("ksp", dependencyNotation.get()) 13 | 14 | internal fun DependencyHandler.compileOnly(dependencyNotation: Optional>): Dependency? = add("compileOnly", dependencyNotation.get()) 15 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/convention/GithubUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.convention 2 | 3 | import java.io.File 4 | import java.util.concurrent.TimeUnit 5 | 6 | object GithubUtils { 7 | fun commitHash(): String = runCommand("git rev-parse --short=8 HEAD") 8 | 9 | fun lastCommitMessage(): String = runCommand("git show -s --format=%B") 10 | 11 | private fun runCommand( 12 | command: String, 13 | workingDir: File = File("."), 14 | ): String = try { 15 | val parts = command.split("\\s".toRegex()) 16 | val process = 17 | ProcessBuilder(*parts.toTypedArray()) 18 | .directory(workingDir) 19 | .redirectOutput(ProcessBuilder.Redirect.PIPE) 20 | .redirectError(ProcessBuilder.Redirect.PIPE) 21 | .start() 22 | 23 | process.waitFor(60, TimeUnit.MINUTES) 24 | process.inputStream 25 | .bufferedReader() 26 | .readText() 27 | .trim() 28 | } catch (e: Exception) { 29 | e.printStackTrace() 30 | "Command failed" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/convention/KotlinAndroid.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.convention 2 | 3 | import com.android.build.api.dsl.CommonExtension 4 | import com.pomonyang.mohanyang.convention.ProjectConfigurations.JAVA_VERSION 5 | import org.gradle.api.Project 6 | import org.gradle.kotlin.dsl.dependencies 7 | import org.gradle.kotlin.dsl.provideDelegate 8 | 9 | internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension<*, *, *, *, *, *>) { 10 | commonExtension.apply { 11 | compileSdk = ProjectConfigurations.COMPILE_SDK 12 | 13 | defaultConfig { 14 | minSdk = ProjectConfigurations.MIN_SDK 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | vectorDrawables.useSupportLibrary = true 17 | resourceConfigurations.addAll(listOf("en", "ko")) 18 | } 19 | 20 | compileOptions { 21 | sourceCompatibility = JAVA_VERSION 22 | targetCompatibility = JAVA_VERSION 23 | } 24 | 25 | buildFeatures { 26 | buildConfig = true 27 | } 28 | 29 | kotlinOptions { 30 | val warningsAsErrors: String? by project 31 | allWarningsAsErrors = warningsAsErrors.toBoolean() 32 | 33 | freeCompilerArgs = freeCompilerArgs + 34 | listOf( 35 | "-opt-in=kotlin.RequiresOptIn", 36 | "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", 37 | "-opt-in=kotlinx.coroutines.FlowPreview", 38 | ) 39 | 40 | jvmTarget = JAVA_VERSION.toString() 41 | } 42 | 43 | dependencies { 44 | add("implementation", libs.findLibrary("timber").get()) 45 | add("implementation", libs.findLibrary("kotlin.serialization.json").get()) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/convention/ProjectConfigurations.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.convention 2 | 3 | import org.gradle.api.JavaVersion 4 | 5 | object ProjectConfigurations { 6 | const val COMPILE_SDK = 34 7 | const val MIN_SDK = 26 8 | const val TARGET_SDK = 34 9 | val JAVA_VERSION = JavaVersion.VERSION_21 10 | } 11 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/convention/ProjectExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.convention 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.VersionCatalog 5 | import org.gradle.api.artifacts.VersionCatalogsExtension 6 | import org.gradle.kotlin.dsl.getByType 7 | 8 | val Project.libs: VersionCatalog 9 | get() = extensions.getByType().named("libs") 10 | 11 | internal fun VersionCatalog.findPluginId(alias: String): String = findPlugin(alias).get().get().pluginId 12 | -------------------------------------------------------------------------------- /build-logic/src/main/java/com/pomonyang/mohanyang/convention/Secrets.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.convention 2 | 3 | import com.google.android.libraries.mapsplatform.secrets_gradle_plugin.SecretsPluginExtension 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.configure 6 | 7 | // https://developers.google.com/maps/documentation/android-sdk/secrets-gradle-plugin?hl=ko 8 | internal fun Project.configureSecret() { 9 | val releasePropertiesName = "release.secrets.properties" 10 | val debugPropertiedName = "debug.secrets.properties" 11 | extensions.configure { 12 | val isDebug = project.gradle.startParameter.taskNames.any { it.contains("Debug", ignoreCase = true) } 13 | propertiesFileName = if (isDebug) debugPropertiedName else releasePropertiesName 14 | ignoreList.add("keyToIgnore") 15 | ignoreList.add("sdk.*") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.kotlin.android) apply false 4 | alias(libs.plugins.kotlin.jvm) apply false 5 | alias(libs.plugins.hilt) apply false 6 | alias(libs.plugins.android.library) apply false 7 | alias(libs.plugins.ksp) apply false 8 | alias(libs.plugins.compose.compiler) apply false 9 | alias(libs.plugins.kotlin.serialization) apply false 10 | alias(libs.plugins.gradle.secrets) apply false 11 | alias(libs.plugins.google.services) apply false 12 | alias(libs.plugins.firebase.pref) apply false 13 | alias(libs.plugins.firebase.crashlytics) apply false 14 | alias(libs.plugins.firebase.appdistribution) apply false 15 | alias(libs.plugins.datadog) apply false 16 | } 17 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("mohanyang.android.library") 3 | id("mohanyang.android.hilt") 4 | } 5 | 6 | android { 7 | namespace = "com.mohanyang.data" 8 | defaultConfig { 9 | consumerProguardFiles("consumer-rules.pro") 10 | } 11 | } 12 | 13 | dependencies { 14 | implementation(libs.kotlin.serialization.json) 15 | implementation(libs.kotlin.serialization.converter) 16 | implementation(libs.retrofit) 17 | implementation(libs.okhttp) 18 | implementation(libs.okhttp.logging) 19 | implementation(libs.kotlin.coroutine.core) 20 | implementation(libs.androidx.datastore) 21 | implementation(libs.androidx.room.runtime) 22 | implementation(libs.androidx.room.ktx) 23 | implementation(libs.datadog.okhttp) 24 | ksp(libs.androidx.room.compiler) 25 | } 26 | -------------------------------------------------------------------------------- /data/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | ## coroutine 2 | -keep class kotlinx.coroutines.android.** {*;} 3 | -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory { *; } 4 | -keepnames class kotlinx.coroutines.CoroutineExceptionHandler { *; } 5 | -keepclassmembernames class kotlinx.** { 6 | volatile ; 7 | } 8 | -dontwarn kotlinx.coroutines.** 9 | 10 | ## Kotlin 11 | -keepattributes *Annotation* 12 | -keep class kotlin.** { *; } 13 | -keep class org.jetbrains.** { *; } -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/datasource/deviceid/DeviceIdLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.datastore.datasource.deviceid 2 | 3 | interface DeviceIdLocalDataSource { 4 | suspend fun getDeviceId(): String 5 | } 6 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/datasource/deviceid/DeviceIdLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.datastore.datasource.deviceid 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.provider.Settings 6 | import androidx.datastore.core.DataStore 7 | import androidx.datastore.core.IOException 8 | import androidx.datastore.preferences.core.Preferences 9 | import androidx.datastore.preferences.core.emptyPreferences 10 | import androidx.datastore.preferences.core.stringPreferencesKey 11 | import com.pomonyang.mohanyang.data.local.datastore.di.DeviceIdDataStore 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import java.util.* 14 | import javax.inject.Inject 15 | import kotlinx.coroutines.flow.catch 16 | import kotlinx.coroutines.flow.first 17 | 18 | internal class DeviceIdLocalDataSourceImpl @Inject constructor( 19 | @DeviceIdDataStore private val dataStore: DataStore, 20 | @ApplicationContext private val context: Context, 21 | ) : DeviceIdLocalDataSource { 22 | 23 | override suspend fun getDeviceId(): String = getStoredDeviceId() ?: getSSAID() ?: getUUID() 24 | 25 | private suspend fun getStoredDeviceId(): String? = dataStore.data 26 | .catch { exception -> 27 | if (exception is IOException) { 28 | emit(emptyPreferences()) 29 | } else { 30 | throw exception 31 | } 32 | }.first()[DEVICE_ID_KEY] 33 | 34 | @SuppressLint("HardwareIds") 35 | private fun getSSAID(): String? = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) 36 | 37 | private fun getUUID(): String = UUID.randomUUID().toString() 38 | 39 | companion object { 40 | const val DEVICE_ID_PREFERENCES_NAME = "device_id_preferences" 41 | private val DEVICE_ID_KEY = stringPreferencesKey("device_id") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/datasource/notification/NotificationLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.datastore.datasource.notification 2 | 3 | interface NotificationLocalDataSource { 4 | suspend fun saveInterruptNotification(isEnabled: Boolean) 5 | suspend fun saveTimerNotification(isEnabled: Boolean) 6 | suspend fun saveLockScreenNotification(isEnabled: Boolean) 7 | suspend fun isInterruptNotificationEnabled(): Boolean 8 | suspend fun isTimerNotification(): Boolean 9 | suspend fun isLockScreenNotification(): Boolean 10 | } 11 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/datasource/token/TokenLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.datastore.datasource.token 2 | 3 | interface TokenLocalDataSource { 4 | suspend fun saveAccessToken(accessToken: String) 5 | suspend fun saveRefreshToken(refreshToken: String) 6 | suspend fun saveFcmToken(fcmToken: String) 7 | suspend fun getAccessToken(): String 8 | suspend fun getRefreshToken(): String 9 | suspend fun getFcmToken(): String 10 | suspend fun clear() 11 | } 12 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/datasource/user/UserLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.datastore.datasource.user 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.UserInfoResponse 4 | 5 | internal interface UserLocalDataSource { 6 | suspend fun saveUserInfo(userInfo: UserInfoResponse) 7 | suspend fun getUserInfo(): UserInfoResponse 8 | } 9 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/di/DataStoreQualifier.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.datastore.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | @Retention(AnnotationRetention.BINARY) 7 | annotation class TokenDataStore 8 | 9 | @Qualifier 10 | @Retention(AnnotationRetention.BINARY) 11 | annotation class DeviceIdDataStore 12 | 13 | @Qualifier 14 | @Retention(AnnotationRetention.BINARY) 15 | annotation class PomodoroDataStore 16 | 17 | @Qualifier 18 | @Retention(AnnotationRetention.BINARY) 19 | annotation class UserDataStore 20 | 21 | @Qualifier 22 | @Retention(AnnotationRetention.BINARY) 23 | annotation class NotificationDataStore 24 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/di/LocalDataSourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.datastore.di 2 | 3 | import com.pomonyang.mohanyang.data.local.datastore.datasource.deviceid.DeviceIdLocalDataSource 4 | import com.pomonyang.mohanyang.data.local.datastore.datasource.deviceid.DeviceIdLocalDataSourceImpl 5 | import com.pomonyang.mohanyang.data.local.datastore.datasource.notification.NotificationLocalDataSource 6 | import com.pomonyang.mohanyang.data.local.datastore.datasource.notification.NotificationLocalDataSourceImpl 7 | import com.pomonyang.mohanyang.data.local.datastore.datasource.token.TokenLocalDataSource 8 | import com.pomonyang.mohanyang.data.local.datastore.datasource.token.TokenLocalDataSourceImpl 9 | import com.pomonyang.mohanyang.data.local.datastore.datasource.user.UserLocalDataSource 10 | import com.pomonyang.mohanyang.data.local.datastore.datasource.user.UserLocalDataSourceImpl 11 | import dagger.Binds 12 | import dagger.Module 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.components.SingletonComponent 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | internal abstract class LocalDataSourceModule { 20 | @Binds 21 | @Singleton 22 | abstract fun provideTokenDataSource(tokenDataSourceImpl: TokenLocalDataSourceImpl): TokenLocalDataSource 23 | 24 | @Binds 25 | @Singleton 26 | abstract fun provideDeviceIdDataSource(deviceIdDataSourceImpl: DeviceIdLocalDataSourceImpl): DeviceIdLocalDataSource 27 | 28 | @Binds 29 | @Singleton 30 | abstract fun provideUserDataSource(userLocalDataSourceImpl: UserLocalDataSourceImpl): UserLocalDataSource 31 | 32 | @Binds 33 | @Singleton 34 | abstract fun provideNotificationDataSource(notificationDataSourceImpl: NotificationLocalDataSourceImpl): NotificationLocalDataSource 35 | } 36 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/model/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/model/.gitkeep -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/util/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/data/src/main/java/com/pomonyang/mohanyang/data/local/datastore/util/.gitkeep -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/device/receiver/LockScreenBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.device.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | class LockScreenBroadcastReceiver( 8 | private var lockStateListener: (Boolean) -> Unit, 9 | ) : BroadcastReceiver() { 10 | 11 | override fun onReceive(context: Context?, intent: Intent?) { 12 | if (intent == null) return 13 | when (intent.action) { 14 | Intent.ACTION_SCREEN_ON -> lockStateListener(false) 15 | Intent.ACTION_SCREEN_OFF -> lockStateListener(true) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/device/util/LockScreenUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.device.util 2 | 3 | import android.content.Context 4 | import android.content.Context.RECEIVER_EXPORTED 5 | import android.content.Context.RECEIVER_NOT_EXPORTED 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.os.Build 9 | import com.pomonyang.mohanyang.data.local.device.receiver.LockScreenBroadcastReceiver 10 | import kotlinx.coroutines.channels.awaitClose 11 | import kotlinx.coroutines.flow.callbackFlow 12 | import kotlinx.coroutines.flow.distinctUntilChanged 13 | 14 | fun Context.lockScreenState() = callbackFlow { 15 | val lockScreenReceiver = LockScreenBroadcastReceiver { isLocked -> 16 | trySend(isLocked) 17 | } 18 | 19 | val screenEventIntentFilter = IntentFilter().apply { 20 | addAction(Intent.ACTION_SCREEN_ON) 21 | addAction(Intent.ACTION_SCREEN_OFF) 22 | } 23 | 24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 25 | applicationContext.registerReceiver( 26 | lockScreenReceiver, 27 | screenEventIntentFilter, 28 | RECEIVER_NOT_EXPORTED, 29 | ) 30 | } else { 31 | applicationContext.registerReceiver( 32 | lockScreenReceiver, 33 | screenEventIntentFilter, 34 | RECEIVER_EXPORTED, 35 | ) 36 | } 37 | 38 | awaitClose { 39 | applicationContext.unregisterReceiver(lockScreenReceiver) 40 | } 41 | }.distinctUntilChanged() 42 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/room/database/PomodoroSettingDataBase.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.room.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.pomonyang.mohanyang.data.local.room.dao.PomodoroSettingDao 6 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroSettingEntity 7 | 8 | @Database(entities = [PomodoroSettingEntity::class], version = 2, exportSchema = false) 9 | internal abstract class PomodoroSettingDataBase : RoomDatabase() { 10 | abstract fun dao(): PomodoroSettingDao 11 | } 12 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/room/database/PomodoroTimerDataBase.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.room.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.pomonyang.mohanyang.data.local.room.dao.PomodoroTimerDao 6 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroTimerEntity 7 | 8 | @Database(entities = [PomodoroTimerEntity::class], version = 1, exportSchema = false) 9 | internal abstract class PomodoroTimerDataBase : RoomDatabase() { 10 | abstract fun dao(): PomodoroTimerDao 11 | } 12 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/room/di/RoomModule.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.room.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.pomonyang.mohanyang.data.local.room.dao.PomodoroSettingDao 6 | import com.pomonyang.mohanyang.data.local.room.dao.PomodoroTimerDao 7 | import com.pomonyang.mohanyang.data.local.room.database.PomodoroSettingDataBase 8 | import com.pomonyang.mohanyang.data.local.room.database.PomodoroTimerDataBase 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | internal object RoomModule { 19 | @Provides 20 | @Singleton 21 | fun providePomodoroSettingDataBase( 22 | @ApplicationContext context: Context, 23 | ): PomodoroSettingDataBase = Room.databaseBuilder( 24 | context = context, 25 | klass = PomodoroSettingDataBase::class.java, 26 | name = "pomodoro-category-database", // database name string 관리 필요 27 | ) 28 | .fallbackToDestructiveMigration() 29 | .build() 30 | 31 | @Provides 32 | @Singleton 33 | fun providePomodoroTimerDataBase( 34 | @ApplicationContext context: Context, 35 | ): PomodoroTimerDataBase = Room 36 | .databaseBuilder( 37 | context = context, 38 | klass = PomodoroTimerDataBase::class.java, 39 | name = "pomodoro-timer-database", // database name string 관리 필요 40 | ) 41 | .fallbackToDestructiveMigration() 42 | .build() 43 | 44 | @Provides 45 | @Singleton 46 | fun providePomodoroSettingDao(database: PomodoroSettingDataBase): PomodoroSettingDao = database.dao() 47 | 48 | @Provides 49 | @Singleton 50 | fun providePomodoroTimerDao(database: PomodoroTimerDataBase): PomodoroTimerDao = database.dao() 51 | } 52 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/room/enitity/PomodoroSettingEntity.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.room.enitity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "pomodoro_setting") 7 | data class PomodoroSettingEntity( 8 | @PrimaryKey 9 | val categoryNo: Int, 10 | val title: String, 11 | val focusTime: String, 12 | val restTime: String, 13 | val iconType: String = "", 14 | val isSelected: Boolean = false, 15 | ) 16 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/room/enitity/PomodoroTimerEntity.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.room.enitity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.pomonyang.mohanyang.data.local.room.util.formatDurationToMinutesString 6 | import com.pomonyang.mohanyang.data.remote.model.request.PomodoroTimerRequest 7 | import com.pomonyang.mohanyang.data.repository.util.getCurrentIsoInstant 8 | 9 | @Entity(tableName = "pomodoro_timer") 10 | data class PomodoroTimerEntity( 11 | @PrimaryKey 12 | val focusTimeId: String, 13 | val focusedTime: Int = 0, 14 | val restedTime: Int = 0, 15 | val doneAt: String = "", 16 | val categoryNo: Int, 17 | ) 18 | 19 | fun PomodoroTimerEntity.toRequestModel() = PomodoroTimerRequest( 20 | clientFocusTimeId = focusTimeId, 21 | categoryNo = categoryNo, 22 | focusedTime = focusedTime.formatDurationToMinutesString(), 23 | restedTime = restedTime.formatDurationToMinutesString(), 24 | doneAt = doneAt.ifEmpty { getCurrentIsoInstant() }, 25 | ) 26 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/local/room/util/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.local.room.util 2 | 3 | import java.time.Duration 4 | 5 | fun Int.formatDurationToMinutesString(): String = Duration.ofMinutes((this / 60).toLong()).toString() 6 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/datasource/auth/AuthRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.datasource.auth 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.TokenResponse 4 | 5 | interface AuthRemoteDataSource { 6 | suspend fun login(deviceId: String): Result 7 | suspend fun logout() // TODO 8 | suspend fun refreshAccessToken(refreshToken: String): Result 9 | } 10 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/datasource/auth/AuthRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.datasource.auth 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.request.RefreshTokenRequest 4 | import com.pomonyang.mohanyang.data.remote.model.request.TokenRequest 5 | import com.pomonyang.mohanyang.data.remote.service.AuthService 6 | import javax.inject.Inject 7 | 8 | internal class AuthRemoteDataSourceImpl @Inject constructor( 9 | private val authService: AuthService, 10 | ) : AuthRemoteDataSource { 11 | 12 | override suspend fun login(deviceId: String) = authService.getTokenByDeviceId(TokenRequest(deviceId)) 13 | 14 | override suspend fun logout() { 15 | } 16 | 17 | override suspend fun refreshAccessToken(refreshToken: String) = authService.refreshToken(RefreshTokenRequest(refreshToken)) 18 | } 19 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/datasource/pomodoro/PomodoroSettingRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.datasource.pomodoro 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.PomodoroSettingResponse 4 | 5 | interface PomodoroSettingRemoteDataSource { 6 | suspend fun getPomodoroSettingList(): Result> 7 | suspend fun modifyCategorySettingOption( 8 | categoryNo: Int, 9 | focusTime: String? = null, 10 | restTime: String? = null, 11 | iconType: String? = null, 12 | title: String? = null, 13 | ): Result 14 | suspend fun addPomodoroCategory( 15 | title: String, 16 | iconType: String, 17 | ): Result 18 | 19 | suspend fun deleteCategories(categoryNumbers: List): Result 20 | 21 | suspend fun updateSelectPomodoroCategory(categoryNo: Int): Result 22 | } 23 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/di/ClientQualifier.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | @Retention(AnnotationRetention.BINARY) 7 | annotation class TokenClient 8 | 9 | @Qualifier 10 | @Retention(AnnotationRetention.BINARY) 11 | annotation class DefaultClient 12 | 13 | @Qualifier 14 | @Retention(AnnotationRetention.BINARY) 15 | annotation class TokenApi 16 | 17 | @Qualifier 18 | @Retention(AnnotationRetention.BINARY) 19 | annotation class DefaultApi 20 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/di/RemoteDataSourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.di 2 | 3 | import com.pomonyang.mohanyang.data.remote.datasource.auth.AuthRemoteDataSource 4 | import com.pomonyang.mohanyang.data.remote.datasource.auth.AuthRemoteDataSourceImpl 5 | import com.pomonyang.mohanyang.data.remote.datasource.pomodoro.PomodoroSettingRemoteDataSource 6 | import com.pomonyang.mohanyang.data.remote.datasource.pomodoro.PomodoroSettingRemoteDataSourceImpl 7 | import dagger.Binds 8 | import dagger.Module 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | internal abstract class RemoteDataSourceModule { 16 | @Binds 17 | @Singleton 18 | abstract fun provideAuthDataSource(authDataSourceImpl: AuthRemoteDataSourceImpl): AuthRemoteDataSource 19 | 20 | @Binds 21 | @Singleton 22 | abstract fun providePomodoroSettingDataSource( 23 | pomodoroSettingRemoteDataSourceImpl: PomodoroSettingRemoteDataSourceImpl, 24 | ): PomodoroSettingRemoteDataSource 25 | } 26 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/AddCategoryRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class AddCategoryRequest( 7 | val title: String, 8 | val iconType: String, 9 | ) 10 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/DeleteCategoryRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class DeleteCategoryRequest( 7 | val no: List, 8 | ) 9 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/PomodoroTimerRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class PomodoroTimerRequest( 7 | val clientFocusTimeId: String, 8 | val categoryNo: Int, 9 | val focusedTime: String, 10 | val restedTime: String, 11 | val doneAt: String, 12 | ) 13 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/RefreshTokenRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RefreshTokenRequest(val refreshToken: String) 7 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/RegisterPushTokenRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RegisterPushTokenRequest( 7 | val deviceToken: String, 8 | val deviceType: String, 9 | ) 10 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/TokenRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class TokenRequest( 7 | val deviceId: String, 8 | ) 9 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/UpdateCatInfoRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class UpdateCatInfoRequest( 7 | val name: String, 8 | ) 9 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/UpdateCatTypeRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class UpdateCatTypeRequest( 7 | val catNo: Int, 8 | ) 9 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/request/UpdateCategoryInfoRequest.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class UpdateCategoryInfoRequest( 7 | val title: String? = null, 8 | val iconType: String? = null, 9 | val focusTime: String? = null, 10 | val restTime: String? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/response/CatTypeResponse.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class CatTypeResponse( 7 | val no: Int = -1, 8 | val name: String = "", 9 | val type: String = "", 10 | ) 11 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/response/ErrorResponse.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.response 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ErrorResponse( 8 | @SerialName("type") val type: String, 9 | @SerialName("message") val message: String, 10 | @SerialName("errorTraceId") val errorTraceId: String, 11 | ) 12 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/response/PomodoroSettingResponse.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.response 2 | 3 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroSettingEntity 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PomodoroSettingResponse( 8 | val no: Int = -1, 9 | val title: String = "", 10 | val focusTime: String = "", 11 | val restTime: String = "", 12 | val iconType: String = "", 13 | val isSelected: Boolean = false, 14 | ) 15 | 16 | internal fun PomodoroSettingResponse.toEntity() = PomodoroSettingEntity( 17 | categoryNo = no, 18 | title = title, 19 | focusTime = focusTime, 20 | restTime = restTime, 21 | iconType = iconType, 22 | isSelected = isSelected, 23 | ) 24 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/response/TokenResponse.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class TokenResponse( 7 | val accessToken: String = "", 8 | val accessTokenExpiredAt: String = "", 9 | val refreshToken: String = "", 10 | val refreshTokenExpiredAt: String = "", 11 | ) 12 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/model/response/UserInfoResponse.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.model.response 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class UserInfoResponse( 7 | val registeredDeviceNo: Int = -1, 8 | val isPushEnabled: Boolean = false, 9 | val cat: CatTypeResponse = CatTypeResponse(), 10 | ) { 11 | fun isNewUser(): Boolean { 12 | /* 서버에서 fetch 한 기본 고양이 id가 -1 인 경우 신규 유저로 판단 */ 13 | return this.cat.no == NEW_USER_VALIDATION_ID 14 | } 15 | 16 | companion object { 17 | const val NEW_USER_VALIDATION_ID = -1 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/service/AuthService.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.service 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.request.RefreshTokenRequest 4 | import com.pomonyang.mohanyang.data.remote.model.request.TokenRequest 5 | import com.pomonyang.mohanyang.data.remote.model.response.TokenResponse 6 | import retrofit2.http.Body 7 | import retrofit2.http.POST 8 | 9 | interface AuthService { 10 | @POST("/papi/v1/tokens/refresh") 11 | suspend fun refreshToken( 12 | @Body request: RefreshTokenRequest, 13 | ): Result 14 | 15 | @POST("/papi/v1/tokens") 16 | suspend fun getTokenByDeviceId( 17 | @Body request: TokenRequest, 18 | ): Result 19 | } 20 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/util/NetworkException.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.util 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.ErrorResponse 4 | 5 | class InternalException(errorResponse: ErrorResponse) : Exception(errorResponse.message) 6 | class BadRequestException(errorResponse: ErrorResponse) : Exception(errorResponse.message) 7 | class ForbiddenException(errorResponse: ErrorResponse) : Exception(errorResponse.message) 8 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/util/NetworkResultCallAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.util 2 | 3 | import java.lang.reflect.Type 4 | import retrofit2.Call 5 | import retrofit2.CallAdapter 6 | 7 | internal class NetworkResultCallAdapter( 8 | private val responseType: Type, 9 | ) : CallAdapter>> { 10 | override fun responseType(): Type = responseType 11 | 12 | override fun adapt(call: Call): Call> = NetworkResultCall(call) 13 | } 14 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/remote/util/NetworkResultCallAdapterFactory.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.remote.util 2 | 3 | import java.lang.reflect.ParameterizedType 4 | import java.lang.reflect.Type 5 | import retrofit2.Call 6 | import retrofit2.CallAdapter 7 | import retrofit2.Retrofit 8 | 9 | internal class NetworkResultCallAdapterFactory : CallAdapter.Factory() { 10 | override fun get( 11 | returnType: Type, 12 | annotations: Array, 13 | retrofit: Retrofit, 14 | ): CallAdapter<*, *>? { 15 | if (getRawType(returnType) != Call::class.java) { 16 | return null 17 | } 18 | 19 | val wrapperType = getParameterUpperBound(0, returnType as ParameterizedType) 20 | if (getRawType(wrapperType) != Result::class.java) { 21 | return null 22 | } 23 | 24 | val resultType = getParameterUpperBound(0, wrapperType as ParameterizedType) 25 | return NetworkResultCallAdapter(resultType) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/cat/CatSettingRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.cat 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.CatTypeResponse 4 | 5 | interface CatSettingRepository { 6 | suspend fun getCatTypes(): Result> 7 | suspend fun updateCatInfo(name: String): Result 8 | suspend fun updateCatType(catNo: Int): Result 9 | } 10 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/cat/CatSettingRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.cat 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.request.UpdateCatInfoRequest 4 | import com.pomonyang.mohanyang.data.remote.model.request.UpdateCatTypeRequest 5 | import com.pomonyang.mohanyang.data.remote.service.MohaNyangService 6 | import javax.inject.Inject 7 | 8 | class CatSettingRepositoryImpl @Inject constructor( 9 | private val mohaNyangService: MohaNyangService, 10 | ) : CatSettingRepository { 11 | override suspend fun getCatTypes() = mohaNyangService.getCatTypes() 12 | 13 | override suspend fun updateCatInfo(name: String) = mohaNyangService.updateCatInfo(UpdateCatInfoRequest(name)) 14 | 15 | override suspend fun updateCatType(catNo: Int) = mohaNyangService.updateCatType(UpdateCatTypeRequest(catNo)) 16 | } 17 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/pomodoro/PomodoroSettingRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.pomodoro 2 | 3 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroSettingEntity 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface PomodoroSettingRepository { 7 | 8 | fun getPomodoroSettingList(): Flow> 9 | 10 | fun getSelectedPomodoroSetting(): Flow 11 | 12 | suspend fun fetchPomodoroSettingList() 13 | 14 | suspend fun updatePomodoroCategorySetting( 15 | categoryNo: Int, 16 | title: String? = null, 17 | iconType: String? = null, 18 | focusTime: Int? = null, 19 | restTime: Int? = null, 20 | ): Result 21 | 22 | suspend fun addPomodoroCategory( 23 | title: String, 24 | iconType: String, 25 | ): Result 26 | 27 | suspend fun updateRecentUseCategoryNo(categoryNo: Int) 28 | 29 | suspend fun deleteCategories(categoryNumbers: List): Result 30 | } 31 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/pomodoro/PomodoroTimerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.pomodoro 2 | 3 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroTimerEntity 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface PomodoroTimerRepository { 7 | suspend fun insertPomodoroTimerInitData(categoryNo: Int, pomodoroTimerId: String) 8 | suspend fun incrementFocusedTime(pomodoroTimerId: String) 9 | suspend fun incrementRestedTime(pomodoroTimerId: String) 10 | suspend fun updatePomodoroDone(pomodoroTimerId: String) 11 | suspend fun updateRecentPomodoroDone() 12 | suspend fun savePomodoroData(pomodoroTimerId: String) 13 | suspend fun savePomodoroCacheData() 14 | fun getPomodoroTimer(timerId: String): Flow 15 | } 16 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/push/PushAlarmRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.push 2 | 3 | interface PushAlarmRepository { 4 | suspend fun saveFcmToken(fcmToken: String) 5 | suspend fun getFcmToken(): String 6 | suspend fun registerPushToken(fcmToken: String): Result 7 | suspend fun unRegisterPushToken(): Result 8 | suspend fun subscribeNotification(): Result 9 | suspend fun unSubscribeNotification(): Result 10 | suspend fun setInterruptNotification(isEnabled: Boolean) 11 | suspend fun isInterruptNotificationEnabled(): Boolean 12 | suspend fun setTimerNotification(isEnabled: Boolean) 13 | suspend fun isTimerNotificationEnabled(): Boolean 14 | suspend fun isLockScreenNotificationEnabled(): Boolean 15 | suspend fun setLockScreenNotification(isEnabled: Boolean) 16 | } 17 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/statistics/StatisticsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.statistics 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.StatisticsResponse 4 | import java.time.LocalDate 5 | 6 | interface StatisticsRepository { 7 | suspend fun getStatistics(date: LocalDate): Result 8 | } 9 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/statistics/StatisticsRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.statistics 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.StatisticsResponse 4 | import com.pomonyang.mohanyang.data.remote.service.MohaNyangService 5 | import java.time.LocalDate 6 | import java.time.format.DateTimeFormatter 7 | import javax.inject.Inject 8 | 9 | internal class StatisticsRepositoryImpl @Inject constructor( 10 | private val mohaNyangService: MohaNyangService, 11 | ) : StatisticsRepository { 12 | 13 | override suspend fun getStatistics(date: LocalDate): Result = runCatching { 14 | mohaNyangService.getStatistics(date.format(DateTimeFormatter.ISO_DATE)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/user/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.user 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.TokenResponse 4 | import com.pomonyang.mohanyang.data.remote.model.response.UserInfoResponse 5 | 6 | interface UserRepository { 7 | suspend fun getDeviceId(): String 8 | fun isNewUser(): Boolean 9 | suspend fun login(deviceId: String): Result 10 | suspend fun saveToken(accessToken: String, refreshToken: String) 11 | suspend fun fetchMyInfo(): Result 12 | suspend fun getMyInfo(): UserInfoResponse 13 | } 14 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/user/UserRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.user 2 | 3 | import com.pomonyang.mohanyang.data.local.datastore.datasource.deviceid.DeviceIdLocalDataSource 4 | import com.pomonyang.mohanyang.data.local.datastore.datasource.token.TokenLocalDataSource 5 | import com.pomonyang.mohanyang.data.local.datastore.datasource.user.UserLocalDataSource 6 | import com.pomonyang.mohanyang.data.remote.model.request.TokenRequest 7 | import com.pomonyang.mohanyang.data.remote.model.response.UserInfoResponse 8 | import com.pomonyang.mohanyang.data.remote.service.AuthService 9 | import com.pomonyang.mohanyang.data.remote.service.MohaNyangService 10 | import javax.inject.Inject 11 | import kotlinx.coroutines.runBlocking 12 | 13 | internal class UserRepositoryImpl @Inject constructor( 14 | private val deviceLocalDataStore: DeviceIdLocalDataSource, 15 | private val tokenLocalDataSource: TokenLocalDataSource, 16 | private val userLocalDataSource: UserLocalDataSource, 17 | private val mohaNyangService: MohaNyangService, 18 | private val authService: AuthService, 19 | ) : UserRepository { 20 | override suspend fun getDeviceId() = deviceLocalDataStore.getDeviceId() 21 | 22 | override fun isNewUser(): Boolean = runBlocking { tokenLocalDataSource.getAccessToken().isEmpty() || userLocalDataSource.getUserInfo().isNewUser() } 23 | 24 | override suspend fun login(deviceId: String) = authService.getTokenByDeviceId(TokenRequest(deviceId)) 25 | 26 | override suspend fun saveToken(accessToken: String, refreshToken: String) { 27 | tokenLocalDataSource.saveAccessToken(accessToken) 28 | tokenLocalDataSource.saveRefreshToken(refreshToken) 29 | } 30 | 31 | override suspend fun fetchMyInfo(): Result = mohaNyangService.getMyInfo().onSuccess { userLocalDataSource.saveUserInfo(it) } 32 | 33 | override suspend fun getMyInfo() = userLocalDataSource.getUserInfo() 34 | } 35 | -------------------------------------------------------------------------------- /data/src/main/java/com/pomonyang/mohanyang/data/repository/util/TimerUtil.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.data.repository.util 2 | 3 | import java.time.Instant 4 | import java.time.format.DateTimeFormatter 5 | 6 | internal fun getCurrentIsoInstant(): String = DateTimeFormatter.ISO_INSTANT.format(Instant.now()) 7 | -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("mohanyang.android.library") 3 | } 4 | 5 | android { 6 | namespace = "com.mohanyang.domain" 7 | } 8 | 9 | dependencies { 10 | implementation(projects.data) 11 | implementation(libs.kotlin.coroutine.core) 12 | implementation(libs.javax.inject) 13 | implementation(libs.androidx.compose.runtime) 14 | } 15 | -------------------------------------------------------------------------------- /domain/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/domain/consumer-rules.pro -------------------------------------------------------------------------------- /domain/src/main/java/com/pomonyang/mohanyang/domain/usecase/AdjustPomodoroTimeUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.domain.usecase 2 | 3 | import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroSettingRepository 4 | import java.time.Duration 5 | import javax.inject.Inject 6 | import kotlinx.coroutines.flow.first 7 | 8 | class AdjustPomodoroTimeUseCase @Inject constructor( 9 | private val pomodoroSettingRepository: PomodoroSettingRepository, 10 | private val getSelectedPomodoroSettingUseCase: GetSelectedPomodoroSettingUseCase, 11 | ) { 12 | 13 | suspend operator fun invoke(isFocusTime: Boolean, isIncrease: Boolean) { 14 | val selectedPomodoroSetting = getSelectedPomodoroSettingUseCase().first() 15 | val adjustment = if (isIncrease) ADJUST_TIME_UNIT else -ADJUST_TIME_UNIT 16 | val focusTime = Duration.parse(selectedPomodoroSetting.focusTime).toMinutes() 17 | val restTime = Duration.parse(selectedPomodoroSetting.restTime).toMinutes() 18 | 19 | val updatedFocusTime = if (isFocusTime) focusTime + adjustment else focusTime 20 | val updatedRestTime = if (!isFocusTime) restTime + adjustment else restTime 21 | 22 | pomodoroSettingRepository.updatePomodoroCategorySetting( 23 | categoryNo = selectedPomodoroSetting.categoryNo, 24 | focusTime = updatedFocusTime.toInt(), 25 | restTime = updatedRestTime.toInt(), 26 | ) 27 | } 28 | 29 | companion object { 30 | private const val ADJUST_TIME_UNIT = 5 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /domain/src/main/java/com/pomonyang/mohanyang/domain/usecase/GetSelectedPomodoroSettingUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.domain.usecase 2 | 3 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroSettingEntity 4 | import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroSettingRepository 5 | import javax.inject.Inject 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.filterNotNull 8 | 9 | class GetSelectedPomodoroSettingUseCase @Inject constructor( 10 | private val pomodoroSettingRepository: PomodoroSettingRepository, 11 | ) { 12 | 13 | operator fun invoke(): Flow { 14 | val settingListFlow: Flow = pomodoroSettingRepository.getSelectedPomodoroSetting().filterNotNull() 15 | return settingListFlow 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /domain/src/main/java/com/pomonyang/mohanyang/domain/usecase/GetTokenByDeviceIdUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.domain.usecase 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.TokenResponse 4 | import com.pomonyang.mohanyang.data.repository.user.UserRepository 5 | import javax.inject.Inject 6 | 7 | class GetTokenByDeviceIdUseCase @Inject constructor( 8 | private val userRepository: UserRepository, 9 | ) { 10 | suspend operator fun invoke(): Result { 11 | val deviceId = userRepository.getDeviceId() 12 | return userRepository.login(deviceId).onSuccess { 13 | userRepository.saveToken( 14 | accessToken = it.accessToken, 15 | refreshToken = it.refreshToken, 16 | ) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /domain/src/main/java/com/pomonyang/mohanyang/domain/usecase/InsertPomodoroInitialDataUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.domain.usecase 2 | 3 | import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroTimerRepository 4 | import javax.inject.Inject 5 | import kotlinx.coroutines.flow.first 6 | 7 | class InsertPomodoroInitialDataUseCase @Inject constructor( 8 | private val pomodoroTimerRepository: PomodoroTimerRepository, 9 | private val getSelectedPomodoroSettingUseCase: GetSelectedPomodoroSettingUseCase, 10 | ) { 11 | 12 | suspend operator fun invoke(focusTimeId: String) { 13 | val selectedPomodoroSetting = getSelectedPomodoroSettingUseCase().first().categoryNo 14 | pomodoroTimerRepository.insertPomodoroTimerInitData(selectedPomodoroSetting, focusTimeId) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 13 21:25:49 KST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /presentation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("mohanyang.android.library") 3 | id("mohanyang.android.hilt") 4 | id("mohanyang.android.library.compose") 5 | id("mohanyang.appversion") 6 | } 7 | 8 | android { 9 | namespace = "com.mohanyang.presentation" 10 | defaultConfig { 11 | buildConfigField("String", "APP_VERSION", "\"${appVersion.name}\"") 12 | } 13 | } 14 | 15 | dependencies { 16 | implementation(libs.material) 17 | implementation(libs.dagger.hilt.android) 18 | implementation(libs.bundles.androidx.compose.navigation) 19 | implementation(libs.permission) 20 | implementation(libs.rive) 21 | implementation(libs.lottie.compose) 22 | implementation(platform(libs.firebase.bom)) 23 | implementation(libs.firebase.analytics) 24 | 25 | // module impl 26 | implementation(projects.domain) 27 | implementation(projects.data) 28 | } 29 | -------------------------------------------------------------------------------- /presentation/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/consumer-rules.pro -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/base/BaseViewElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.base 2 | 3 | interface ViewState 4 | 5 | interface ViewEvent 6 | 7 | interface ViewSideEffect 8 | 9 | open class NetworkViewState( 10 | open val isLoading: Boolean = false, 11 | open val isInternalError: Boolean = false, 12 | open val isInvalidError: Boolean = false, 13 | open val lastRequestAction: ViewEvent? = null, 14 | ) : ViewState 15 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.base 2 | 3 | import androidx.annotation.CallSuper 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import kotlinx.coroutines.channels.Channel 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.flow.receiveAsFlow 11 | import kotlinx.coroutines.flow.update 12 | import kotlinx.coroutines.launch 13 | 14 | abstract class BaseViewModel : ViewModel() { 15 | // UI의 초기 상태를 설정 16 | abstract fun setInitialState(): STATE 17 | 18 | abstract fun handleEvent(event: EVENT) 19 | 20 | // 초기 상태를 지연 초기화 21 | private val initialState: STATE by lazy { setInitialState() } 22 | 23 | private val _state = MutableStateFlow(initialState) 24 | val state = _state.asStateFlow() 25 | 26 | private val _effects: Channel = Channel(Channel.BUFFERED) 27 | val effects: Flow = _effects.receiveAsFlow() 28 | 29 | protected fun updateState(reducer: STATE.() -> STATE) { 30 | _state.update { it.reducer() } 31 | } 32 | 33 | // Intent -> Model -> View 의 사이클을 벗어난 비동기 작업이 완료된 후 UI 상태 변경 외의 작업을 수행할 때 사용 34 | protected fun setEffect(vararg builder: EFFECT) { 35 | viewModelScope.launch { 36 | for (effectValue in builder) { 37 | _effects.send(effectValue) 38 | } 39 | } 40 | } 41 | 42 | // viewmodel이 destroy 될 때 추가적인 작업이 필요하다면 43 | protected open fun onDestroy() {} 44 | 45 | @CallSuper 46 | override fun onCleared() { 47 | super.onCleared() 48 | onDestroy() 49 | _effects.close() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/component/TimerType.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.component 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import com.mohanyang.presentation.R 12 | import com.pomonyang.mohanyang.presentation.designsystem.icon.MnSmallIcon 13 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnSpacing 14 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 15 | 16 | @Composable 17 | fun TimerType( 18 | modifier: Modifier = Modifier, 19 | type: String, 20 | iconRes: Int = R.drawable.ic_null, 21 | ) { 22 | Row( 23 | modifier = modifier.padding(top = MnSpacing.xLarge), 24 | verticalAlignment = Alignment.CenterVertically, 25 | ) { 26 | MnSmallIcon(resourceId = iconRes, tint = Color.Unspecified) 27 | Text( 28 | modifier = Modifier.padding(MnSpacing.xSmall), 29 | text = type, 30 | style = MnTheme.typography.header5, 31 | color = MnTheme.textColorScheme.secondary, 32 | ) 33 | } 34 | } 35 | 36 | @Preview 37 | @Composable 38 | private fun TimerTypePreview() { 39 | MnTheme { 40 | TimerType(type = "집중시간") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/bottomsheet/MnBottomSheetDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.bottomsheet 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.text.TextStyle 7 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnColor 8 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 9 | 10 | object MnBottomSheetDefaults { 11 | @Composable 12 | fun colors( 13 | containerColor: Color = MnColor.White, 14 | titleColor: Color = MnTheme.textColorScheme.primary, 15 | subTitleColor: Color = MnTheme.textColorScheme.secondary, 16 | ) = MnBottomSheetColors( 17 | containerColor = containerColor, 18 | titleColor = titleColor, 19 | subTitleColor = subTitleColor, 20 | ) 21 | 22 | @Composable 23 | fun textStyles( 24 | titleTextStyle: TextStyle = MnTheme.typography.header3, 25 | subTitleTextStyle: TextStyle = MnTheme.typography.bodyRegular, 26 | ) = MnBottomSheetTextStyles( 27 | titleTextStyle = titleTextStyle, 28 | subTitleTextStyle = subTitleTextStyle, 29 | ) 30 | } 31 | 32 | @Immutable 33 | data class MnBottomSheetColors( 34 | val containerColor: Color, 35 | val titleColor: Color, 36 | val subTitleColor: Color, 37 | ) 38 | 39 | @Immutable 40 | data class MnBottomSheetTextStyles( 41 | val titleTextStyle: TextStyle, 42 | val subTitleTextStyle: TextStyle, 43 | ) 44 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/button/box/MnBoxButtonColorType.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.button.box 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 5 | 6 | object MnBoxButtonColorType { 7 | val primary: MnBoxButtonColors 8 | @Composable 9 | get() = MnBoxButtonColors( 10 | containerColor = MnTheme.backgroundColorScheme.accent1, 11 | contentColor = MnTheme.textColorScheme.inverse, 12 | iconColor = MnTheme.iconColorScheme.inverse, 13 | disabledContainerColor = MnTheme.backgroundColorScheme.secondary, 14 | disabledContentColor = MnTheme.textColorScheme.disabled, 15 | disabledIconColor = MnTheme.iconColorScheme.disabled, 16 | ) 17 | 18 | val secondary: MnBoxButtonColors 19 | @Composable 20 | get() = MnBoxButtonColors( 21 | containerColor = MnTheme.backgroundColorScheme.inverse, 22 | contentColor = MnTheme.textColorScheme.inverse, 23 | iconColor = MnTheme.iconColorScheme.inverse, 24 | disabledContainerColor = MnTheme.backgroundColorScheme.inverse, 25 | disabledContentColor = MnTheme.textColorScheme.inverse, 26 | disabledIconColor = MnTheme.iconColorScheme.inverse, 27 | ) 28 | 29 | val tertiary: MnBoxButtonColors 30 | @Composable 31 | get() = MnBoxButtonColors( 32 | containerColor = MnTheme.backgroundColorScheme.secondary, 33 | contentColor = MnTheme.textColorScheme.tertiary, 34 | iconColor = MnTheme.iconColorScheme.tertiary, 35 | disabledContainerColor = MnTheme.backgroundColorScheme.secondary, 36 | disabledContentColor = MnTheme.textColorScheme.tertiary, 37 | disabledIconColor = MnTheme.iconColorScheme.tertiary, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/button/box/MnBoxButtonColors.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.button.box 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | @Immutable 7 | data class MnBoxButtonColors( 8 | val containerColor: Color, 9 | val contentColor: Color, 10 | val iconColor: Color, 11 | val disabledContainerColor: Color, 12 | val disabledContentColor: Color, 13 | val disabledIconColor: Color, 14 | ) 15 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/button/round/MnRoundButtonColorType.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.button.round 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 5 | 6 | object MnRoundButtonColorType { 7 | val primary: MnRoundButtonColors 8 | @Composable 9 | get() = MnRoundButtonColors( 10 | containerColor = MnTheme.backgroundColorScheme.accent1, 11 | iconColor = MnTheme.iconColorScheme.inverse, 12 | ) 13 | val secondary: MnRoundButtonColors 14 | @Composable 15 | get() = MnRoundButtonColors( 16 | containerColor = MnTheme.backgroundColorScheme.inverse, 17 | iconColor = MnTheme.iconColorScheme.tertiary, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/button/round/MnRoundButtonColors.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.button.round 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | @Immutable 7 | data class MnRoundButtonColors( 8 | val containerColor: Color, 9 | val iconColor: Color, 10 | ) 11 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/button/select/MnSelectListDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.button.select 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.ui.graphics.Color 6 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 7 | 8 | @Immutable 9 | data class MnSelectListColors( 10 | val enabledTextColor: Color, 11 | val disabledTextColor: Color, 12 | val enabledIconTint: Color, 13 | val disabledIconTint: Color, 14 | ) 15 | 16 | object MnSelectListDefaults { 17 | 18 | @Composable 19 | fun colors( 20 | enabledTextColor: Color = MnTheme.textColorScheme.primary, 21 | disabledTextColor: Color = MnTheme.textColorScheme.disabled, 22 | enabledIconTint: Color = Color.Unspecified, 23 | disabledIconTint: Color = MnTheme.textColorScheme.disabled, 24 | ) = MnSelectListColors( 25 | enabledTextColor = enabledTextColor, 26 | disabledTextColor = disabledTextColor, 27 | enabledIconTint = enabledIconTint, 28 | disabledIconTint = disabledIconTint, 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/button/toggle/MnToggleButtonSize.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.button.toggle 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object MnToggleButtonSize { 6 | val width = 48.dp 7 | val height = 28.dp 8 | val thumbSize = 22.dp 9 | val padding = (height - thumbSize) / 2 10 | } 11 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/icon/MnLargeIcon.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.icon 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.LocalContentColor 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.res.painterResource 12 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnIconSize 13 | 14 | @Composable 15 | fun MnLargeIcon( 16 | @DrawableRes resourceId: Int, 17 | modifier: Modifier = Modifier, 18 | contentDescription: String? = null, 19 | tint: Color = LocalContentColor.current, 20 | ) { 21 | Icon( 22 | painter = painterResource(id = resourceId), 23 | contentDescription = contentDescription, 24 | modifier = modifier.size(MnIconSize.large), 25 | tint = tint, 26 | ) 27 | } 28 | 29 | @Composable 30 | fun MnLargeIcon( 31 | imageVector: ImageVector, 32 | modifier: Modifier = Modifier, 33 | contentDescription: String? = null, 34 | tint: Color = LocalContentColor.current, 35 | ) { 36 | Icon( 37 | imageVector = imageVector, 38 | contentDescription = contentDescription, 39 | modifier = modifier.size(MnIconSize.large), 40 | tint = tint, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/icon/MnMediumIcon.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.icon 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.LocalContentColor 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.res.painterResource 12 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnIconSize 13 | 14 | @Composable 15 | fun MnMediumIcon( 16 | @DrawableRes resourceId: Int, 17 | modifier: Modifier = Modifier, 18 | contentDescription: String? = null, 19 | tint: Color = LocalContentColor.current, 20 | ) { 21 | Icon( 22 | painter = painterResource(id = resourceId), 23 | contentDescription = contentDescription, 24 | modifier = modifier.size(MnIconSize.medium), 25 | tint = tint, 26 | ) 27 | } 28 | 29 | @Composable 30 | fun MnMediumIcon( 31 | imageVector: ImageVector, 32 | modifier: Modifier = Modifier, 33 | contentDescription: String? = null, 34 | tint: Color = LocalContentColor.current, 35 | ) { 36 | Icon( 37 | imageVector = imageVector, 38 | contentDescription = contentDescription, 39 | modifier = modifier.size(MnIconSize.medium), 40 | tint = tint, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/icon/MnSmallIcon.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.icon 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.LocalContentColor 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.res.painterResource 12 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnIconSize 13 | 14 | @Composable 15 | fun MnSmallIcon( 16 | @DrawableRes resourceId: Int, 17 | modifier: Modifier = Modifier, 18 | contentDescription: String? = null, 19 | tint: Color = LocalContentColor.current, 20 | ) { 21 | Icon( 22 | painter = painterResource(id = resourceId), 23 | contentDescription = contentDescription, 24 | modifier = modifier.size(MnIconSize.small), 25 | tint = tint, 26 | ) 27 | } 28 | 29 | @Composable 30 | fun MnSmallIcon( 31 | imageVector: ImageVector, 32 | modifier: Modifier = Modifier, 33 | contentDescription: String? = null, 34 | tint: Color = LocalContentColor.current, 35 | ) { 36 | Icon( 37 | imageVector = imageVector, 38 | contentDescription = contentDescription, 39 | modifier = modifier.size(MnIconSize.small), 40 | tint = tint, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/icon/MnXLargeIcon.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.icon 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.LocalContentColor 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.res.painterResource 12 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnIconSize 13 | 14 | @Composable 15 | fun MnXLargeIcon( 16 | @DrawableRes resourceId: Int, 17 | modifier: Modifier = Modifier, 18 | contentDescription: String? = null, 19 | tint: Color = LocalContentColor.current, 20 | ) { 21 | Icon( 22 | painter = painterResource(id = resourceId), 23 | contentDescription = contentDescription, 24 | modifier = modifier.size(MnIconSize.xLarge), 25 | tint = tint, 26 | ) 27 | } 28 | 29 | @Composable 30 | fun MnXLargeIcon( 31 | imageVector: ImageVector, 32 | modifier: Modifier = Modifier, 33 | contentDescription: String? = null, 34 | tint: Color = LocalContentColor.current, 35 | ) { 36 | Icon( 37 | imageVector = imageVector, 38 | contentDescription = contentDescription, 39 | modifier = modifier.size(MnIconSize.xLarge), 40 | tint = tint, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/icon/MnXSmallIcon.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.icon 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.LocalContentColor 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | import androidx.compose.ui.res.painterResource 12 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnIconSize 13 | 14 | @Composable 15 | fun MnXSmallIcon( 16 | @DrawableRes resourceId: Int, 17 | modifier: Modifier = Modifier, 18 | contentDescription: String? = null, 19 | tint: Color = LocalContentColor.current, 20 | ) { 21 | Icon( 22 | painter = painterResource(id = resourceId), 23 | contentDescription = contentDescription, 24 | modifier = modifier.size(MnIconSize.xSmall), 25 | tint = tint, 26 | ) 27 | } 28 | 29 | @Composable 30 | fun MnXSmallIcon( 31 | imageVector: ImageVector, 32 | modifier: Modifier = Modifier, 33 | contentDescription: String? = null, 34 | tint: Color = LocalContentColor.current, 35 | ) { 36 | Icon( 37 | imageVector = imageVector, 38 | contentDescription = contentDescription, 39 | modifier = modifier.size(MnIconSize.xSmall), 40 | tint = tint, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/picker/MnWheelPickerDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.picker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.text.TextStyle 7 | import androidx.compose.ui.unit.Dp 8 | import androidx.compose.ui.unit.dp 9 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 10 | 11 | object MnWheelPickerDefaults { 12 | val itemHeight: Dp = 98.dp 13 | 14 | @Composable 15 | fun colors( 16 | fadeColor: Color = MnTheme.backgroundColorScheme.inverse, 17 | selectedTextColor: Color = MnTheme.textColorScheme.primary, 18 | unSelectedTextColor: Color = MnTheme.textColorScheme.disabled, 19 | ) = MnWheelPickerColor( 20 | fadeColor = fadeColor, 21 | selectedTextColor = selectedTextColor, 22 | unSelectedTextColor = unSelectedTextColor, 23 | ) 24 | 25 | @Composable 26 | fun styles() = MnWheelPickerStyles( 27 | selectedTextStyle = MnTheme.typography.header1, 28 | unSelectedTextStyle = MnTheme.typography.header2, 29 | ) 30 | } 31 | 32 | @Stable 33 | data class MnWheelPickerColor( 34 | val fadeColor: Color, 35 | val selectedTextColor: Color, 36 | val unSelectedTextColor: Color, 37 | ) 38 | 39 | @Stable 40 | data class MnWheelPickerStyles( 41 | val selectedTextStyle: TextStyle, 42 | val unSelectedTextStyle: TextStyle, 43 | ) 44 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/spinner/MnSpinner.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.spinner 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.unit.dp 13 | import com.airbnb.lottie.compose.LottieAnimation 14 | import com.airbnb.lottie.compose.LottieCompositionSpec 15 | import com.airbnb.lottie.compose.LottieConstants 16 | import com.airbnb.lottie.compose.animateLottieCompositionAsState 17 | import com.airbnb.lottie.compose.rememberLottieComposition 18 | import com.mohanyang.presentation.R 19 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnRadius 20 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 21 | 22 | @Composable 23 | fun MnSpinner( 24 | modifier: Modifier = Modifier, 25 | ) { 26 | Box( 27 | modifier = modifier 28 | .background(MnTheme.backgroundColorScheme.inverse, RoundedCornerShape(MnRadius.small)) 29 | .size(82.dp) 30 | .alpha(0.9f), 31 | contentAlignment = Alignment.Center, 32 | ) { 33 | val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.spinner)) 34 | val progress by animateLottieCompositionAsState( 35 | composition, 36 | iterations = LottieConstants.IterateForever, 37 | ) 38 | LottieAnimation( 39 | composition = composition, 40 | progress = { progress }, 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/token/MnIconSize.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.token 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import androidx.compose.ui.unit.dp 9 | import com.mohanyang.presentation.R 10 | import com.pomonyang.mohanyang.presentation.designsystem.icon.MnLargeIcon 11 | import com.pomonyang.mohanyang.presentation.designsystem.icon.MnMediumIcon 12 | import com.pomonyang.mohanyang.presentation.designsystem.icon.MnSmallIcon 13 | import com.pomonyang.mohanyang.presentation.designsystem.icon.MnXLargeIcon 14 | import com.pomonyang.mohanyang.presentation.designsystem.icon.MnXSmallIcon 15 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 16 | import com.pomonyang.mohanyang.presentation.util.ThemePreviews 17 | 18 | object MnIconSize { 19 | val xSmall = 16.dp 20 | val small = 20.dp 21 | val medium = 24.dp 22 | val large = 32.dp 23 | val xLarge = 48.dp 24 | } 25 | 26 | @ThemePreviews 27 | @Composable 28 | @Preview 29 | private fun MohaNyangIconPreview() { 30 | MnTheme { 31 | Column( 32 | verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top), 33 | ) { 34 | MnXSmallIcon(resourceId = R.drawable.ic_null) 35 | MnSmallIcon(resourceId = R.drawable.ic_null) 36 | MnMediumIcon(resourceId = R.drawable.ic_null) 37 | MnLargeIcon(resourceId = R.drawable.ic_null) 38 | MnXLargeIcon(resourceId = R.drawable.ic_null) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/token/MnInteraction.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.token 2 | 3 | object MnInteraction { 4 | val default = MnColor.White.copy(alpha = 0f) 5 | val hover = MnColor.White.copy(alpha = 0.1f) 6 | val pressed = MnColor.Black.copy(alpha = 0.05f) 7 | } 8 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/token/MnRadius.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.token 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object MnRadius { 6 | val twoXSmall = 4.dp 7 | val threeXSmall = 8.dp 8 | val xSmall = 12.dp 9 | val small = 16.dp 10 | val medium = 20.dp 11 | val large = 24.dp 12 | val max = 500.dp 13 | } 14 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/token/MnSpacing.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.token 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object MnSpacing { 6 | val twoXSmall = 2.dp 7 | val xSmall = 4.dp 8 | val small = 8.dp 9 | val medium = 12.dp 10 | val large = 16.dp 11 | val xLarge = 20.dp 12 | val twoXLarge = 24.dp 13 | val threeXLarge = 32.dp 14 | } 15 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/token/MnStroke.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.token 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object MnStroke { 6 | val small = 0.5.dp 7 | val medium = 1.dp 8 | val large = 2.dp 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/tooltip/MnTooltipDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.tooltip 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.unit.dp 7 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnColor 8 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 9 | 10 | object MnTooltipDefaults { 11 | val anchorWidth = 14.dp 12 | val anchorHeight = 9.dp 13 | val overlayBackgroundColor = MnColor.Black.copy(alpha = 0.5f) 14 | 15 | @Composable 16 | fun lightTooltipColors( 17 | containerColor: Color = MnColor.White, 18 | contentColor: Color = MnTheme.textColorScheme.secondary, 19 | ) = MnTooltipColors( 20 | containerColor = containerColor, 21 | contentColor = contentColor, 22 | ) 23 | 24 | @Composable 25 | fun darkTooltipColors( 26 | containerColor: Color = MnTheme.backgroundColorScheme.inverse, 27 | contentColor: Color = MnTheme.textColorScheme.inverse, 28 | ) = MnTooltipColors( 29 | containerColor = containerColor, 30 | contentColor = contentColor, 31 | ) 32 | } 33 | 34 | @Immutable 35 | data class MnTooltipColors( 36 | val containerColor: Color, 37 | val contentColor: Color, 38 | ) 39 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/designsystem/topappbar/MnTopAppBarDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.designsystem.topappbar 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.unit.dp 7 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnColor 8 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 9 | 10 | object MnTopAppBarDefaults { 11 | val height = 56.dp 12 | val iconHorizontalPadding = 8.dp 13 | 14 | @Composable 15 | fun topAppBarColors( 16 | containerColor: Color = MnColor.Gray50, 17 | navigationIconContentColor: Color = MnTheme.iconColorScheme.primary, 18 | titleContentColor: Color = MnTheme.textColorScheme.primary, 19 | actionIconContentColor: Color = MnTheme.iconColorScheme.primary, 20 | ) = MnAppBarColors( 21 | containerColor = containerColor, 22 | navigationIconContentColor = navigationIconContentColor, 23 | titleContentColor = titleContentColor, 24 | actionIconContentColor = actionIconContentColor, 25 | ) 26 | } 27 | 28 | @Immutable 29 | data class MnAppBarColors( 30 | val containerColor: Color, 31 | val navigationIconContentColor: Color, 32 | val titleContentColor: Color, 33 | val actionIconContentColor: Color, 34 | ) 35 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/di/MohanyangLoggerModule.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.di 2 | 3 | import com.pomonyang.mohanyang.presentation.util.MohanyangEventLogger 4 | import com.pomonyang.mohanyang.presentation.util.MohanyangEventLoggerImpl 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | internal abstract class MohanyangLoggerModule { 14 | 15 | @Binds 16 | @Singleton 17 | abstract fun provideMohanyangEventLogger( 18 | mohanyangEventLoggerImpl: MohanyangEventLoggerImpl, 19 | ): MohanyangEventLogger 20 | } 21 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/di/PomodoroModule.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.di 2 | 3 | import com.pomonyang.mohanyang.presentation.service.PomodoroTimer 4 | import com.pomonyang.mohanyang.presentation.service.focus.FocusTimer 5 | import com.pomonyang.mohanyang.presentation.service.rest.RestTimer 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.components.ServiceComponent 10 | 11 | @Module 12 | @InstallIn(ServiceComponent::class) 13 | internal object PomodoroModule { 14 | 15 | @Provides 16 | @FocusTimerType 17 | fun provideFocusTimer(): PomodoroTimer = FocusTimer() 18 | 19 | @Provides 20 | @RestTimerType 21 | fun provideRestTimer(): PomodoroTimer = RestTimer() 22 | } 23 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/di/Qualifier.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier 6 | @Retention(AnnotationRetention.BINARY) 7 | annotation class PomodoroNotification 8 | 9 | @Qualifier 10 | @Retention(AnnotationRetention.BINARY) 11 | annotation class FocusTimerType 12 | 13 | @Qualifier 14 | @Retention(AnnotationRetention.BINARY) 15 | annotation class RestTimerType 16 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/model/cat/CatInfoModel.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.model.cat 2 | 3 | import androidx.compose.runtime.Stable 4 | import com.pomonyang.mohanyang.data.remote.model.response.CatTypeResponse 5 | 6 | @Stable 7 | data class CatInfoModel( 8 | val no: Int, 9 | val name: String, 10 | val type: CatType, 11 | ) 12 | 13 | fun CatTypeResponse.toModel(): CatInfoModel = CatInfoModel( 14 | no = no, 15 | name = name, 16 | type = CatType.safeValueOf(type), 17 | ) 18 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/model/category/PomodoroCategoryModel.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.model.category 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroSettingEntity 5 | import com.pomonyang.mohanyang.presentation.screen.home.category.model.CategoryIcon 6 | 7 | @Immutable 8 | data class PomodoroCategoryModel( 9 | val categoryNo: Int, 10 | val title: String, 11 | val categoryIcon: CategoryIcon, 12 | ) 13 | 14 | fun PomodoroSettingEntity.toCategoryModel() = PomodoroCategoryModel( 15 | categoryNo = categoryNo, 16 | title = title, 17 | categoryIcon = CategoryIcon.safeValueOf(iconType), 18 | ) 19 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/model/setting/PomodoroSettingModel.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.model.setting 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.pomonyang.mohanyang.data.local.room.enitity.PomodoroSettingEntity 5 | import com.pomonyang.mohanyang.presentation.screen.home.category.model.CategoryIcon 6 | import java.time.Duration 7 | 8 | @Immutable 9 | data class PomodoroSettingModel( 10 | val categoryNo: Int, 11 | val title: String, 12 | val categoryIcon: CategoryIcon, 13 | val focusTime: Int, 14 | val restTime: Int, 15 | val isSelected: Boolean, 16 | ) 17 | 18 | fun PomodoroSettingEntity.toModel() = PomodoroSettingModel( 19 | categoryNo = categoryNo, 20 | title = title, 21 | categoryIcon = CategoryIcon.safeValueOf(iconType), 22 | focusTime = Duration.parse(focusTime).toMinutes().toInt(), 23 | restTime = Duration.parse(restTime).toMinutes().toInt(), 24 | isSelected = isSelected, 25 | ) 26 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/model/user/UserInfoModel.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.model.user 2 | 3 | import com.pomonyang.mohanyang.data.remote.model.response.UserInfoResponse 4 | import com.pomonyang.mohanyang.presentation.model.cat.CatInfoModel 5 | import com.pomonyang.mohanyang.presentation.model.cat.toModel 6 | 7 | data class UserInfoModel( 8 | val registeredDeviceNo: Int, 9 | val isPushEnabled: Boolean, 10 | val cat: CatInfoModel, 11 | ) 12 | 13 | fun UserInfoResponse.toModel() = UserInfoModel( 14 | registeredDeviceNo = this.registeredDeviceNo, 15 | isPushEnabled = this.isPushEnabled, 16 | cat = this.cat.toModel(), 17 | ) 18 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/PomodoroConstants.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen 2 | 3 | import com.mohanyang.presentation.BuildConfig 4 | 5 | object PomodoroConstants { 6 | val TIMER_DELAY = if (BuildConfig.DEBUG) 100L else 1_000L 7 | val MAX_EXCEEDED_TIME = if (BuildConfig.DEBUG) 60 else 3600 8 | const val MAX_FOCUS_MINUTES = 60 9 | const val MAX_REST_MINUTES = 30 10 | const val MIN_FOCUS_MINUTES = 10 11 | const val MIN_REST_MINUTES = 5 12 | const val ONE_SECOND = 1 13 | const val DEFAULT_TIME = "00:00" 14 | const val POMODORO_NOTIFICATION_CHANNEL_ID = "pomodoro_notification_channel_v2" 15 | const val POMODORO_NOTIFICATION_CHANNEL_NAME = "pomodoro_notification_channel_name" 16 | const val POMODORO_NOTIFICATION_ID = 1 17 | } 18 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/common/LoadingScreen.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.common 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.input.pointer.pointerInput 11 | import com.pomonyang.mohanyang.presentation.designsystem.spinner.MnSpinner 12 | 13 | @Composable 14 | fun LoadingScreen( 15 | modifier: Modifier = Modifier, 16 | ) { 17 | Box( 18 | modifier = modifier 19 | .fillMaxSize() 20 | .background(Color.Transparent) 21 | .pointerInput(Unit) {}, 22 | contentAlignment = Alignment.Center, 23 | ) { 24 | MnSpinner() 25 | } 26 | } 27 | 28 | @Composable 29 | fun LoadingContentContainer( 30 | isLoading: Boolean, 31 | content: @Composable () -> Unit, 32 | ) { 33 | Box { 34 | content() 35 | if (isLoading) { 36 | LoadingScreen() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/common/NetworkErrorDialog.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.common 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.platform.LocalContext 7 | import androidx.compose.ui.window.DialogProperties 8 | import com.mohanyang.presentation.R 9 | import com.pomonyang.mohanyang.presentation.designsystem.button.box.MnBoxButton 10 | import com.pomonyang.mohanyang.presentation.designsystem.button.box.MnBoxButtonColorType 11 | import com.pomonyang.mohanyang.presentation.designsystem.button.box.MnBoxButtonStyles 12 | import com.pomonyang.mohanyang.presentation.designsystem.dialog.MnDialog 13 | 14 | @Composable 15 | fun NetworkErrorDialog( 16 | onClickRefresh: () -> Unit, 17 | onDismissRequest: () -> Unit, 18 | modifier: Modifier = Modifier, 19 | ) = with(LocalContext.current) { 20 | MnDialog( 21 | modifier = modifier, 22 | properties = DialogProperties( 23 | dismissOnClickOutside = false, 24 | dismissOnBackPress = true, 25 | ), 26 | title = getString(R.string.network_error_title), 27 | subTitle = getString(R.string.network_error_content), 28 | positiveButton = { 29 | MnBoxButton( 30 | modifier = Modifier.fillMaxWidth(), 31 | text = getString(R.string.network_refresh), 32 | onClick = onClickRefresh, 33 | colors = MnBoxButtonColorType.primary, 34 | styles = MnBoxButtonStyles.medium, 35 | 36 | ) 37 | }, 38 | onDismissRequest = onDismissRequest, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/home/category/CategoryNameVerifier.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.home.category 2 | 3 | import com.pomonyang.mohanyang.presentation.model.category.PomodoroCategoryModel 4 | import com.pomonyang.mohanyang.presentation.screen.onboarding.naming.ValidationResult 5 | import kotlinx.collections.immutable.ImmutableList 6 | 7 | object CategoryNameVerifier { 8 | 9 | private const val NAME_MAX_LENGTH = 10 10 | private const val NAME_MIN_LENGTH = 1 11 | 12 | fun validateCategoryName(name: String, categoryList: ImmutableList): ValidationResult = when { 13 | categoryList.any { it.title == name } -> ValidationResult(false, "이미 존재하는 카테고리예요.") 14 | else -> ValidationResult( 15 | isValid = name.length in NAME_MIN_LENGTH..NAME_MAX_LENGTH, 16 | message = if (name.length <= NAME_MAX_LENGTH) "" else "최대 ${NAME_MAX_LENGTH}자리까지 입력할 수 있어요.", 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/home/category/component/CategoryBottomSheetHeaderContents.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.home.category.component 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import com.mohanyang.presentation.R 9 | import com.pomonyang.mohanyang.presentation.designsystem.button.icon.MnIconButton 10 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 11 | 12 | @Composable 13 | fun CategoryBottomSheetHeaderContents( 14 | isVisibleAddButton: Boolean, 15 | onEditClick: () -> Unit, 16 | onMoreMenuClick: () -> Unit, 17 | modifier: Modifier = Modifier, 18 | ) { 19 | Row( 20 | modifier = modifier, 21 | horizontalArrangement = Arrangement.SpaceBetween, 22 | ) { 23 | if (isVisibleAddButton) { 24 | MnIconButton( 25 | onClick = onEditClick, 26 | iconResourceId = R.drawable.ic_plus, 27 | ) 28 | } 29 | MnIconButton( 30 | onClick = onMoreMenuClick, 31 | iconResourceId = R.drawable.ic_ellipsis, 32 | ) 33 | } 34 | } 35 | 36 | @Preview(showBackground = true) 37 | @Composable 38 | private fun CategoryBottomSheetHeaderPreview() { 39 | MnTheme { 40 | CategoryBottomSheetHeaderContents( 41 | isVisibleAddButton = true, 42 | onEditClick = {}, 43 | onMoreMenuClick = {}, 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/home/category/model/CategoryManageState.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.home.category.model 2 | 3 | import androidx.annotation.StringRes 4 | import com.mohanyang.presentation.R 5 | 6 | enum class CategoryManageState( 7 | @StringRes val title: Int, 8 | ) { 9 | DEFAULT(R.string.change_category_title), 10 | EDIT(R.string.change_category_edit_title), 11 | DELETE(R.string.change_category_delete_title), 12 | ; 13 | 14 | fun isEdit() = this == EDIT 15 | 16 | fun isDefault() = this == DEFAULT 17 | 18 | fun isDelete() = this == DELETE 19 | } 20 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/home/category/model/CategoryModel.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.home.category.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class CategoryModel( 7 | val name: String, 8 | val icon: CategoryIcon = CategoryIcon.CAT, 9 | ) : java.io.Serializable 10 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/home/time/PomodoroTimeSettingElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.home.time 2 | 3 | import com.pomonyang.mohanyang.presentation.base.NetworkViewState 4 | import com.pomonyang.mohanyang.presentation.base.ViewEvent 5 | import com.pomonyang.mohanyang.presentation.base.ViewSideEffect 6 | import com.pomonyang.mohanyang.presentation.screen.home.category.model.CategoryIcon 7 | 8 | data class PomodoroTimeSettingState( 9 | val categoryNo: Int = 0, 10 | val titleName: String = "", 11 | val initialFocusTime: Int = 10, 12 | val initialRestTime: Int = 10, 13 | val pickFocusTime: Int = 10, 14 | val pickRestTime: Int = 10, 15 | val isFocus: Boolean = false, 16 | val categoryIcon: CategoryIcon = CategoryIcon.CAT, 17 | override val isLoading: Boolean = false, 18 | override val isInternalError: Boolean = false, 19 | override val isInvalidError: Boolean = false, 20 | override val lastRequestAction: PomodoroTimeSettingEvent? = null, 21 | ) : NetworkViewState() 22 | 23 | sealed interface PomodoroTimeSettingEvent : ViewEvent { 24 | data class Init(val isFocusTime: Boolean) : PomodoroTimeSettingEvent 25 | 26 | data object Submit : PomodoroTimeSettingEvent 27 | 28 | data class ChangePickTime( 29 | val time: Int, 30 | ) : PomodoroTimeSettingEvent 31 | 32 | data object ClickClose : PomodoroTimeSettingEvent 33 | 34 | data object ClickRetry : PomodoroTimeSettingEvent 35 | } 36 | 37 | sealed interface PomodoroTimeSettingEffect : ViewSideEffect { 38 | data object GoToPomodoroSettingScreen : PomodoroTimeSettingEffect 39 | 40 | data object ClosePomodoroTimerSettingScreen : PomodoroTimeSettingEffect 41 | } 42 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/mypage/MyPageElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.mypage 2 | 3 | import com.pomonyang.mohanyang.presentation.base.ViewEvent 4 | import com.pomonyang.mohanyang.presentation.base.ViewSideEffect 5 | import com.pomonyang.mohanyang.presentation.base.ViewState 6 | 7 | data class MyPageState( 8 | val catName: String = "", 9 | val isInterruptNotificationEnabled: Boolean = false, 10 | val isTimerNotificationEnabled: Boolean = false, 11 | val isLockScreenNotificationEnabled: Boolean = false, 12 | ) : ViewState 13 | 14 | sealed interface MyPageEvent : ViewEvent { 15 | data class Init(val appNotificationGranted: Boolean) : MyPageEvent 16 | data class ClickCatProfile(val isOffline: Boolean) : MyPageEvent 17 | data class ChangeInterruptNotification(val isEnabled: Boolean) : MyPageEvent 18 | data class ChangeTimerNotification(val isEnabled: Boolean) : MyPageEvent 19 | data class ChangeLockScreenNotification(val isEnabled: Boolean) : MyPageEvent 20 | data object CloseDialog : MyPageEvent 21 | data object OpenSetting : MyPageEvent 22 | data object ClickSuggestion : MyPageEvent 23 | } 24 | 25 | sealed interface MyPageSideEffect : ViewSideEffect { 26 | data object GoToCatProfilePage : MyPageSideEffect 27 | data class CheckNotificationPermission(val request: NotificationRequest, val onGranted: () -> Unit) : MyPageSideEffect 28 | data class OpenExternalWebPage(val url: String) : MyPageSideEffect 29 | data object CloseDialog : MyPageSideEffect 30 | data object OpenDialog : MyPageSideEffect 31 | data class ShowSnackBar(val message: String) : MyPageSideEffect 32 | } 33 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/onboarding/model/OnboardingGuideContent.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.onboarding.model 2 | 3 | import android.content.Context 4 | import androidx.annotation.DrawableRes 5 | import androidx.compose.runtime.Stable 6 | import com.mohanyang.presentation.R 7 | import kotlin.math.max 8 | 9 | @Stable 10 | data class OnboardingGuideContent( 11 | val title: String, 12 | val subtitle: String, 13 | @DrawableRes val image: Int, 14 | ) 15 | 16 | fun Context.getOnBoardingContents(): List { 17 | val guideTitles = this.resources.getStringArray(R.array.onboarding_guide_title) 18 | val guideSubTitles = this.resources.getStringArray(R.array.onboarding_guide_subtitle) 19 | val guideImages = this.resources.obtainTypedArray(R.array.onboarding_guide_image) 20 | 21 | val maxSize = max(guideTitles.size, guideSubTitles.size) 22 | 23 | val guides = List(maxSize) { index -> 24 | val title = guideTitles.getOrElse(index) { "" } 25 | val subtitle = guideSubTitles.getOrElse(index) { "" } 26 | val guideImage = guideImages.getResourceId(index, R.drawable.onboarding_contents_1) 27 | OnboardingGuideContent(title, subtitle, guideImage) 28 | } 29 | guideImages.recycle() 30 | 31 | return guides 32 | } 33 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/onboarding/naming/CatNameVerifier.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.onboarding.naming 2 | 3 | import javax.inject.Inject 4 | 5 | data class ValidationResult(val isValid: Boolean, val message: String = "") 6 | 7 | class CatNameVerifier @Inject constructor() { 8 | 9 | fun verifyName(name: String): ValidationResult { 10 | val validators = listOf( 11 | ::validateLength, 12 | ::validatePattern, 13 | ::validateFirstSpace, 14 | ) 15 | 16 | return validators.asSequence() 17 | .map { it(name) } 18 | .firstOrNull { it.isValid.not() } 19 | ?: ValidationResult(true) 20 | } 21 | 22 | private fun validateLength(name: String) = ValidationResult( 23 | isValid = name.length <= NAME_MAX_LENGTH, 24 | message = if (name.length <= NAME_MAX_LENGTH) "" else "최대 ${NAME_MAX_LENGTH}자리까지 허용됩니다.", 25 | ) 26 | 27 | private fun validatePattern(name: String) = ValidationResult( 28 | isValid = NAME_PATTERN.toRegex().matches(name), 29 | message = if (NAME_PATTERN.toRegex().matches(name)) "" else "특수 문자는 사용할 수 없습니다.", 30 | ) 31 | 32 | private fun validateFirstSpace(name: String) = ValidationResult( 33 | isValid = !name.first().isWhitespace(), 34 | message = if (name.first().isWhitespace()) "고양이 이름은 빈 칸이 될 수 없어요" else "", 35 | ) 36 | 37 | companion object { 38 | private const val NAME_MAX_LENGTH = 10 39 | private const val NAME_MIN_LENGTH = 1 40 | private const val NAME_PATTERN = "^[\\w\\s\\n]{${NAME_MIN_LENGTH},${NAME_MAX_LENGTH}}$" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/onboarding/naming/OnboardingNamingElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.onboarding.naming 2 | 3 | import com.pomonyang.mohanyang.presentation.base.NetworkViewState 4 | import com.pomonyang.mohanyang.presentation.base.ViewEvent 5 | import com.pomonyang.mohanyang.presentation.base.ViewSideEffect 6 | 7 | data class NamingState( 8 | override val isLoading: Boolean = false, 9 | override val isInvalidError: Boolean = false, 10 | override val isInternalError: Boolean = false, 11 | override val lastRequestAction: NamingEvent? = null, 12 | ) : NetworkViewState() 13 | 14 | sealed interface NamingEvent : ViewEvent { 15 | data class OnComplete(val name: String) : NamingEvent 16 | data object OnClickRetry : NamingEvent 17 | } 18 | 19 | sealed interface NamingSideEffect : ViewSideEffect { 20 | data object NavToNext : NamingSideEffect 21 | } 22 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/onboarding/select/OnboardingSelectCatElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.onboarding.select 2 | 3 | import com.pomonyang.mohanyang.presentation.base.NetworkViewState 4 | import com.pomonyang.mohanyang.presentation.base.ViewEvent 5 | import com.pomonyang.mohanyang.presentation.base.ViewSideEffect 6 | import com.pomonyang.mohanyang.presentation.model.cat.CatInfoModel 7 | import com.pomonyang.mohanyang.presentation.model.cat.CatType 8 | 9 | data class SelectCatState( 10 | override val isLoading: Boolean = true, 11 | override val isInternalError: Boolean = false, 12 | override val isInvalidError: Boolean = false, 13 | override val lastRequestAction: SelectCatEvent? = null, 14 | val cats: List = emptyList(), 15 | val selectedType: CatType? = null, 16 | ) : NetworkViewState() 17 | 18 | sealed interface SelectCatEvent : ViewEvent { 19 | data class Init(val catNo: Int? = null) : SelectCatEvent 20 | data class OnSelectType(val type: CatType) : SelectCatEvent 21 | data object OnStartClick : SelectCatEvent 22 | data object OnGrantedAlarmPermission : SelectCatEvent 23 | data object OnClickRetry : SelectCatEvent 24 | } 25 | 26 | sealed interface SelectCatSideEffect : ViewSideEffect { 27 | data class OnNavToNaming(val no: Int, val catName: String, val catTypeName: String) : SelectCatSideEffect 28 | data object GoToBack : SelectCatSideEffect 29 | data class ShowSnackBar(val message: String) : SelectCatSideEffect 30 | } 31 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/pomodoro/focus/PomodoroFocusElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.pomodoro.focus 2 | 3 | import com.pomonyang.mohanyang.presentation.base.ViewEvent 4 | import com.pomonyang.mohanyang.presentation.base.ViewSideEffect 5 | import com.pomonyang.mohanyang.presentation.base.ViewState 6 | import com.pomonyang.mohanyang.presentation.model.cat.CatType 7 | import com.pomonyang.mohanyang.presentation.screen.home.category.model.CategoryIcon 8 | import com.pomonyang.mohanyang.presentation.util.formatTime 9 | import java.util.* 10 | 11 | data class PomodoroFocusState( 12 | val pomodoroId: String = UUID.randomUUID().toString(), 13 | val remainingFocusTime: Int = 0, 14 | val focusExceededTime: Int = 0, 15 | val maxFocusTime: Int = 0, 16 | val title: String = "", 17 | val categoryIcon: CategoryIcon = CategoryIcon.CAT, 18 | val cat: CatType = CatType.CHEESE, 19 | val categoryNo: Int = -1, 20 | val forceGoRest: Boolean = false, 21 | ) : ViewState { 22 | 23 | fun displayFocusTime(): String = remainingFocusTime.formatTime() 24 | fun displayFocusExceedTime(): String = focusExceededTime.formatTime() 25 | 26 | val currentFocusTime: Int 27 | get() = maxFocusTime - remainingFocusTime 28 | } 29 | 30 | sealed interface PomodoroFocusEvent : ViewEvent { 31 | data object Init : PomodoroFocusEvent 32 | data object ClickRest : PomodoroFocusEvent 33 | data object ClickHome : PomodoroFocusEvent 34 | } 35 | 36 | sealed interface PomodoroFocusEffect : ViewSideEffect { 37 | data object GoToPomodoroRest : PomodoroFocusEffect 38 | data object GoToPomodoroSetting : PomodoroFocusEffect 39 | data object StartFocusAlarm : PomodoroFocusEffect 40 | data object StopFocusAlarm : PomodoroFocusEffect 41 | } 42 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/statistics/StaticsNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.statistics 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import androidx.navigation.NavHostController 5 | import androidx.navigation.navigation 6 | import com.pomonyang.mohanyang.presentation.util.composableWithDefaultTransition 7 | import kotlinx.serialization.Serializable 8 | 9 | @Serializable 10 | data object StatisticsGraph 11 | 12 | @Serializable 13 | data object Statistics 14 | 15 | fun NavGraphBuilder.statisticsScreen( 16 | onShowSnackbar: (String, Int?) -> Unit, 17 | navHostController: NavHostController, 18 | ) { 19 | navigation( 20 | startDestination = Statistics, 21 | ) { 22 | composableWithDefaultTransition { 23 | StatisticsRoute( 24 | onShowSnackbar = onShowSnackbar, 25 | ) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/statistics/StatisticsElements.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.statistics 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.runtime.Stable 5 | import com.pomonyang.mohanyang.presentation.base.ViewEvent 6 | import com.pomonyang.mohanyang.presentation.base.ViewSideEffect 7 | import com.pomonyang.mohanyang.presentation.base.ViewState 8 | import com.pomonyang.mohanyang.presentation.screen.statistics.model.StatisticsModel 9 | 10 | @Stable 11 | data class StatisticsState( 12 | val statisticsModel: StatisticsModel, 13 | ) : ViewState 14 | 15 | @Immutable 16 | sealed interface StatisticsEvent : ViewEvent 17 | 18 | @Immutable 19 | sealed interface StatisticsSideEffect : ViewSideEffect 20 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/statistics/StatisticsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.statistics 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.material3.Surface 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.hilt.navigation.compose.hiltViewModel 9 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 10 | import com.pomonyang.mohanyang.presentation.util.ThemePreviews 11 | 12 | @Composable 13 | fun StatisticsRoute( 14 | onShowSnackbar: (String, Int?) -> Unit, 15 | modifier: Modifier = Modifier, 16 | viewModel: StatisticsViewModel = hiltViewModel(), 17 | ) { 18 | StatisticsScreen(modifier) 19 | } 20 | 21 | @Composable 22 | private fun StatisticsScreen( 23 | modifier: Modifier = Modifier, 24 | ) { 25 | Surface( 26 | modifier = modifier.fillMaxSize(), 27 | color = MnTheme.backgroundColorScheme.primary, 28 | ) { 29 | Text("StatisticsScreen") 30 | } 31 | } 32 | 33 | @ThemePreviews 34 | @Composable 35 | private fun StatisticsScreenPreview() { 36 | MnTheme { 37 | StatisticsScreen() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/statistics/StatisticsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.statistics 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.pomonyang.mohanyang.data.repository.statistics.StatisticsRepository 5 | import com.pomonyang.mohanyang.presentation.base.BaseViewModel 6 | import com.pomonyang.mohanyang.presentation.screen.statistics.model.StatisticsModel 7 | import com.pomonyang.mohanyang.presentation.screen.statistics.model.mapper.toModel 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import java.time.LocalDate 10 | import javax.inject.Inject 11 | import kotlinx.coroutines.launch 12 | import timber.log.Timber 13 | 14 | @HiltViewModel 15 | class StatisticsViewModel @Inject constructor( 16 | private val statisticsRepository: StatisticsRepository, 17 | ) : BaseViewModel() { 18 | 19 | init { 20 | viewModelScope.launch { 21 | statisticsRepository.getStatistics( 22 | LocalDate.now(), 23 | ).onSuccess { statisticsResponse -> 24 | updateState { 25 | copy( 26 | statisticsModel = statisticsResponse.toModel(), 27 | ) 28 | } 29 | }.onFailure { error -> 30 | Timber.e("getStatistics fail $error") 31 | } 32 | } 33 | } 34 | 35 | override fun setInitialState(): StatisticsState = StatisticsState( 36 | statisticsModel = StatisticsModel.placeHolder, 37 | ) 38 | 39 | override fun handleEvent(event: StatisticsEvent) { 40 | when (event) { 41 | else -> {} 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/statistics/component/Dot.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.statistics.component 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.unit.dp 16 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 17 | 18 | @Composable 19 | fun Dot( 20 | modifier: Modifier = Modifier, 21 | ) { 22 | Box( 23 | modifier = modifier 24 | .size( 25 | width = 1.dp, 26 | height = 5.dp, 27 | ) 28 | .background( 29 | color = MnTheme.iconColorScheme.disabled, 30 | shape = RoundedCornerShape(50.dp), 31 | ), 32 | ) 33 | } 34 | 35 | @Preview 36 | @Composable 37 | private fun DotPreview() { 38 | val repeatCount = 100 39 | MnTheme { 40 | Column( 41 | modifier = Modifier 42 | .fillMaxSize() 43 | .background(Color.Black), 44 | verticalArrangement = Arrangement.spacedBy(3.dp, Alignment.CenterVertically), 45 | horizontalAlignment = Alignment.CenterHorizontally, 46 | ) { 47 | repeat(repeatCount) { 48 | Dot() 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/screen/statistics/component/StatisticsContentHeader.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.screen.statistics.component 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import com.mohanyang.presentation.R 11 | import com.pomonyang.mohanyang.presentation.designsystem.icon.MnSmallIcon 12 | import com.pomonyang.mohanyang.presentation.designsystem.token.MnSpacing 13 | import com.pomonyang.mohanyang.presentation.theme.MnTheme 14 | 15 | @Composable 16 | fun StatisticsContentHeader( 17 | time: String, // 데이터 형식 어떻게 뽑을지 고민 중 18 | modifier: Modifier = Modifier, 19 | ) { 20 | Row( 21 | verticalAlignment = Alignment.CenterVertically, 22 | horizontalArrangement = Arrangement.spacedBy(MnSpacing.xSmall), 23 | modifier = modifier, 24 | ) { 25 | MnSmallIcon( 26 | resourceId = R.drawable.ic_circle, 27 | modifier = modifier, 28 | tint = MnTheme.iconColorScheme.disabled, 29 | ) 30 | 31 | Text( 32 | text = time, 33 | style = MnTheme.typography.subBodyRegular, 34 | color = MnTheme.textColorScheme.tertiary, 35 | ) 36 | } 37 | } 38 | 39 | @Preview 40 | @Composable 41 | private fun StatisticsContentHeaderPreview() { 42 | MnTheme { 43 | StatisticsContentHeader("11:58-13:32") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimer.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.service 2 | 3 | import com.pomonyang.mohanyang.presentation.screen.home.category.model.CategoryModel 4 | 5 | internal interface PomodoroTimer { 6 | 7 | fun startTimer( 8 | timerId: String, 9 | maxTime: Int, 10 | eventHandler: PomodoroTimerEventHandler, 11 | category: CategoryModel? = null, 12 | ) 13 | 14 | fun stopTimer() 15 | } 16 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.service 2 | 3 | import com.pomonyang.mohanyang.presentation.screen.home.category.model.CategoryModel 4 | 5 | internal interface PomodoroTimerEventHandler { 6 | fun onTimeEnd() 7 | fun onTimeExceeded() 8 | fun updateTimer( 9 | timerId: String, 10 | time: String, 11 | overtime: String, 12 | category: CategoryModel?, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/PomodoroTimerServiceExtras.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.service 2 | 3 | internal object PomodoroTimerServiceExtras { 4 | const val INTENT_TIMER_MAX_TIME = "mohanyang.intent.MAX_TIME" 5 | const val INTENT_TIMER_ID = "mohanyang.intent.TIMER_ID" 6 | const val INTENT_CATEGORY = "mohanyang.intent.CATEGORY" 7 | const val ACTION_TIMER_START = "mohanyang.action.TIMER_START" 8 | const val ACTION_TIMER_STOP = "mohanyang.action.TIMER_STOP" 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/focus/FocusTimer.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.service.focus 2 | 3 | import com.pomonyang.mohanyang.presentation.service.BasePomodoroTimer 4 | import javax.inject.Inject 5 | 6 | internal class FocusTimer @Inject constructor() : BasePomodoroTimer() { 7 | 8 | override fun getTagName(): String = "TIMER_FOCUS" 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/service/rest/RestTimer.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.service.rest 2 | 3 | import com.pomonyang.mohanyang.presentation.service.BasePomodoroTimer 4 | import javax.inject.Inject 5 | 6 | internal class RestTimer @Inject constructor() : BasePomodoroTimer() { 7 | 8 | override fun getTagName(): String = "TIMER_REST" 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/DpPxSpConversionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.platform.LocalDensity 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.TextUnit 7 | import androidx.compose.ui.unit.dp 8 | import androidx.compose.ui.unit.sp 9 | 10 | // dp(Dp) → px(Float) 11 | @Composable 12 | internal fun Dp.dpToPx(): Float = this.value * LocalDensity.current.density 13 | 14 | // dp(Dp) → sp(TextUnit) 15 | @Composable 16 | internal fun Dp.dpToSp(): TextUnit = (this.value * LocalDensity.current.density / LocalDensity.current.fontScale).sp 17 | 18 | // px(Float) → dp(Dp) 19 | @Composable 20 | internal fun Float.pxToDp(): Dp = (this / LocalDensity.current.density).dp 21 | 22 | // px(Float) → sp(TextUnit) 23 | @Composable 24 | internal fun Float.pxToSp(): TextUnit = (this / LocalDensity.current.fontScale).sp 25 | 26 | // sp(TextUnit) → dp(Dp) 27 | @Composable 28 | internal fun TextUnit.spToDp(): Dp = (this.value * LocalDensity.current.fontScale / LocalDensity.current.density).dp 29 | 30 | // sp(TextUnit) → px(Float) 31 | @Composable 32 | internal fun TextUnit.spToPx(): Float = this.value * LocalDensity.current.fontScale 33 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/FlowUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.compose.LocalLifecycleOwner 7 | import androidx.lifecycle.repeatOnLifecycle 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Composable 11 | inline fun Flow.collectWithLifecycle( 12 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 13 | noinline action: suspend (T) -> Unit, 14 | ) { 15 | val lifecycleOwner = LocalLifecycleOwner.current 16 | 17 | LaunchedEffect(this, lifecycleOwner) { 18 | lifecycleOwner.lifecycle.repeatOnLifecycle(minActiveState) { 19 | this@collectWithLifecycle.collect { action(it) } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/IntentUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.util 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import java.io.Serializable 6 | 7 | inline fun Intent.getSerializableExtraCompat(key: String): T? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 8 | getSerializableExtra(key, T::class.java) 9 | } else { 10 | @Suppress("DEPRECATION") 11 | getSerializableExtra(key) as? T 12 | } 13 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/NavigationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.util 2 | 3 | import androidx.compose.animation.AnimatedContentScope 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.runtime.Composable 8 | import androidx.navigation.NavBackStackEntry 9 | import androidx.navigation.NavGraphBuilder 10 | import androidx.navigation.compose.composable 11 | 12 | inline fun NavGraphBuilder.composableWithDefaultTransition( 13 | noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, 14 | ) { 15 | composable( 16 | enterTransition = { fadeIn(animationSpec = tween(300)) }, 17 | popEnterTransition = { fadeIn(animationSpec = tween(300)) }, 18 | popExitTransition = { fadeOut(animationSpec = tween(300)) }, 19 | content = content, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/PreviewUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.util 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.ui.tooling.preview.Devices 5 | import androidx.compose.ui.tooling.preview.Preview 6 | 7 | @Preview( 8 | name = "LightTheme", 9 | group = "Theme", 10 | showBackground = true, 11 | backgroundColor = 0xFFFAF6F3, 12 | uiMode = Configuration.UI_MODE_NIGHT_NO, 13 | ) 14 | @Preview( 15 | name = "DarkTheme", 16 | group = "Theme", 17 | showBackground = true, 18 | backgroundColor = 0xFFFAF6F3, 19 | uiMode = Configuration.UI_MODE_NIGHT_YES, 20 | ) 21 | annotation class ThemePreviews 22 | 23 | @Preview( 24 | name = "Normal", 25 | device = "spec:shape=Normal,width=1440,height=2800,unit=px,dpi=515", 26 | showBackground = true, 27 | backgroundColor = 0xFFFAF6F3, 28 | ) 29 | @Preview( 30 | name = "Short", 31 | device = "spec:shape=Normal,width=1440,height=2000,unit=px,dpi=515", 32 | showBackground = true, 33 | backgroundColor = 0xFFFAF6F3, 34 | ) 35 | @Preview( 36 | name = "Foldable", 37 | device = Devices.FOLDABLE, 38 | showBackground = true, 39 | backgroundColor = 0xFFFAF6F3, 40 | ) 41 | annotation class DevicePreviews 42 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/pomonyang/mohanyang/presentation/util/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pomonyang.mohanyang.presentation.util 2 | 3 | import java.time.LocalTime 4 | import java.util.* 5 | 6 | fun LocalTime.displayAlarm(): String = "${this.hour}".padStart(2, '0') + ":" + "${this.minute}".padStart(2, '0') + " " + if (this.hour < 12) "AM" else "PM" 7 | 8 | fun Int.formatTime(): String { 9 | val minutesPart = this / 60 10 | val secondsPart = this % 60 11 | return String.format(Locale.KOREAN, "%02d:%02d", minutesPart, secondsPart) 12 | } 13 | 14 | fun Int.formatToMinutesAndSeconds(): String { 15 | val minutes = this 16 | val seconds = 0 17 | return String.format("%02d:%02d", minutes, seconds) 18 | } 19 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_alert.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_arrow_left.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_arrow_up.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_asterisk.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_box_pen.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_brifecase.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_bubble_ellipses.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_category_default.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_chart_bar.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_chart_bar_fill.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_chevron_down.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_chevron_left.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_chevron_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_chevron_up.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_clock.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_dumbbell.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_ellipsis.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_feedback.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 16 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_fire.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_house.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_house_fill.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_lightning.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_lock.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_minus.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_monitor.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_moon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_null.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_open_book.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_pen.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_plus.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_static_ready.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_trashcan.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_user.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_user_fill.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/img_touch_hair_ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/drawable/img_touch_hair_ball.png -------------------------------------------------------------------------------- /presentation/src/main/res/font/pretendard_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/font/pretendard_bold.ttf -------------------------------------------------------------------------------- /presentation/src/main/res/font/pretendard_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/font/pretendard_regular.ttf -------------------------------------------------------------------------------- /presentation/src/main/res/font/pretendard_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/font/pretendard_semibold.ttf -------------------------------------------------------------------------------- /presentation/src/main/res/layout/notification_pomodoro_standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 18 | 19 | 24 | 25 | 26 | 27 | 31 | -------------------------------------------------------------------------------- /presentation/src/main/res/raw/alarm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/raw/alarm.mp3 -------------------------------------------------------------------------------- /presentation/src/main/res/raw/cat_focus.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/raw/cat_focus.riv -------------------------------------------------------------------------------- /presentation/src/main/res/raw/cat_home.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/raw/cat_home.riv -------------------------------------------------------------------------------- /presentation/src/main/res/raw/cat_rename_2.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/raw/cat_rename_2.riv -------------------------------------------------------------------------------- /presentation/src/main/res/raw/cat_rest.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/raw/cat_rest.riv -------------------------------------------------------------------------------- /presentation/src/main/res/raw/cat_select_2.riv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nexters/PomoNyang-Android/b0d194d192747a7fe1abc9a5d9bffea357610a7f/presentation/src/main/res/raw/cat_select_2.riv -------------------------------------------------------------------------------- /presentation/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @drawable/onboarding_contents_1 5 | @drawable/onboarding_contents_2 6 | @drawable/onboarding_contents_3 7 | 8 | 9 | -------------------------------------------------------------------------------- /presentation/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E46900 4 | #000000 5 | #8F887E 6 | #3D3732 7 | #FF7E65 8 | #1D1B1B 9 | #3F4946 10 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild("build-logic") 3 | repositories { 4 | google { 5 | content { 6 | includeGroupByRegex("com\\.android.*") 7 | includeGroupByRegex("com\\.google.*") 8 | includeGroupByRegex("androidx.*") 9 | } 10 | } 11 | mavenCentral() 12 | gradlePluginPortal() 13 | } 14 | } 15 | dependencyResolutionManagement { 16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 17 | repositories { 18 | google() 19 | mavenCentral() 20 | } 21 | } 22 | 23 | rootProject.name = "moha-nyang" 24 | include(":app") 25 | include(":presentation") 26 | include(":data") 27 | include(":domain") 28 | --------------------------------------------------------------------------------