├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── labeler.yml └── workflows │ ├── android.yml │ └── labeler.yml ├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── drunkenboys │ │ └── calendarun │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── default.db │ │ └── switching_calendar.json │ ├── java │ │ └── com │ │ │ └── drunkenboys │ │ │ └── calendarun │ │ │ ├── App.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SavedStateKeys.kt │ │ │ ├── data │ │ │ ├── calendar │ │ │ │ ├── entity │ │ │ │ │ └── Calendar.kt │ │ │ │ └── local │ │ │ │ │ ├── CalendarDao.kt │ │ │ │ │ ├── CalendarLocalDataSource.kt │ │ │ │ │ └── CalendarLocalDataSourceImpl.kt │ │ │ ├── calendartheme │ │ │ │ ├── entity │ │ │ │ │ └── CalendarTheme.kt │ │ │ │ └── local │ │ │ │ │ ├── CalendarThemeDao.kt │ │ │ │ │ ├── CalendarThemeLocalDataSource.kt │ │ │ │ │ └── CalendarThemeLocalDataSourceImpl.kt │ │ │ ├── holiday │ │ │ │ ├── ResponseHolidayInfo.kt │ │ │ │ ├── entity │ │ │ │ │ └── Holiday.kt │ │ │ │ ├── local │ │ │ │ │ ├── HolidayDao.kt │ │ │ │ │ ├── HolidayLocalDataSource.kt │ │ │ │ │ └── HolidayLocalDataSourceImpl.kt │ │ │ │ ├── remote │ │ │ │ │ ├── HolidayRemoteDataSource.kt │ │ │ │ │ ├── HolidayRemoteDataSourceImpl.kt │ │ │ │ │ └── HolidayRemoteService.kt │ │ │ │ └── repository │ │ │ │ │ ├── HolidayRepository.kt │ │ │ │ │ └── HolidayRepositoryImpl.kt │ │ │ ├── room │ │ │ │ ├── Converters.kt │ │ │ │ └── Database.kt │ │ │ ├── schedule │ │ │ │ ├── entity │ │ │ │ │ └── Schedule.kt │ │ │ │ └── local │ │ │ │ │ ├── ScheduleDao.kt │ │ │ │ │ ├── ScheduleLocalDataSource.kt │ │ │ │ │ └── ScheduleLocalDataSourceImpl.kt │ │ │ └── slice │ │ │ │ ├── entity │ │ │ │ └── Slice.kt │ │ │ │ └── local │ │ │ │ ├── SliceDao.kt │ │ │ │ ├── SliceLocalDataSource.kt │ │ │ │ └── SliceLocalDataSourceImpl.kt │ │ │ ├── di │ │ │ ├── CoroutineModule.kt │ │ │ ├── DataSourceModule.kt │ │ │ ├── HolidayRetrofitServiceModule.kt │ │ │ ├── LocalDatabaseModule.kt │ │ │ ├── RepositoryModule.kt │ │ │ └── RetrofitModule.kt │ │ │ ├── receiver │ │ │ ├── BootReceiver.kt │ │ │ └── ScheduleAlarmReceiver.kt │ │ │ ├── ui │ │ │ ├── appinfo │ │ │ │ └── AppInfoFragment.kt │ │ │ ├── appwidget │ │ │ │ ├── CalendaRunAppWidget.kt │ │ │ │ ├── CalendaRunRemoteViewsFactory.kt │ │ │ │ └── CalendaRunRemoteViewsService.kt │ │ │ ├── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ └── BaseViewHolder.kt │ │ │ ├── dayschedule │ │ │ │ ├── DayScheduleAdapter.kt │ │ │ │ ├── DayScheduleDialog.kt │ │ │ │ └── DayScheduleViewModel.kt │ │ │ ├── maincalendar │ │ │ │ ├── LoadingDialog.kt │ │ │ │ ├── MainCalendarFragment.kt │ │ │ │ └── MainCalendarViewModel.kt │ │ │ ├── managecalendar │ │ │ │ ├── DeleteCalendarDialog.kt │ │ │ │ ├── ManageCalendarAdapter.kt │ │ │ │ ├── ManageCalendarFragment.kt │ │ │ │ ├── ManageCalendarViewModel.kt │ │ │ │ └── model │ │ │ │ │ └── CalendarItem.kt │ │ │ ├── savecalendar │ │ │ │ ├── SaveCalendarAdapter.kt │ │ │ │ ├── SaveCalendarFragment.kt │ │ │ │ ├── SaveCalendarViewModel.kt │ │ │ │ └── model │ │ │ │ │ └── SliceItem.kt │ │ │ ├── saveschedule │ │ │ │ ├── DeleteScheduleDialog.kt │ │ │ │ ├── SaveScheduleFragment.kt │ │ │ │ ├── SaveScheduleViewModel.kt │ │ │ │ └── model │ │ │ │ │ ├── BehaviorType.kt │ │ │ │ │ └── DateType.kt │ │ │ ├── searchschedule │ │ │ │ ├── SearchScheduleAdapter.kt │ │ │ │ ├── SearchScheduleDivider.kt │ │ │ │ ├── SearchScheduleFragment.kt │ │ │ │ ├── SearchScheduleViewModel.kt │ │ │ │ └── model │ │ │ │ │ └── DateScheduleItem.kt │ │ │ ├── setting │ │ │ │ ├── SettingFragment.kt │ │ │ │ └── SettingViewModel.kt │ │ │ └── theme │ │ │ │ ├── CalendarThemeMapper.kt │ │ │ │ ├── ThemeFragment.kt │ │ │ │ ├── ThemeResetDialog.kt │ │ │ │ ├── ThemeViewModel.kt │ │ │ │ └── model │ │ │ │ └── ThemeColorType.kt │ │ │ ├── util │ │ │ ├── DateConverter.kt │ │ │ ├── HorizontalInsetDividerDecoration.kt │ │ │ └── extensions │ │ │ │ ├── AnimationExt.kt │ │ │ │ ├── ContextExt.kt │ │ │ │ ├── FlowExt.kt │ │ │ │ ├── FragmentExt.kt │ │ │ │ ├── NavigationExt.kt │ │ │ │ ├── NetworkStateUtil.kt │ │ │ │ ├── PendingIntentExt.kt │ │ │ │ ├── ViewExt.kt │ │ │ │ └── ViewHolderExt.kt │ │ │ └── view │ │ │ ├── ColorIndicatorTextView.kt │ │ │ ├── ErrorGuideEditText.kt │ │ │ ├── ErrorGuideTextView.kt │ │ │ └── ExpandableLinearLayout.kt │ └── res │ │ ├── anim │ │ ├── hide_scale_down.xml │ │ ├── show_scale_up.xml │ │ ├── slide_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ │ ├── color │ │ ├── navigation_item_background_color.xml │ │ └── navigation_item_color.xml │ │ ├── drawable-v21 │ │ ├── app_widget_background.xml │ │ └── app_widget_inner_view_background.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── ic_check_box_normal_18.xml │ │ ├── drawable │ │ ├── bg_on_surface_radius_8dp.xml │ │ ├── bg_tag_color.xml │ │ ├── bg_tag_color_black_stroke_grey.xml │ │ ├── bg_tag_color_white_stroke_grey.xml │ │ ├── bg_white_radius_24dp.xml │ │ ├── btn_rect_black_background.xml │ │ ├── ic_add.xml │ │ ├── ic_add_circle_24.xml │ │ ├── ic_assignment.xml │ │ ├── ic_baseline_check_box_24.xml │ │ ├── ic_baseline_check_box_outline_blank_24.xml │ │ ├── ic_calendar_today.xml │ │ ├── ic_calendar_today_white.xml │ │ ├── ic_check_box_pressed_18.xml │ │ ├── ic_delete.xml │ │ ├── ic_favorite_24.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_license.xml │ │ ├── ic_list.xml │ │ ├── ic_main_background.xml │ │ ├── ic_menu_24.xml │ │ ├── ic_notifications.xml │ │ ├── ic_palette.xml │ │ ├── ic_refresh.xml │ │ ├── ic_search_white.xml │ │ ├── ic_settings.xml │ │ ├── ic_share_white.xml │ │ ├── ic_text_fields.xml │ │ └── selector_check_box_18.xml │ │ ├── font │ │ └── inter_medium.ttf │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── calendarun_app_widget.xml │ │ ├── dialog_day_schedule.xml │ │ ├── dialog_loading.xml │ │ ├── drawer_header.xml │ │ ├── fragment_app_info.xml │ │ ├── fragment_main_calendar.xml │ │ ├── fragment_manage_calendar.xml │ │ ├── fragment_save_calendar.xml │ │ ├── fragment_save_schedule.xml │ │ ├── fragment_search_schedule.xml │ │ ├── fragment_setting.xml │ │ ├── fragment_theme.xml │ │ ├── item_app_widget_schedule.xml │ │ ├── item_calendar.xml │ │ ├── item_date.xml │ │ ├── item_drop_down_list.xml │ │ ├── item_schedule.xml │ │ ├── item_slice.xml │ │ ├── view_color_set.xml │ │ └── view_pick_tag_color.xml │ │ ├── menu │ │ ├── menu_main_calendar_nav_drawer.xml │ │ ├── menu_main_calendar_toolbar.xml │ │ ├── menu_manage_calendar_toolbar.xml │ │ ├── menu_save_calendar_toolbar.xml │ │ ├── menu_save_schedule_toolbar.xml │ │ └── menu_theme_toolbar.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ ├── ic_launcher_round.xml │ │ ├── ic_main.xml │ │ └── ic_main_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ ├── ic_main.png │ │ ├── ic_main_foreground.png │ │ └── ic_main_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ ├── ic_main.png │ │ ├── ic_main_foreground.png │ │ └── ic_main_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ ├── ic_main.png │ │ ├── ic_main_foreground.png │ │ └── ic_main_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ ├── ic_main.png │ │ ├── ic_main_foreground.png │ │ └── ic_main_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ ├── ic_main.png │ │ ├── ic_main_foreground.png │ │ └── ic_main_round.png │ │ ├── navigation │ │ └── nav_main.xml │ │ ├── values-night-v31 │ │ └── themes.xml │ │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-v31 │ │ ├── styles.xml │ │ └── themes.xml │ │ ├── values │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ ├── xml-v31 │ │ └── calenda_run_app_widget_info.xml │ │ └── xml │ │ └── calenda_run_app_widget_info.xml │ └── test │ └── java │ └── com │ └── drunkenboys │ └── calendarun │ ├── ExampleUnitTest.kt │ ├── data │ ├── calendar │ │ └── local │ │ │ └── FakeCalendarLocalDataSource.kt │ ├── schedule │ │ └── local │ │ │ └── FakeScheduleLocalDataSource.kt │ └── slice │ │ └── local │ │ └── FakeSliceLocalDataSource.kt │ └── ui │ ├── savecalendar │ └── SaveCalendarViewModelTest.kt │ ├── saveschedule │ └── SaveScheduleViewModelTest.kt │ └── searchschedule │ └── SearchScheduleViewModelTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── library ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── drunkenboys │ │ └── ckscalendar │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_main-playstore.png │ ├── java │ │ └── com │ │ │ └── drunkenboys │ │ │ └── ckscalendar │ │ │ ├── WeekCalendarView.kt │ │ │ ├── data │ │ │ ├── CalendarDate.kt │ │ │ ├── CalendarDesignObject.kt │ │ │ ├── CalendarScheduleObject.kt │ │ │ ├── CalendarSet.kt │ │ │ ├── DayType.kt │ │ │ └── ScheduleColorType.kt │ │ │ ├── listener │ │ │ ├── OnDayClickListener.kt │ │ │ └── OnDaySecondClickListener.kt │ │ │ ├── monthcalendar │ │ │ ├── MonthAdapter.kt │ │ │ ├── MonthCalendarView.kt │ │ │ ├── MonthCellFactory.kt │ │ │ ├── MonthCellPositionStore.kt │ │ │ ├── MonthHeaderItemDecorator.kt │ │ │ ├── MonthPageAdapter.kt │ │ │ ├── MonthState.kt │ │ │ └── OnDaySelectStateListener.kt │ │ │ ├── utils │ │ │ ├── CalendarSetConverter.kt │ │ │ ├── ComposeStateExtensions.kt │ │ │ ├── DateConverter.kt │ │ │ ├── GravityMapper.kt │ │ │ ├── TimeUtils.kt │ │ │ └── ViewExtensions.kt │ │ │ ├── weekcalendar │ │ │ └── WeekCalendar.kt │ │ │ └── yearcalendar │ │ │ ├── CustomTheme.kt │ │ │ ├── YearCalendarView.kt │ │ │ ├── YearCalendarViewModel.kt │ │ │ └── composeView │ │ │ ├── BaseText.kt │ │ │ ├── CalendarLazyColumn.kt │ │ │ ├── DayText.kt │ │ │ ├── MonthHeader.kt │ │ │ ├── README.md │ │ │ ├── ScheduleText.kt │ │ │ ├── WeekCalendar.kt │ │ │ ├── WeekHeader.kt │ │ │ └── YearHeader.kt │ └── res │ │ ├── drawable │ │ ├── bg_calendar_line_05px.xml │ │ ├── bg_calendar_today.xml │ │ ├── bg_month_date_selected.xml │ │ └── ic_main_background.xml │ │ ├── layout │ │ ├── item_month_cell.xml │ │ ├── item_month_page.xml │ │ ├── layout_month_calendar.xml │ │ ├── layout_week_calendar.xml │ │ └── layout_year_calendar.xml │ │ ├── values-night │ │ └── colors.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── drunkenboys │ └── ckscalendar │ ├── ExampleUnitTest.kt │ ├── utils │ ├── CalendarSetConverterTest.kt │ └── UtilsTest.kt │ └── yearcalendar │ └── YearCalendarViewModelTest.kt └── settings.gradle /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @boostcampwm-2021/andrunken @shshksh @Cha-Ji @PsPLoG @simeunseok -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: feature 이슈 템플릿 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **사전 작업** 11 | - 라벨 달기(app, library, chore, bug, hotfix, feature) 12 | - project달기 13 | - 마일스톤 달기 14 | - asignees 지정 15 | 16 | ## 진행 상황(option) 17 | - [ ] 18 | - [ ] 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## what is this pr 2 | 4 | 5 | ## Changes 6 | 7 | 8 | ## screenshot 9 | 10 | 11 | 12 | ## testchecklist 13 | 14 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | library: 2 | - library/**/* 3 | 4 | app: 5 | - app/**/* 6 | 7 | test: 8 | - app/src/test/**/* 9 | - app/src/androidTest/**/* 10 | - library/src/test/**/* 11 | - library/src/androidTest/**/* 12 | 13 | chore: 14 | - app/*.gradle 15 | - library/*.gradle 16 | - build.gradle -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ develop, master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: set up JDK 11 15 | uses: actions/setup-java@v2 16 | with: 17 | java-version: '11' 18 | distribution: 'adopt' 19 | cache: gradle 20 | 21 | - name: Grant execute permission for gradlew 22 | run: chmod +x gradlew 23 | 24 | - name: generate local.properties 25 | run: | 26 | echo ${{ secrets.HOLIDAY_API_KEY }} 27 | echo ${{ secrets.HOLIDAY_API_KEY }} > ./local.properties 28 | cat ./local.properties 29 | 30 | - name: Build with Gradle 31 | run: ./gradlew build 32 | 33 | - name: Unit Test 34 | run: ./gradlew testDebugUnitTest 35 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | pull_request_target: 4 | branches: 5 | - develop 6 | 7 | jobs: 8 | label: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/labeler@v2 13 | with: 14 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | /.idea/ 87 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "969552868617", 4 | "project_id": "calendarun-496ef", 5 | "storage_bucket": "calendarun-496ef.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:969552868617:android:0883d9aebfe785bf894fd8", 11 | "android_client_info": { 12 | "package_name": "com.drunkenboys.calendarun" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "969552868617-bd4bpbtqahmisq5e5jnpsgnrvpanuh4b.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyD9eXO5MQd50gOqu93bTdzPJAPThlP5nyk" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "969552868617-bd4bpbtqahmisq5e5jnpsgnrvpanuh4b.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /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/androidTest/java/com/drunkenboys/calendarun/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.drunkenboys.calendarun", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/assets/default.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/assets/default.db -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/App.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun 2 | 3 | import android.app.Application 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.os.Build 7 | import androidx.core.content.getSystemService 8 | import com.drunkenboys.calendarun.receiver.ScheduleAlarmReceiver 9 | import dagger.hilt.android.HiltAndroidApp 10 | 11 | @HiltAndroidApp 12 | class App : Application() { 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | prepareNotificationChannel() 17 | } 18 | 19 | private fun prepareNotificationChannel() { 20 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 21 | val notificationManager = applicationContext.getSystemService() ?: return 22 | 23 | notificationManager.getNotificationChannel(ScheduleAlarmReceiver.SCHEDULE_NOTIFICATION_CHANNEL_ID) ?: run { 24 | val notificationChannel = NotificationChannel( 25 | ScheduleAlarmReceiver.SCHEDULE_NOTIFICATION_CHANNEL_ID, 26 | ScheduleAlarmReceiver.SCHEDULE_NOTIFICATION_CHANNEL_NAME, 27 | NotificationManager.IMPORTANCE_HIGH 28 | ) 29 | 30 | notificationManager.createNotificationChannel(notificationChannel) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/SavedStateKeys.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun 2 | 3 | const val KEY_CALENDAR_ID = "calendarId" 4 | const val KEY_SCHEDULE_ID = "scheduleId" 5 | const val KEY_LOCAL_DATE = "localDate" 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendar/entity/Calendar.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendar.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import java.time.LocalDate 6 | 7 | @Entity(tableName = "Calendar") 8 | data class Calendar( 9 | @PrimaryKey(autoGenerate = true) val id: Long = 0, 10 | val name: String, 11 | val startDate: LocalDate, 12 | val endDate: LocalDate 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendar/local/CalendarDao.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendar.local 2 | 3 | import androidx.room.* 4 | import com.drunkenboys.calendarun.data.calendar.entity.Calendar 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | @Dao 8 | interface CalendarDao { 9 | 10 | @Insert 11 | suspend fun insertCalendar(calendar: Calendar): Long 12 | 13 | @Query("SELECT * FROM `calendar`") 14 | fun fetchAllCalendar(): Flow> 15 | 16 | @Query("SELECT * FROM `calendar` WHERE id != 1") 17 | fun fetchCustomCalendar(): Flow> 18 | 19 | @Query("SELECT * FROM `calendar` WHERE id == :id") 20 | suspend fun fetchCalendar(id: Long): Calendar 21 | 22 | @Delete 23 | suspend fun deleteCalendar(calendar: Calendar) 24 | 25 | @Update 26 | suspend fun updateCalendar(calendar: Calendar) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendar/local/CalendarLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendar.local 2 | 3 | import com.drunkenboys.calendarun.data.calendar.entity.Calendar 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface CalendarLocalDataSource { 7 | 8 | suspend fun insertCalendar(calendar: Calendar): Long 9 | 10 | fun fetchAllCalendar(): Flow> 11 | 12 | fun fetchCustomCalendar(): Flow> 13 | 14 | suspend fun fetchCalendar(id: Long): Calendar 15 | 16 | suspend fun deleteCalendar(calendar: Calendar) 17 | 18 | suspend fun updateCalendar(calendar: Calendar) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendar/local/CalendarLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendar.local 2 | 3 | import com.drunkenboys.calendarun.data.calendar.entity.Calendar 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import javax.inject.Inject 8 | 9 | class CalendarLocalDataSourceImpl @Inject constructor( 10 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO, 11 | private val calendarDao: CalendarDao 12 | ) : CalendarLocalDataSource { 13 | 14 | override suspend fun insertCalendar(calendar: Calendar) = withContext(dispatcher) { 15 | calendarDao.insertCalendar(calendar) 16 | } 17 | 18 | override fun fetchAllCalendar() = calendarDao.fetchAllCalendar() 19 | 20 | override fun fetchCustomCalendar() = calendarDao.fetchCustomCalendar() 21 | 22 | override suspend fun fetchCalendar(id: Long) = withContext(dispatcher) { 23 | calendarDao.fetchCalendar(id) 24 | } 25 | 26 | override suspend fun deleteCalendar(calendar: Calendar) { 27 | calendarDao.deleteCalendar(calendar) 28 | } 29 | 30 | override suspend fun updateCalendar(calendar: Calendar) { 31 | calendarDao.updateCalendar(calendar) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendartheme/entity/CalendarTheme.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendartheme.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "CalendarTheme") 7 | data class CalendarTheme( 8 | @PrimaryKey val id: Long = 1, 9 | val weekDayTextColor: Int, 10 | val holidayTextColor: Int, 11 | val saturdayTextColor: Int, 12 | val sundayTextColor: Int, 13 | val selectedFrameColor: Int, 14 | val backgroundColor: Int, 15 | val textSize: Float, 16 | val textAlign: Int, 17 | val languageType: LanguageType, 18 | val visibleScheduleCount: Int 19 | ) { 20 | 21 | enum class LanguageType(val description: String) { 22 | KOREAN("한글"), 23 | ENGLISH("영어") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendartheme/local/CalendarThemeDao.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendartheme.local 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import androidx.room.Update 6 | import com.drunkenboys.calendarun.data.calendartheme.entity.CalendarTheme 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface CalendarThemeDao { 11 | 12 | @Query("SELECT * FROM CalendarTheme where id = 1") 13 | fun fetchCalendarTheme(): Flow 14 | 15 | @Update 16 | suspend fun updateCalendarTheme(theme: CalendarTheme) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendartheme/local/CalendarThemeLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendartheme.local 2 | 3 | import com.drunkenboys.calendarun.data.calendartheme.entity.CalendarTheme 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface CalendarThemeLocalDataSource { 7 | 8 | fun fetchCalendarTheme(): Flow 9 | 10 | suspend fun updateCalendarTheme(theme: CalendarTheme) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/calendartheme/local/CalendarThemeLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendartheme.local 2 | 3 | import com.drunkenboys.calendarun.data.calendartheme.entity.CalendarTheme 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.withContext 7 | import javax.inject.Inject 8 | 9 | class CalendarThemeLocalDataSourceImpl @Inject constructor( 10 | private val calendarThemeDao: CalendarThemeDao, 11 | private val dispatcher: CoroutineDispatcher 12 | ) : CalendarThemeLocalDataSource { 13 | 14 | override fun fetchCalendarTheme(): Flow = calendarThemeDao.fetchCalendarTheme() 15 | 16 | override suspend fun updateCalendarTheme(theme: CalendarTheme) = withContext(dispatcher) { 17 | calendarThemeDao.updateCalendarTheme(theme) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/ResponseHolidayInfo.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ResponseHolidayListInfo( 6 | val response: HolidayListResponse 7 | ) 8 | 9 | data class HolidayListResponse( 10 | val body: HolidayListBody 11 | ) 12 | 13 | data class HolidayListBody( 14 | val items: HolidayListItems 15 | ) 16 | 17 | data class HolidayListItems( 18 | val item: List 19 | ) 20 | 21 | data class ResponseHolidayInfo( 22 | val response: HolidayResponse 23 | ) 24 | 25 | data class HolidayResponse( 26 | val body: HolidayBody 27 | ) 28 | 29 | data class HolidayBody( 30 | val items: HolidayItems 31 | ) 32 | 33 | data class HolidayItems( 34 | val item: Item 35 | ) 36 | 37 | 38 | data class Item( 39 | val dateName: String, 40 | @SerializedName("locdate") 41 | val localDate: Int 42 | ) 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/entity/Holiday.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import java.time.LocalDate 6 | 7 | @Entity 8 | data class Holiday( 9 | @PrimaryKey(autoGenerate = true) val id: Long = 0, 10 | val name: String, 11 | val date: LocalDate, 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/local/HolidayDao.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.local 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy.REPLACE 6 | import androidx.room.Query 7 | import com.drunkenboys.calendarun.data.holiday.entity.Holiday 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface HolidayDao { 12 | 13 | @Insert(onConflict = REPLACE) 14 | suspend fun insertHoliday(holiday: Holiday) 15 | 16 | @Query("SELECT * FROM `Holiday`") 17 | fun fetchAllHoliday(): Flow> 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/local/HolidayLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.local 2 | 3 | import com.drunkenboys.calendarun.data.holiday.entity.Holiday 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface HolidayLocalDataSource { 7 | 8 | suspend fun insertHoliday(holiday: Holiday) 9 | 10 | fun fetchAllHoliday(): Flow> 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/local/HolidayLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.local 2 | 3 | import com.drunkenboys.calendarun.data.holiday.entity.Holiday 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.withContext 7 | import javax.inject.Inject 8 | 9 | class HolidayLocalDataSourceImpl @Inject constructor( 10 | private val holidayDao: HolidayDao, 11 | private val dispatcher: CoroutineDispatcher 12 | ) : HolidayLocalDataSource { 13 | 14 | override suspend fun insertHoliday(holiday: Holiday) = withContext(dispatcher) { 15 | holidayDao.insertHoliday(holiday) 16 | } 17 | 18 | override fun fetchAllHoliday(): Flow> = holidayDao.fetchAllHoliday() 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/remote/HolidayRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.remote 2 | 3 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayInfo 4 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayListInfo 5 | 6 | interface HolidayRemoteDataSource { 7 | 8 | suspend fun fetchHolidayListOnYear(year: String, pageNo: Int): ResponseHolidayListInfo 9 | 10 | suspend fun fetchHolidayListOnMonth(year: String, month: String): ResponseHolidayListInfo 11 | 12 | suspend fun fetchHolidayOnYear(year: String, pageNo: Int): ResponseHolidayInfo 13 | 14 | suspend fun fetchHolidayOnMonth(year: String, month: String): ResponseHolidayInfo 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/remote/HolidayRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.remote 2 | 3 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayInfo 4 | import javax.inject.Inject 5 | 6 | class HolidayRemoteDataSourceImpl @Inject constructor( 7 | private val holidayRemoteService: HolidayRemoteService 8 | ) : HolidayRemoteDataSource { 9 | 10 | override suspend fun fetchHolidayListOnYear(year: String, pageNo: Int) = 11 | holidayRemoteService.fetchHolidayListOnYear(year, pageNo) 12 | 13 | override suspend fun fetchHolidayListOnMonth(year: String, month: String) = 14 | holidayRemoteService.fetchHolidayListOnMonth(year, month) 15 | 16 | override suspend fun fetchHolidayOnYear(year: String, pageNo: Int): ResponseHolidayInfo = 17 | holidayRemoteService.fetchHolidayOnYear(year, pageNo) 18 | 19 | override suspend fun fetchHolidayOnMonth(year: String, month: String): ResponseHolidayInfo = 20 | holidayRemoteService.fetchHolidayOnMonth(year, month) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/remote/HolidayRemoteService.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.remote 2 | 3 | import com.drunkenboys.calendarun.BuildConfig 4 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayInfo 5 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayListInfo 6 | import retrofit2.http.GET 7 | import retrofit2.http.Query 8 | 9 | interface HolidayRemoteService { 10 | 11 | @GET("/B090041/openapi/service/SpcdeInfoService/getRestDeInfo") 12 | suspend fun fetchHolidayListOnYear( 13 | @Query("solYear") year: String, 14 | @Query("pageNo") pageNo: Int, 15 | @Query("_type") type: String = "json", 16 | @Query("ServiceKey") serviceKey: String = BuildConfig.HOLIDAY_API_KEY 17 | ): ResponseHolidayListInfo 18 | 19 | @GET("/B090041/openapi/service/SpcdeInfoService/getRestDeInfo") 20 | suspend fun fetchHolidayListOnMonth( 21 | @Query("solYear") year: String, 22 | @Query("solMonth") month: String, 23 | @Query("_type") type: String = "json", 24 | @Query("ServiceKey") serviceKey: String = BuildConfig.HOLIDAY_API_KEY 25 | ): ResponseHolidayListInfo 26 | 27 | @GET("/B090041/openapi/service/SpcdeInfoService/getRestDeInfo") 28 | suspend fun fetchHolidayOnYear( 29 | @Query("solYear") year: String, 30 | @Query("pageNo") pageNo: Int, 31 | @Query("_type") type: String = "json", 32 | @Query("ServiceKey") serviceKey: String = BuildConfig.HOLIDAY_API_KEY 33 | ): ResponseHolidayInfo 34 | 35 | @GET("/B090041/openapi/service/SpcdeInfoService/getRestDeInfo") 36 | suspend fun fetchHolidayOnMonth( 37 | @Query("solYear") year: String, 38 | @Query("solMonth") month: String, 39 | @Query("_type") type: String = "json", 40 | @Query("ServiceKey") serviceKey: String = BuildConfig.HOLIDAY_API_KEY 41 | ): ResponseHolidayInfo 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/repository/HolidayRepository.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.repository 2 | 3 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayInfo 4 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayListInfo 5 | import com.drunkenboys.calendarun.data.holiday.entity.Holiday 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface HolidayRepository { 9 | 10 | suspend fun insertHoliday(holiday: Holiday) 11 | 12 | fun fetchAllHoliday(): Flow> 13 | 14 | suspend fun fetchHolidayListOnYear(year: String, pageNo: Int): ResponseHolidayListInfo 15 | 16 | suspend fun fetchHolidayListOnMonth(year: String, month: String): ResponseHolidayListInfo 17 | 18 | suspend fun fetchHolidayOnYear(year: String, pageNo: Int): ResponseHolidayInfo 19 | 20 | suspend fun fetchHolidayOnMonth(year: String, month: String): ResponseHolidayInfo 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/holiday/repository/HolidayRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.holiday.repository 2 | 3 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayInfo 4 | import com.drunkenboys.calendarun.data.holiday.ResponseHolidayListInfo 5 | import com.drunkenboys.calendarun.data.holiday.entity.Holiday 6 | import com.drunkenboys.calendarun.data.holiday.local.HolidayLocalDataSource 7 | import com.drunkenboys.calendarun.data.holiday.remote.HolidayRemoteDataSource 8 | import kotlinx.coroutines.flow.Flow 9 | import javax.inject.Inject 10 | 11 | class HolidayRepositoryImpl @Inject constructor( 12 | private val holidayLocalDataSource: HolidayLocalDataSource, 13 | private val holidayRemoteDataSource: HolidayRemoteDataSource 14 | ) : HolidayRepository { 15 | 16 | override suspend fun insertHoliday(holiday: Holiday) { 17 | holidayLocalDataSource.insertHoliday(holiday) 18 | } 19 | 20 | override fun fetchAllHoliday(): Flow> = 21 | holidayLocalDataSource.fetchAllHoliday() 22 | 23 | override suspend fun fetchHolidayListOnYear(year: String, pageNo: Int): ResponseHolidayListInfo = 24 | holidayRemoteDataSource.fetchHolidayListOnYear(year, pageNo) 25 | 26 | override suspend fun fetchHolidayListOnMonth(year: String, month: String): ResponseHolidayListInfo = 27 | holidayRemoteDataSource.fetchHolidayListOnMonth(year, month) 28 | 29 | override suspend fun fetchHolidayOnYear(year: String, pageNo: Int): ResponseHolidayInfo = 30 | holidayRemoteDataSource.fetchHolidayOnYear(year, pageNo) 31 | 32 | override suspend fun fetchHolidayOnMonth(year: String, month: String): ResponseHolidayInfo = 33 | holidayRemoteDataSource.fetchHolidayOnMonth(year, month) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/room/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.room 2 | 3 | import androidx.room.TypeConverter 4 | import java.time.Instant 5 | import java.time.LocalDate 6 | import java.time.LocalDateTime 7 | import java.time.ZoneOffset 8 | 9 | class Converters { 10 | 11 | @TypeConverter 12 | fun fromDatestamp(value: Long): LocalDate = LocalDate.ofEpochDay(value) 13 | 14 | @TypeConverter 15 | fun dateToTimestamp(date: LocalDate): Long = date.toEpochDay() 16 | 17 | @TypeConverter 18 | fun fromDateTimestamp(value: Long): LocalDateTime = LocalDateTime.ofEpochSecond(value, 0, zoneOffset) 19 | 20 | @TypeConverter 21 | fun dateToDateTimestamp(time: LocalDateTime): Long = time.toEpochSecond(zoneOffset) 22 | 23 | companion object { 24 | private val zoneOffset = ZoneOffset.systemDefault().rules.getOffset(Instant.now()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/schedule/entity/Schedule.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.schedule.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.ForeignKey 5 | import androidx.room.PrimaryKey 6 | import com.drunkenboys.calendarun.data.calendar.entity.Calendar 7 | import com.drunkenboys.calendarun.util.defaultZoneOffset 8 | import java.time.LocalDateTime 9 | 10 | @Entity( 11 | tableName = "Schedule", 12 | foreignKeys = [ 13 | ForeignKey( 14 | entity = Calendar::class, 15 | parentColumns = ["id"], 16 | childColumns = ["calendarId"], 17 | onDelete = ForeignKey.CASCADE 18 | ) 19 | ] 20 | ) 21 | data class Schedule( 22 | @PrimaryKey(autoGenerate = true) val id: Long = 0, 23 | val calendarId: Long, 24 | val name: String, 25 | val startDate: LocalDateTime, 26 | val endDate: LocalDateTime, 27 | val notificationType: NotificationType, 28 | val memo: String, 29 | val color: Int 30 | ) { 31 | 32 | fun notificationDateTimeMillis(): Long { 33 | return when (notificationType) { 34 | NotificationType.NONE -> return 0 35 | NotificationType.TEN_MINUTES_AGO -> startDate.minusMinutes(10).toEpochSecond(defaultZoneOffset) 36 | NotificationType.A_HOUR_AGO -> startDate.minusHours(1).toEpochSecond(defaultZoneOffset) 37 | NotificationType.A_DAY_AGO -> startDate.minusDays(1).toEpochSecond(defaultZoneOffset) 38 | } * 1000 39 | } 40 | 41 | enum class NotificationType { 42 | NONE, 43 | TEN_MINUTES_AGO, 44 | A_HOUR_AGO, 45 | A_DAY_AGO 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/schedule/local/ScheduleDao.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.schedule.local 2 | 3 | import androidx.room.* 4 | import com.drunkenboys.calendarun.data.schedule.entity.Schedule 5 | import kotlinx.coroutines.flow.Flow 6 | import java.time.LocalDateTime 7 | 8 | @Dao 9 | interface ScheduleDao { 10 | 11 | @Insert 12 | suspend fun insertSchedule(schedule: Schedule): Long 13 | 14 | @Query("SELECT * FROM `schedule`") 15 | suspend fun fetchAllSchedule(): List 16 | 17 | @Query("SELECT * FROM `schedule` WHERE id == :id") 18 | suspend fun fetchSchedule(id: Long): Schedule 19 | 20 | @Query("SELECT * FROM `schedule` WHERE calendarId == :calendarId") 21 | fun fetchCalendarSchedules(calendarId: Long): Flow> 22 | 23 | @Update 24 | suspend fun updateSchedule(schedule: Schedule) 25 | 26 | @Delete 27 | suspend fun deleteSchedule(schedule: Schedule) 28 | 29 | @Query("SELECT * FROM `schedule` WHERE startDate > :time AND `name` LIKE '%' || :word || '%' ORDER BY startDate ASC LIMIT $SCHEDULE_PAGING_SIZE") 30 | suspend fun fetchMatchedScheduleAfter(word: String, time: Long): List 31 | 32 | @Query("SELECT * FROM (SELECT * FROM `schedule` WHERE startDate < :time AND `name` LIKE '%' || :word || '%' ORDER BY startDate DESC LIMIT $SCHEDULE_PAGING_SIZE) A ORDER BY A.startDate ASC") 33 | suspend fun fetchMatchedScheduleBefore(word: String, time: Long): List 34 | 35 | @Query("SELECT * FROM `schedule` WHERE startDate <= :endOfDate AND endDate >= :startOfDate ORDER BY startDate ASC") 36 | fun fetchDateSchedule(startOfDate: LocalDateTime, endOfDate: LocalDateTime): Flow> 37 | 38 | companion object { 39 | 40 | const val SCHEDULE_PAGING_SIZE = 30 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/schedule/local/ScheduleLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.schedule.local 2 | 3 | import com.drunkenboys.calendarun.data.schedule.entity.Schedule 4 | import kotlinx.coroutines.flow.Flow 5 | import java.time.LocalDateTime 6 | 7 | interface ScheduleLocalDataSource { 8 | 9 | suspend fun insertSchedule(schedule: Schedule): Long 10 | 11 | suspend fun fetchAllSchedule(): List 12 | 13 | suspend fun fetchSchedule(id: Long): Schedule 14 | 15 | fun fetchCalendarSchedules(calendarId: Long): Flow> 16 | 17 | suspend fun updateSchedule(schedule: Schedule) 18 | 19 | suspend fun deleteSchedule(schedule: Schedule) 20 | 21 | suspend fun fetchMatchedScheduleAfter(word: String, time: Long): List 22 | 23 | suspend fun fetchMatchedScheduleBefore(word: String, time: Long): List 24 | 25 | fun fetchDateSchedule(startOfDate: LocalDateTime, endOfDate: LocalDateTime): Flow> 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/schedule/local/ScheduleLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.schedule.local 2 | 3 | import com.drunkenboys.calendarun.data.schedule.entity.Schedule 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import java.time.LocalDateTime 8 | import javax.inject.Inject 9 | 10 | class ScheduleLocalDataSourceImpl @Inject constructor( 11 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO, 12 | private val scheduleDao: ScheduleDao 13 | ) : ScheduleLocalDataSource { 14 | 15 | override suspend fun insertSchedule(schedule: Schedule) = withContext(dispatcher) { 16 | scheduleDao.insertSchedule(schedule) 17 | } 18 | 19 | override suspend fun fetchAllSchedule() = withContext(dispatcher) { 20 | scheduleDao.fetchAllSchedule() 21 | } 22 | 23 | override fun fetchCalendarSchedules(calendarId: Long) = scheduleDao.fetchCalendarSchedules(calendarId) 24 | 25 | override suspend fun fetchSchedule(id: Long) = withContext(dispatcher) { 26 | scheduleDao.fetchSchedule(id) 27 | } 28 | 29 | override suspend fun updateSchedule(schedule: Schedule) = withContext(dispatcher) { 30 | scheduleDao.updateSchedule(schedule) 31 | } 32 | 33 | override suspend fun deleteSchedule(schedule: Schedule) = withContext(dispatcher) { 34 | scheduleDao.deleteSchedule(schedule) 35 | } 36 | 37 | override suspend fun fetchMatchedScheduleAfter(word: String, time: Long) = withContext(dispatcher) { 38 | scheduleDao.fetchMatchedScheduleAfter(word, time) 39 | } 40 | 41 | override suspend fun fetchMatchedScheduleBefore(word: String, time: Long) = withContext(dispatcher) { 42 | scheduleDao.fetchMatchedScheduleBefore(word, time) 43 | } 44 | 45 | override fun fetchDateSchedule(startOfDate: LocalDateTime, endOfDate: LocalDateTime) = 46 | scheduleDao.fetchDateSchedule(startOfDate, endOfDate) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/slice/entity/Slice.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.slice.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.ForeignKey 5 | import androidx.room.PrimaryKey 6 | import com.drunkenboys.calendarun.data.calendar.entity.Calendar 7 | import java.time.LocalDate 8 | 9 | @Entity( 10 | tableName = "Slice", 11 | foreignKeys = [ 12 | ForeignKey( 13 | entity = Calendar::class, 14 | parentColumns = ["id"], 15 | childColumns = ["calendarId"], 16 | onDelete = ForeignKey.CASCADE, 17 | onUpdate = ForeignKey.CASCADE 18 | ) 19 | ] 20 | ) 21 | data class Slice( 22 | @PrimaryKey(autoGenerate = true) val id: Long = 0, 23 | val calendarId: Long, 24 | val name: String, 25 | val startDate: LocalDate, 26 | val endDate: LocalDate 27 | ) 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/slice/local/SliceDao.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.slice.local 2 | 3 | import androidx.room.* 4 | import com.drunkenboys.calendarun.data.slice.entity.Slice 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | @Dao 8 | interface SliceDao { 9 | 10 | @Insert 11 | suspend fun insertSlice(slice: Slice): Long 12 | 13 | @Query("SELECT * FROM `Slice`") 14 | suspend fun fetchAllSlice(): List 15 | 16 | @Query("SELECT * FROM `Slice` WHERE id == :id") 17 | suspend fun fetchSlice(id: Long): Slice 18 | 19 | @Query("SELECT * FROM `Slice` WHERE calendarId == :calendarId ORDER BY startDate ASC") 20 | fun fetchCalendarSliceList(calendarId: Long): Flow> 21 | 22 | @Update 23 | suspend fun updateSlice(slice: Slice) 24 | 25 | @Query("DELETE FROM `Slice` WHERE calendarId == :calendarId") 26 | suspend fun deleteSliceList(calendarId: Long) 27 | 28 | @Delete 29 | suspend fun deleteSlice(slice: Slice) 30 | 31 | @Query("DELETE FROM `Slice` WHERE id == :id") 32 | suspend fun deleteSliceById(id: Long) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/slice/local/SliceLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.slice.local 2 | 3 | import com.drunkenboys.calendarun.data.slice.entity.Slice 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface SliceLocalDataSource { 7 | 8 | suspend fun insertSlice(slice: Slice): Long 9 | 10 | suspend fun fetchAllSlice(): List 11 | 12 | suspend fun fetchSlice(id: Long): Slice 13 | 14 | fun fetchCalendarSliceList(calendarId: Long): Flow> 15 | 16 | suspend fun updateSlice(slice: Slice) 17 | 18 | suspend fun deleteSliceList(calendarId: Long) 19 | 20 | suspend fun deleteSlice(slice: Slice) 21 | 22 | suspend fun deleteSliceById(id: Long) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/data/slice/local/SliceLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.slice.local 2 | 3 | import com.drunkenboys.calendarun.data.slice.entity.Slice 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import javax.inject.Inject 8 | 9 | class SliceLocalDataSourceImpl @Inject constructor( 10 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO, 11 | private val sliceDao: SliceDao 12 | ) : SliceLocalDataSource { 13 | 14 | override suspend fun insertSlice(slice: Slice) = withContext(dispatcher) { 15 | sliceDao.insertSlice(slice) 16 | } 17 | 18 | override suspend fun fetchAllSlice() = withContext(dispatcher) { 19 | sliceDao.fetchAllSlice() 20 | } 21 | 22 | override suspend fun fetchSlice(id: Long) = withContext(dispatcher) { 23 | sliceDao.fetchSlice(id) 24 | } 25 | 26 | override fun fetchCalendarSliceList(calendarId: Long) = sliceDao.fetchCalendarSliceList(calendarId) 27 | 28 | override suspend fun updateSlice(slice: Slice) { 29 | sliceDao.updateSlice(slice) 30 | } 31 | 32 | override suspend fun deleteSliceList(calendarId: Long) { 33 | sliceDao.deleteSliceList(calendarId) 34 | } 35 | 36 | override suspend fun deleteSlice(slice: Slice) { 37 | sliceDao.deleteSlice(slice) 38 | } 39 | 40 | override suspend fun deleteSliceById(id: Long) { 41 | sliceDao.deleteSliceById(id) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/di/CoroutineModule.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.Dispatchers 8 | 9 | @InstallIn(SingletonComponent::class) 10 | @Module 11 | object CoroutineModule { 12 | 13 | @Provides 14 | fun provideIODispatcher() = Dispatchers.IO 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/di/DataSourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.di 2 | 3 | import com.drunkenboys.calendarun.data.calendar.local.CalendarLocalDataSource 4 | import com.drunkenboys.calendarun.data.calendar.local.CalendarLocalDataSourceImpl 5 | import com.drunkenboys.calendarun.data.calendartheme.local.CalendarThemeLocalDataSource 6 | import com.drunkenboys.calendarun.data.calendartheme.local.CalendarThemeLocalDataSourceImpl 7 | import com.drunkenboys.calendarun.data.holiday.local.HolidayLocalDataSource 8 | import com.drunkenboys.calendarun.data.holiday.local.HolidayLocalDataSourceImpl 9 | import com.drunkenboys.calendarun.data.holiday.remote.HolidayRemoteDataSource 10 | import com.drunkenboys.calendarun.data.holiday.remote.HolidayRemoteDataSourceImpl 11 | import com.drunkenboys.calendarun.data.schedule.local.ScheduleLocalDataSource 12 | import com.drunkenboys.calendarun.data.schedule.local.ScheduleLocalDataSourceImpl 13 | import com.drunkenboys.calendarun.data.slice.local.SliceLocalDataSource 14 | import com.drunkenboys.calendarun.data.slice.local.SliceLocalDataSourceImpl 15 | import dagger.Binds 16 | import dagger.Module 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | 20 | @InstallIn(SingletonComponent::class) 21 | @Module 22 | abstract class DataSourceModule { 23 | 24 | @Binds 25 | abstract fun bindCalendarDataSource(dataSource: CalendarLocalDataSourceImpl): CalendarLocalDataSource 26 | 27 | @Binds 28 | abstract fun bindSliceDataSource(dataSource: SliceLocalDataSourceImpl): SliceLocalDataSource 29 | 30 | @Binds 31 | abstract fun bindScheduleDataSource(dataSource: ScheduleLocalDataSourceImpl): ScheduleLocalDataSource 32 | 33 | @Binds 34 | abstract fun bindCalendarThemeDataSource(dataSource: CalendarThemeLocalDataSourceImpl): CalendarThemeLocalDataSource 35 | 36 | @Binds 37 | abstract fun bindHolidayLocalDataSource(dataSource: HolidayLocalDataSourceImpl): HolidayLocalDataSource 38 | 39 | @Binds 40 | abstract fun bindHolidayRemoteDataSource(dataSource: HolidayRemoteDataSourceImpl): HolidayRemoteDataSource 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/di/HolidayRetrofitServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.di 2 | 3 | import com.drunkenboys.calendarun.data.holiday.remote.HolidayRemoteService 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import retrofit2.Retrofit 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object HolidayRetrofitServiceModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun provideHolidayService(retrofit: Retrofit): HolidayRemoteService = 18 | retrofit.create(HolidayRemoteService::class.java) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/di/LocalDatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.drunkenboys.calendarun.data.room.Database 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 | import javax.inject.Singleton 12 | 13 | @InstallIn(SingletonComponent::class) 14 | @Module 15 | object LocalDatabaseModule { 16 | 17 | @Provides 18 | @Singleton 19 | fun provideDatabase(@ApplicationContext appContext: Context) = Room.databaseBuilder( 20 | appContext, 21 | Database::class.java, 22 | "AppDatabase.db" 23 | ) 24 | .createFromAsset("default.db") 25 | .addMigrations(Database.MIGRATION_2_3, Database.MIGRATION_3_4, Database.MIGRATION_4_5, Database.MIGRATION_5_6) 26 | .build() 27 | 28 | @Provides 29 | fun provideCalendarDao(database: Database) = database.calendarDao() 30 | 31 | @Provides 32 | fun provideSliceDao(database: Database) = database.sliceDao() 33 | 34 | @Provides 35 | fun provideScheduleDao(database: Database) = database.scheduleDao() 36 | 37 | @Provides 38 | fun provideCalendarThemeDao(database: Database) = database.calendarThemeDao() 39 | 40 | @Provides 41 | fun provideHolidayDao(database: Database) = database.holidayDao() 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.di 2 | 3 | import com.drunkenboys.calendarun.data.holiday.repository.HolidayRepository 4 | import com.drunkenboys.calendarun.data.holiday.repository.HolidayRepositoryImpl 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | 10 | @InstallIn(SingletonComponent::class) 11 | @Module 12 | abstract class RepositoryModule { 13 | 14 | @Binds 15 | abstract fun bindHolidayRepository(repository: HolidayRepositoryImpl): HolidayRepository 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/di/RetrofitModule.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.di 2 | 3 | import com.google.gson.GsonBuilder 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import retrofit2.Retrofit 9 | import retrofit2.converter.gson.GsonConverterFactory 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object RetrofitModule { 15 | 16 | private const val HOLIDAY_URL = "http://apis.data.go.kr" 17 | 18 | private val gson = GsonBuilder() 19 | .setLenient() 20 | .create() 21 | 22 | @Provides 23 | @Singleton 24 | fun provideHolidayRetrofit(): Retrofit = 25 | Retrofit.Builder() 26 | .baseUrl(HOLIDAY_URL) 27 | .addConverterFactory(GsonConverterFactory.create(gson)) 28 | .build() 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/receiver/BootReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.receiver 2 | 3 | import android.app.AlarmManager 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import androidx.core.content.getSystemService 8 | import com.drunkenboys.calendarun.data.calendar.local.CalendarLocalDataSource 9 | import com.drunkenboys.calendarun.data.schedule.entity.Schedule 10 | import com.drunkenboys.calendarun.data.schedule.local.ScheduleLocalDataSource 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Job 14 | import kotlinx.coroutines.flow.firstOrNull 15 | import kotlinx.coroutines.launch 16 | import java.time.LocalDateTime 17 | import javax.inject.Inject 18 | 19 | @AndroidEntryPoint 20 | class BootReceiver : BroadcastReceiver() { 21 | 22 | @Inject lateinit var calendarDataSource: CalendarLocalDataSource 23 | @Inject lateinit var scheduleDataSource: ScheduleLocalDataSource 24 | 25 | private val job = Job() 26 | private val coroutineScope = CoroutineScope(job) 27 | 28 | private val today = LocalDateTime.now() 29 | 30 | private lateinit var alarmManager: AlarmManager 31 | 32 | override fun onReceive(context: Context, intent: Intent) { 33 | if (intent.action == Intent.ACTION_BOOT_COMPLETED) { 34 | alarmManager = context.getSystemService() ?: return 35 | setNotifications(context) 36 | } else { 37 | return 38 | } 39 | } 40 | 41 | private fun setNotifications(context: Context) { 42 | coroutineScope.launch { 43 | val calendarMap = calendarDataSource.fetchAllCalendar().firstOrNull() 44 | ?.associate { it.id to it.name } ?: return@launch 45 | 46 | scheduleDataSource.fetchAllSchedule() 47 | .forEach { schedule -> 48 | setAlarmIfScheduleInFuture( 49 | schedule, 50 | context, 51 | calendarMap.getOrDefault(schedule.id, "null") 52 | ) 53 | } 54 | } 55 | } 56 | 57 | private fun setAlarmIfScheduleInFuture(schedule: Schedule, context: Context, calendarName: String) { 58 | if (schedule.startDate > today) { 59 | val triggerAtMillis = schedule.notificationDateTimeMillis() 60 | alarmManager.set( 61 | AlarmManager.RTC_WAKEUP, 62 | triggerAtMillis, 63 | ScheduleAlarmReceiver.createPendingIntent(context, schedule, calendarName) 64 | ) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/appinfo/AppInfoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.appinfo 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.navigation.ui.setupWithNavController 6 | import com.drunkenboys.calendarun.R 7 | import com.drunkenboys.calendarun.databinding.FragmentAppInfoBinding 8 | import com.drunkenboys.calendarun.ui.base.BaseFragment 9 | 10 | class AppInfoFragment : BaseFragment(R.layout.fragment_app_info) { 11 | 12 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 13 | super.onViewCreated(view, savedInstanceState) 14 | 15 | setupToolbar() 16 | } 17 | 18 | private fun setupToolbar() { 19 | binding.toolbarAppInfo.setupWithNavController(navController) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/appwidget/CalendaRunRemoteViewsFactory.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.appwidget 2 | 3 | import android.content.Context 4 | import android.widget.RemoteViews 5 | import android.widget.RemoteViewsService 6 | import com.drunkenboys.calendarun.R 7 | import com.drunkenboys.calendarun.data.schedule.local.ScheduleLocalDataSource 8 | import com.drunkenboys.calendarun.util.getEndOfDate 9 | import com.drunkenboys.calendarun.util.getStartOfDate 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.SharingStarted 13 | import kotlinx.coroutines.flow.collectLatest 14 | import kotlinx.coroutines.flow.stateIn 15 | import kotlinx.coroutines.launch 16 | 17 | class CalendaRunRemoteViewsFactory constructor( 18 | private val context: Context, 19 | private val scheduleDataSource: ScheduleLocalDataSource 20 | ) : RemoteViewsService.RemoteViewsFactory { 21 | 22 | private val coroutineScope = CoroutineScope(Dispatchers.IO) 23 | private var scheduleList = scheduleDataSource.fetchDateSchedule( 24 | getStartOfDate(), getEndOfDate() 25 | ).stateIn(coroutineScope, SharingStarted.Eagerly, emptyList()) 26 | 27 | override fun onCreate() { 28 | coroutineScope.launch { 29 | scheduleList.collectLatest { 30 | CalendaRunAppWidget.sendUpdateBroadcast(context) 31 | } 32 | } 33 | } 34 | 35 | override fun onDataSetChanged() {} 36 | 37 | override fun onDestroy() {} 38 | 39 | override fun getCount() = scheduleList.value.size 40 | 41 | override fun getViewAt(position: Int): RemoteViews { 42 | val widget = RemoteViews(context.packageName, R.layout.item_app_widget_schedule).apply { 43 | setTextViewText(R.id.tv_appWidget_schedule_name, scheduleList.value[position].name) 44 | setTextViewText(R.id.tv_appWidget_schedule_memo, scheduleList.value[position].memo) 45 | } 46 | 47 | return widget 48 | } 49 | 50 | override fun getLoadingView(): RemoteViews? = null 51 | 52 | override fun getViewTypeCount() = 1 53 | 54 | override fun getItemId(position: Int) = 0L 55 | 56 | override fun hasStableIds() = false 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/appwidget/CalendaRunRemoteViewsService.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.appwidget 2 | 3 | import android.content.Intent 4 | import android.widget.RemoteViewsService 5 | import com.drunkenboys.calendarun.data.schedule.local.ScheduleLocalDataSource 6 | import dagger.hilt.android.AndroidEntryPoint 7 | import javax.inject.Inject 8 | 9 | @AndroidEntryPoint 10 | class CalendaRunRemoteViewsService : RemoteViewsService() { 11 | 12 | @Inject 13 | lateinit var scheduleDataSource: ScheduleLocalDataSource 14 | 15 | override fun onGetViewFactory(p0: Intent?) = CalendaRunRemoteViewsFactory(this.applicationContext, scheduleDataSource) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.databinding.ViewDataBinding 8 | import androidx.viewbinding.ViewBinding 9 | 10 | // for view binding 11 | open class BaseViewActivity(private val inflate: ((LayoutInflater) -> T)?) : AppCompatActivity() { 12 | 13 | lateinit var binding: T 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | inflate?.let { 18 | binding = it.invoke(layoutInflater) 19 | setContentView(binding.root) 20 | } 21 | } 22 | } 23 | 24 | // for data binding 25 | open class BaseActivity(private val layoutId: Int) : BaseViewActivity(null) { 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | binding = DataBindingUtil.setContentView(this, layoutId) 30 | binding.lifecycleOwner = this 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.base 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | import androidx.fragment.app.Fragment 10 | import androidx.navigation.fragment.findNavController 11 | import androidx.viewbinding.ViewBinding 12 | 13 | // for view binding 14 | open class BaseViewFragment(private val inflate: ((LayoutInflater, ViewGroup?, Boolean) -> T)? = null) : Fragment() { 15 | 16 | var _binding: T? = null 17 | val binding get() = _binding ?: throw IllegalStateException(ERROR_BINDING_INITIALIZE) 18 | 19 | val navController by lazy { findNavController() } 20 | 21 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 22 | return inflate?.let { 23 | _binding = it.invoke(inflater, container, false) 24 | binding.root 25 | } ?: super.onCreateView(inflater, container, savedInstanceState) 26 | } 27 | 28 | override fun onDestroyView() { 29 | super.onDestroyView() 30 | _binding = null 31 | } 32 | 33 | companion object { 34 | 35 | private const val ERROR_BINDING_INITIALIZE = "binding 초기화 실패" 36 | } 37 | } 38 | 39 | // for data binding 40 | open class BaseFragment(private val layoutId: Int) : BaseViewFragment() { 41 | 42 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 43 | _binding = DataBindingUtil.inflate(inflater, layoutId, container, false) 44 | return binding.root 45 | } 46 | 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 48 | binding.lifecycleOwner = viewLifecycleOwner 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/base/BaseViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.base 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.databinding.ViewDataBinding 7 | import androidx.recyclerview.widget.RecyclerView 8 | 9 | class BaseViewHolder(val binding: VDB) : RecyclerView.ViewHolder(binding.root) { 10 | 11 | constructor(parent: ViewGroup, layoutId: Int) : this( 12 | DataBindingUtil.inflate(LayoutInflater.from(parent.context), layoutId, parent, false) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/dayschedule/DayScheduleAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.dayschedule 2 | 3 | import android.view.ViewGroup 4 | import androidx.recyclerview.widget.ListAdapter 5 | import com.drunkenboys.calendarun.R 6 | import com.drunkenboys.calendarun.databinding.ItemScheduleBinding 7 | import com.drunkenboys.calendarun.ui.base.BaseViewHolder 8 | import com.drunkenboys.calendarun.ui.searchschedule.model.ScheduleItem 9 | import java.time.LocalDate 10 | import java.time.LocalDateTime 11 | import java.time.LocalTime 12 | 13 | class DayScheduleAdapter(private val localDate: LocalDate) : 14 | ListAdapter>(ScheduleItem.diffUtil) { 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = 17 | BaseViewHolder(parent, R.layout.item_schedule) 18 | 19 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { 20 | holder.binding.item = currentList[position] 21 | holder.binding.time = LocalDateTime.of(localDate, LocalTime.MIN) 22 | holder.binding.executePendingBindings() 23 | holder.binding.root.post { 24 | val textLine = holder.binding.tvScheduleTime.lineCount 25 | if (textLine == 2) { 26 | val text = holder.binding.tvScheduleTime.text 27 | holder.binding.tvScheduleTime.text = text.replace("~ ".toRegex(), "~\n") 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/dayschedule/DayScheduleViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.dayschedule 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.drunkenboys.calendarun.KEY_CALENDAR_ID 7 | import com.drunkenboys.calendarun.KEY_LOCAL_DATE 8 | import com.drunkenboys.calendarun.data.schedule.entity.Schedule 9 | import com.drunkenboys.calendarun.data.schedule.local.ScheduleLocalDataSource 10 | import com.drunkenboys.calendarun.ui.searchschedule.model.ScheduleItem 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.* 13 | import kotlinx.coroutines.launch 14 | import java.time.LocalDate 15 | import java.time.format.DateTimeFormatter 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class DayScheduleViewModel @Inject constructor( 20 | savedStateHandle: SavedStateHandle, 21 | scheduleDataSource: ScheduleLocalDataSource 22 | ) : ViewModel() { 23 | 24 | private val calendarId = savedStateHandle[KEY_CALENDAR_ID] ?: 0L 25 | private val localDate = savedStateHandle.get(KEY_LOCAL_DATE) 26 | ?.let { LocalDate.parse(it) } 27 | 28 | val dateString = localDate?.format(DateTimeFormatter.ofPattern("M월 d일")) 29 | 30 | val listItem = scheduleDataSource.fetchCalendarSchedules(calendarId) 31 | .map { scheduleList -> 32 | scheduleList.filter { localDate in it.startDate.toLocalDate()..it.endDate.toLocalDate() } 33 | .map { schedule -> ScheduleItem(schedule) { emitScheduleClickEvent(schedule) } } 34 | .sortedBy { dateScheduleItem -> dateScheduleItem.schedule.startDate } 35 | }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) 36 | 37 | private val _scheduleClickEvent = MutableSharedFlow() 38 | val scheduleClickEvent: SharedFlow = _scheduleClickEvent 39 | 40 | private fun emitScheduleClickEvent(schedule: Schedule) { 41 | viewModelScope.launch { 42 | _scheduleClickEvent.emit(schedule) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/maincalendar/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.maincalendar 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.app.Dialog 6 | import android.graphics.Color 7 | import android.graphics.drawable.ColorDrawable 8 | import android.os.Bundle 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import androidx.fragment.app.DialogFragment 13 | import com.airbnb.lottie.LottieDrawable 14 | import com.drunkenboys.calendarun.databinding.DialogLoadingBinding 15 | 16 | /* 17 | * @params isWaiting 18 | * tre : 따로 Dismiss()를 호출 할 때까지 무한 반복 19 | * false : 60frame 실행 후 자동 종료 20 | * */ 21 | class LoadingDialog(private var isWaiting: Boolean = false) : DialogFragment() { 22 | 23 | private var _binding: DialogLoadingBinding? = null 24 | private val binding get() = _binding!! 25 | 26 | 27 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 28 | return super.onCreateDialog(savedInstanceState).apply { 29 | setCancelable(false) 30 | setCanceledOnTouchOutside(false) 31 | window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 32 | window?.setDimAmount(0.3f) 33 | } 34 | } 35 | 36 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 37 | _binding = DialogLoadingBinding.inflate(layoutInflater, container, false) 38 | return binding.root 39 | } 40 | 41 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 42 | super.onViewCreated(view, savedInstanceState) 43 | binding.lottieLoading.setAnimation(LOTTIE_ANIMATION_PATH) 44 | binding.lottieLoading.speed = 3.5f 45 | if (!isWaiting) { 46 | binding.lottieLoading.setMaxFrame(60) 47 | } else { 48 | binding.lottieLoading.repeatCount = LottieDrawable.INFINITE 49 | } 50 | binding.lottieLoading.playAnimation() 51 | binding.lottieLoading.addAnimatorListener(object : AnimatorListenerAdapter() { 52 | override fun onAnimationEnd(animation: Animator?) { 53 | if (!isWaiting) { 54 | dismiss() 55 | } 56 | } 57 | }) 58 | } 59 | 60 | override fun onDestroyView() { 61 | super.onDestroyView() 62 | _binding = null 63 | } 64 | 65 | companion object { 66 | private const val LOTTIE_ANIMATION_PATH = "switching_calendar.json" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/managecalendar/DeleteCalendarDialog.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.managecalendar 2 | 3 | import android.app.AlertDialog 4 | import android.os.Bundle 5 | import androidx.fragment.app.DialogFragment 6 | import androidx.navigation.fragment.findNavController 7 | import androidx.navigation.navGraphViewModels 8 | import com.drunkenboys.calendarun.R 9 | import dagger.hilt.android.AndroidEntryPoint 10 | 11 | @AndroidEntryPoint 12 | class DeleteCalendarDialog : DialogFragment() { 13 | 14 | private val manageCalendarViewModel: ManageCalendarViewModel 15 | by navGraphViewModels(R.id.manageCalendarFragment) { defaultViewModelProviderFactory } 16 | 17 | override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = AlertDialog.Builder(requireContext()) 18 | .setTitle(getString(R.string.deleteCalendar_title)) 19 | .setMessage(getString(R.string.deleteCalendar_message)) 20 | .setPositiveButton(getString(R.string.delete)) { _, _ -> 21 | manageCalendarViewModel.emitDeleteCalendarEvent() 22 | findNavController().navigateUp() 23 | } 24 | .setNegativeButton(getString(R.string.cancel)) { _, _ -> } 25 | .create() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/managecalendar/ManageCalendarAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.managecalendar 2 | 3 | import android.view.ViewGroup 4 | import android.widget.CompoundButton 5 | import androidx.recyclerview.widget.ListAdapter 6 | import com.drunkenboys.calendarun.BR 7 | import com.drunkenboys.calendarun.R 8 | import com.drunkenboys.calendarun.databinding.ItemCalendarBinding 9 | import com.drunkenboys.calendarun.ui.base.BaseViewHolder 10 | import com.drunkenboys.calendarun.ui.managecalendar.model.CalendarItem 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | 14 | class ManageCalendarAdapter(val onClick: (CalendarItem) -> Unit) : 15 | ListAdapter>(CalendarItem.diffUtil) { 16 | 17 | private val _checkedCalendarNum = MutableStateFlow(0) 18 | val checkedCalendarNum: StateFlow = _checkedCalendarNum 19 | 20 | val onCheckedChangeListener = CompoundButton.OnCheckedChangeListener { button, isChecked -> 21 | if (isChecked) { 22 | _checkedCalendarNum.value++ 23 | } else { 24 | _checkedCalendarNum.value-- 25 | } 26 | } 27 | 28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = 29 | BaseViewHolder(parent, R.layout.item_calendar) 30 | 31 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { 32 | with(holder.binding) { 33 | setVariable(BR.item, currentList[position]) 34 | setVariable(BR.adapter, this@ManageCalendarAdapter) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/managecalendar/ManageCalendarViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.managecalendar 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.drunkenboys.calendarun.data.calendar.local.CalendarLocalDataSource 6 | import com.drunkenboys.calendarun.ui.managecalendar.model.CalendarItem 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import kotlinx.coroutines.flow.* 9 | import kotlinx.coroutines.launch 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class ManageCalendarViewModel @Inject constructor( 14 | private val calendarLocalDataSource: CalendarLocalDataSource 15 | ) : ViewModel() { 16 | 17 | val calendarItemList = calendarLocalDataSource.fetchCustomCalendar() 18 | .map { calendarList -> 19 | calendarList.map { calendar -> CalendarItem.from(calendar) } 20 | }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) 21 | 22 | private val _deleteCalendarEvent = MutableSharedFlow() 23 | val deleteCalendarEvent: SharedFlow = _deleteCalendarEvent 24 | 25 | private val _openDeleteDialogEvent = MutableSharedFlow() 26 | val openDeleteDialogEvent: SharedFlow = _openDeleteDialogEvent 27 | 28 | fun deleteCalendarItem(currentCalendarItemList: List) { 29 | viewModelScope.launch { 30 | currentCalendarItemList 31 | .filter { calendarItem -> calendarItem.check } 32 | .forEach { calendarItem -> 33 | calendarLocalDataSource.deleteCalendar( 34 | calendarItem.toCalendar() 35 | ) 36 | } 37 | } 38 | } 39 | 40 | fun emitDeleteCalendarEvent() { 41 | viewModelScope.launch { 42 | _deleteCalendarEvent.emit(Unit) 43 | } 44 | } 45 | 46 | fun emitOpenDeleteDialogEvent(id: Int) { 47 | viewModelScope.launch { 48 | _openDeleteDialogEvent.emit(id) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/managecalendar/model/CalendarItem.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.managecalendar.model 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.drunkenboys.calendarun.data.calendar.entity.Calendar 5 | import java.time.LocalDate 6 | 7 | data class CalendarItem( 8 | val id: Long, 9 | val name: String, 10 | val startDate: LocalDate, 11 | val endDate: LocalDate, 12 | var check: Boolean = false 13 | ) { 14 | 15 | fun toCalendar() = Calendar( 16 | id = id, 17 | name = name, 18 | startDate = startDate, 19 | endDate = endDate 20 | ) 21 | 22 | companion object { 23 | 24 | val diffUtil by lazy { 25 | object : DiffUtil.ItemCallback() { 26 | override fun areItemsTheSame(oldItem: CalendarItem, newItem: CalendarItem) = oldItem.id == newItem.id 27 | 28 | override fun areContentsTheSame(oldItem: CalendarItem, newItem: CalendarItem) = oldItem == newItem 29 | } 30 | } 31 | 32 | fun from(calendar: Calendar) = CalendarItem( 33 | id = calendar.id, 34 | name = calendar.name, 35 | startDate = calendar.startDate, 36 | endDate = calendar.endDate 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/savecalendar/SaveCalendarAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.savecalendar 2 | 3 | import android.view.ViewGroup 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.lifecycleScope 6 | import androidx.recyclerview.widget.ListAdapter 7 | import com.drunkenboys.calendarun.BR 8 | import com.drunkenboys.calendarun.R 9 | import com.drunkenboys.calendarun.databinding.ItemSliceBinding 10 | import com.drunkenboys.calendarun.ui.base.BaseViewHolder 11 | import com.drunkenboys.calendarun.ui.savecalendar.model.SliceItem 12 | import com.drunkenboys.calendarun.view.ErrorGuideEditText 13 | import com.drunkenboys.calendarun.view.ErrorGuideTextView 14 | import kotlinx.coroutines.flow.collectLatest 15 | import kotlinx.coroutines.launch 16 | 17 | class SaveCalendarAdapter( 18 | private val viewLifecycleOwner: LifecycleOwner, 19 | val onClick: (SliceItem) -> Unit 20 | ) : ListAdapter>(SliceItem.diffUtil) { 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = 23 | BaseViewHolder(parent, R.layout.item_slice) 24 | 25 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { 26 | with(holder.binding) { 27 | setVariable(BR.item, currentList[position]) 28 | setVariable(BR.adapter, this@SaveCalendarAdapter) 29 | lifecycleOwner = viewLifecycleOwner 30 | 31 | collectNameBlank(currentList[position], holder.binding.etSliceName) 32 | collectStartBlank(currentList[position], holder.binding.tvSliceStartDatePicker) 33 | collectEndBlank(currentList[position], holder.binding.tvSliceEndDatePicker) 34 | } 35 | } 36 | 37 | private fun collectNameBlank(item: SliceItem, view: ErrorGuideEditText) { 38 | viewLifecycleOwner.lifecycleScope.launch { 39 | item.isNameBlank.collectLatest { 40 | view.isError = true 41 | } 42 | 43 | } 44 | } 45 | 46 | private fun collectStartBlank(item: SliceItem, view: ErrorGuideTextView) { 47 | viewLifecycleOwner.lifecycleScope.launch { 48 | item.isStartDateBlank.collectLatest { 49 | view.isError = true 50 | } 51 | } 52 | } 53 | 54 | private fun collectEndBlank(item: SliceItem, view: ErrorGuideTextView) { 55 | viewLifecycleOwner.lifecycleScope.launch { 56 | item.isEndDateBlank.collectLatest { 57 | view.isError = true 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/savecalendar/model/SliceItem.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.savecalendar.model 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import java.time.LocalDate 7 | 8 | data class SliceItem( 9 | val id: Long = 0L, 10 | val name: MutableStateFlow = MutableStateFlow(""), 11 | val startDate: MutableStateFlow = MutableStateFlow(null), 12 | val endDate: MutableStateFlow = MutableStateFlow(null), 13 | var check: Boolean = false, 14 | val isNameBlank: MutableSharedFlow = MutableSharedFlow(), 15 | val isStartDateBlank: MutableSharedFlow = MutableSharedFlow(), 16 | val isEndDateBlank: MutableSharedFlow = MutableSharedFlow() 17 | ) : Comparable { 18 | 19 | override fun compareTo(other: SliceItem): Int = 20 | compareValuesBy(this, other) { slice -> slice.startDate.value } 21 | 22 | companion object { 23 | 24 | val diffUtil by lazy { 25 | object : DiffUtil.ItemCallback() { 26 | override fun areItemsTheSame(oldItem: SliceItem, newItem: SliceItem) = 27 | oldItem.startDate == newItem.startDate 28 | 29 | override fun areContentsTheSame(oldItem: SliceItem, newItem: SliceItem) = 30 | oldItem == newItem 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/saveschedule/DeleteScheduleDialog.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.saveschedule 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AlertDialog 5 | import androidx.fragment.app.DialogFragment 6 | import androidx.navigation.fragment.findNavController 7 | import androidx.navigation.navGraphViewModels 8 | import com.drunkenboys.calendarun.R 9 | import com.google.android.material.color.MaterialColors 10 | import dagger.hilt.android.AndroidEntryPoint 11 | 12 | @AndroidEntryPoint 13 | class DeleteScheduleDialog : DialogFragment() { 14 | 15 | private val saveScheduleViewModel: SaveScheduleViewModel 16 | by navGraphViewModels(R.id.saveScheduleFragment) { defaultViewModelProviderFactory } 17 | 18 | override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = AlertDialog.Builder(requireContext()) 19 | .setTitle(R.string.deleteScheduleDialog_title) 20 | .setMessage(R.string.deleteScheduleDialog_message) 21 | .setPositiveButton(R.string.delete) { _, _ -> 22 | saveScheduleViewModel.deleteSchedule() 23 | findNavController().navigateUp() 24 | } 25 | .setNegativeButton(R.string.cancel) { _, _ -> } 26 | .create() 27 | 28 | override fun onResume() { 29 | super.onResume() 30 | 31 | val dialog = (dialog as? AlertDialog) ?: return 32 | val textColor = MaterialColors.getColor(requireContext(), R.attr.colorOnSurface, "") 33 | dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(textColor) 34 | dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(textColor) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/saveschedule/model/BehaviorType.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.saveschedule.model 2 | 3 | enum class BehaviorType { 4 | INSERT, UPDATE 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/saveschedule/model/DateType.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.saveschedule.model 2 | 3 | enum class DateType { 4 | START, 5 | END 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/searchschedule/SearchScheduleAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.searchschedule 2 | 3 | import android.view.ViewGroup 4 | import androidx.databinding.ViewDataBinding 5 | import androidx.recyclerview.widget.ListAdapter 6 | import com.drunkenboys.calendarun.BR 7 | import com.drunkenboys.calendarun.R 8 | import com.drunkenboys.calendarun.ui.base.BaseViewHolder 9 | import com.drunkenboys.calendarun.ui.searchschedule.model.DateItem 10 | import com.drunkenboys.calendarun.ui.searchschedule.model.DateScheduleItem 11 | import com.drunkenboys.calendarun.ui.searchschedule.model.ScheduleItem 12 | 13 | class SearchScheduleAdapter : ListAdapter>(DateScheduleItem.diffUtil) { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = 16 | BaseViewHolder(parent, viewType) 17 | 18 | override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { 19 | holder.binding.setVariable(BR.item, currentList[position]) 20 | holder.binding.executePendingBindings() 21 | } 22 | 23 | override fun getItemViewType(position: Int) = when (currentList[position]) { 24 | is DateItem -> R.layout.item_date 25 | is ScheduleItem -> R.layout.item_schedule 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/searchschedule/SearchScheduleDivider.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.searchschedule 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Rect 6 | import androidx.recyclerview.widget.DividerItemDecoration 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.drunkenboys.calendarun.util.extensions.dp2px 9 | import kotlin.math.roundToInt 10 | 11 | class SearchScheduleDivider(private val context: Context) : DividerItemDecoration(context, RecyclerView.VERTICAL) { 12 | 13 | private var leftInset = 16f 14 | private var rightInset = 16f 15 | 16 | private val mBounds = Rect() 17 | 18 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 19 | if (parent.layoutManager == null || drawable == null) { 20 | return 21 | } 22 | drawVertical(c, parent) 23 | } 24 | 25 | private fun drawVertical(canvas: Canvas, parent: RecyclerView) { 26 | val drawable = drawable ?: return 27 | 28 | canvas.save() 29 | var left: Int 30 | var right: Int 31 | if (parent.clipToPadding) { 32 | left = parent.paddingLeft 33 | right = parent.width - parent.paddingRight 34 | canvas.clipRect( 35 | left, parent.paddingTop, right, 36 | parent.height - parent.paddingBottom 37 | ) 38 | } else { 39 | left = 0 40 | right = parent.width 41 | } 42 | left += context.dp2px(leftInset).toInt() 43 | right -= context.dp2px(rightInset).toInt() 44 | 45 | val childCount = parent.childCount 46 | for (i in 0 until childCount) { 47 | val child = parent.getChildAt(i) 48 | if (child.tag == "DateItem") { 49 | parent.getDecoratedBoundsWithMargins(child, mBounds) 50 | val bottom = mBounds.top + child.translationY.roundToInt() 51 | val top = bottom - drawable.intrinsicHeight 52 | drawable.setBounds(left, top, right, bottom) 53 | drawable.draw(canvas) 54 | } 55 | } 56 | canvas.restore() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/searchschedule/model/DateScheduleItem.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.searchschedule.model 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.drunkenboys.calendarun.data.schedule.entity.Schedule 5 | import com.drunkenboys.calendarun.util.amPm 6 | import com.drunkenboys.calendarun.util.relativeDateFormat 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.format.DateTimeFormatter 10 | 11 | sealed class DateScheduleItem { 12 | 13 | companion object { 14 | 15 | val diffUtil by lazy { 16 | object : DiffUtil.ItemCallback() { 17 | 18 | override fun areItemsTheSame(oldItem: DateScheduleItem, newItem: DateScheduleItem) = 19 | oldItem.hashCode() == newItem.hashCode() 20 | 21 | override fun areContentsTheSame(oldItem: DateScheduleItem, newItem: DateScheduleItem) = oldItem == newItem 22 | } 23 | } 24 | } 25 | } 26 | 27 | data class DateItem(val date: LocalDate) : DateScheduleItem() { 28 | 29 | override fun toString(): String = date.format(relativeDateFormat(LocalDate.now(), date)) 30 | } 31 | 32 | data class ScheduleItem(val schedule: Schedule, val onClick: () -> Unit) : DateScheduleItem() { 33 | 34 | override fun toString(): String { 35 | val startDateFormat = DateTimeFormatter.ofPattern("${schedule.startDate.amPm} hh:mm") 36 | val endDateFormat = relativeDateFormat(schedule.startDate, schedule.endDate) 37 | 38 | return "${schedule.startDate.format(startDateFormat)} ~ ${schedule.endDate.format(endDateFormat)}" 39 | } 40 | 41 | fun toString(baseDateTime: LocalDateTime?): String { 42 | baseDateTime ?: return toString() 43 | 44 | val startFormat = relativeDateFormat(baseDateTime, schedule.startDate) 45 | val endFormat = relativeDateFormat(baseDateTime, schedule.endDate) 46 | 47 | return "${schedule.startDate.format(startFormat)} ~ ${schedule.endDate.format(endFormat)}" 48 | } 49 | 50 | companion object { 51 | 52 | val diffUtil by lazy { 53 | object : DiffUtil.ItemCallback() { 54 | override fun areItemsTheSame(oldItem: ScheduleItem, newItem: ScheduleItem) = 55 | oldItem.schedule.id == newItem.schedule.id 56 | 57 | override fun areContentsTheSame(oldItem: ScheduleItem, newItem: ScheduleItem) = oldItem == newItem 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/setting/SettingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.setting 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.MutableSharedFlow 6 | import kotlinx.coroutines.flow.SharedFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class SettingViewModel : ViewModel() { 10 | 11 | private val _manageCalendarClickEvent = MutableSharedFlow() 12 | val manageCalendarClickEvent: SharedFlow = _manageCalendarClickEvent 13 | 14 | private val _themeClickEvent = MutableSharedFlow() 15 | val themeClickEvent: SharedFlow = _themeClickEvent 16 | 17 | private val _infoClickEvent = MutableSharedFlow() 18 | val infoClickEvent: SharedFlow = _infoClickEvent 19 | 20 | private val _licenseClickEvent = MutableSharedFlow() 21 | val licenseClickEvent: SharedFlow = _licenseClickEvent 22 | 23 | fun emitManageCalendarClickEvent() { 24 | viewModelScope.launch { 25 | _manageCalendarClickEvent.emit(Unit) 26 | } 27 | } 28 | 29 | fun emitThemeClickEvent() { 30 | viewModelScope.launch { 31 | _themeClickEvent.emit(Unit) 32 | } 33 | } 34 | 35 | fun emitInfoClickEvent() { 36 | viewModelScope.launch { 37 | _infoClickEvent.emit(Unit) 38 | } 39 | } 40 | 41 | fun emitLicenseClickEvent() { 42 | viewModelScope.launch { 43 | _licenseClickEvent.emit(Unit) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/theme/CalendarThemeMapper.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.theme 2 | 3 | import com.drunkenboys.calendarun.data.calendartheme.entity.CalendarTheme 4 | import com.drunkenboys.ckscalendar.data.CalendarDesignObject 5 | 6 | fun CalendarTheme.toCalendarDesignObject() = CalendarDesignObject( 7 | weekDayTextColor = weekDayTextColor, 8 | holidayTextColor = holidayTextColor, 9 | saturdayTextColor = saturdayTextColor, 10 | sundayTextColor = sundayTextColor, 11 | selectedFrameColor = selectedFrameColor, 12 | backgroundColor = backgroundColor, 13 | textSize = textSize, 14 | textAlign = textAlign, 15 | weekSimpleStringSet = languageType.weekSimpleStringSet, 16 | weekFullStringSet = languageType.weekFullStringSet, 17 | visibleScheduleCount = visibleScheduleCount 18 | ) 19 | 20 | fun CalendarDesignObject.toCalendarTheme() = CalendarTheme( 21 | weekDayTextColor = weekDayTextColor, 22 | holidayTextColor = holidayTextColor, 23 | saturdayTextColor = saturdayTextColor, 24 | sundayTextColor = sundayTextColor, 25 | selectedFrameColor = selectedFrameColor, 26 | backgroundColor = backgroundColor, 27 | textSize = textSize, 28 | textAlign = textAlign, 29 | languageType = CalendarTheme.LanguageType.KOREAN, 30 | visibleScheduleCount = visibleScheduleCount 31 | ) 32 | 33 | val CalendarTheme.LanguageType.weekSimpleStringSet: List 34 | get() = when (this) { 35 | CalendarTheme.LanguageType.KOREAN -> listOf("일", "월", "화", "수", "목", "금", "토") 36 | CalendarTheme.LanguageType.ENGLISH -> listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") 37 | } 38 | val CalendarTheme.LanguageType.weekFullStringSet: List 39 | get() = when (this) { 40 | CalendarTheme.LanguageType.KOREAN -> listOf("일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일") 41 | CalendarTheme.LanguageType.ENGLISH -> listOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/theme/ThemeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.theme 2 | 3 | import android.os.Bundle 4 | import android.view.Gravity 5 | import android.view.View 6 | import androidx.navigation.navGraphViewModels 7 | import androidx.navigation.ui.setupWithNavController 8 | import com.drunkenboys.calendarun.R 9 | import com.drunkenboys.calendarun.databinding.FragmentThemeBinding 10 | import com.drunkenboys.calendarun.ui.base.BaseFragment 11 | import com.drunkenboys.calendarun.util.extensions.launchAndRepeatWithViewLifecycle 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.coroutines.flow.collect 14 | import kotlinx.coroutines.launch 15 | 16 | @AndroidEntryPoint 17 | class ThemeFragment : BaseFragment(R.layout.fragment_theme) { 18 | 19 | private val themeViewModel by navGraphViewModels(R.id.themeFragment) { defaultViewModelProviderFactory } 20 | 21 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 22 | super.onViewCreated(view, savedInstanceState) 23 | binding.viewModel = themeViewModel 24 | 25 | setupToolbar() 26 | 27 | launchAndRepeatWithViewLifecycle { 28 | launch { collectTextAlign() } 29 | } 30 | } 31 | 32 | private fun setupToolbar() { 33 | binding.toolbarTheme.setupWithNavController(navController) 34 | binding.toolbarTheme.setOnMenuItemClickListener { menu -> 35 | if (menu.itemId == R.id.menu_theme_reset) { 36 | navigateToThemeResetDialog() 37 | true 38 | } else { 39 | false 40 | } 41 | } 42 | } 43 | 44 | private fun navigateToThemeResetDialog() { 45 | val action = ThemeFragmentDirections.toThemeResetDialog() 46 | navController.navigate(action) 47 | } 48 | 49 | private suspend fun collectTextAlign() { 50 | themeViewModel.textAlign.collect { align -> 51 | when (align) { 52 | Gravity.START -> binding.tvThemeTextAlignContent.setText(R.string.start) 53 | Gravity.CENTER -> binding.tvThemeTextAlignContent.setText(R.string.center) 54 | Gravity.END -> binding.tvThemeTextAlignContent.setText(R.string.end) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/theme/ThemeResetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.theme 2 | 3 | import android.app.AlertDialog 4 | import android.os.Bundle 5 | import androidx.fragment.app.DialogFragment 6 | import androidx.navigation.fragment.findNavController 7 | import androidx.navigation.navGraphViewModels 8 | import com.drunkenboys.calendarun.R 9 | import dagger.hilt.android.AndroidEntryPoint 10 | 11 | @AndroidEntryPoint 12 | class ThemeResetDialog : DialogFragment() { 13 | 14 | private val themeViewModel: ThemeViewModel 15 | by navGraphViewModels(R.id.themeFragment) { defaultViewModelProviderFactory } 16 | 17 | override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog = AlertDialog.Builder(requireContext()) 18 | .setTitle(getString(R.string.resetTheme)) 19 | .setMessage(getString(R.string.resetThemeMessage)) 20 | .setPositiveButton(R.string.reset) { _, _ -> 21 | themeViewModel.resetTheme() 22 | findNavController().navigateUp() 23 | } 24 | .setNegativeButton(R.string.cancel) { _, _ -> } 25 | .create() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/ui/theme/model/ThemeColorType.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.ui.theme.model 2 | 3 | enum class ThemeColorType { 4 | WEEKDAY, 5 | HOLIDAY, 6 | SATURDAY, 7 | SUNDAY, 8 | SELECTED_DAY_STROKE, 9 | DAY_BACKGROUND 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/DateConverter.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util 2 | 3 | import com.drunkenboys.calendarun.util.DateFormatLimitType.* 4 | import java.time.Instant 5 | import java.time.LocalDate 6 | import java.time.LocalDateTime 7 | import java.time.ZoneOffset 8 | import java.time.format.DateTimeFormatter 9 | 10 | val defaultZoneOffset: ZoneOffset = ZoneOffset.systemDefault().rules.getOffset(Instant.now()) 11 | 12 | fun LocalDate?.localDateToString(): String = this?.format(DateTimeFormatter.ofPattern("yyyy. M. d")) ?: "" 13 | 14 | val LocalDate.milliseconds get() = seconds * 1000 15 | 16 | val LocalDate.seconds get() = toEpochDay() * 24 * 60 * 60 17 | 18 | val LocalDateTime.amPm: String get() = if (hour < 12) "오전" else "오후" 19 | 20 | fun getStartOfDate(): LocalDateTime = LocalDate.now().atTime(0, 0, 0) 21 | 22 | fun getEndOfDate(): LocalDateTime = LocalDate.now().atTime(23, 59, 59) 23 | 24 | fun relativeDateFormat(baseDateTime: LocalDateTime, targetDateTime: LocalDateTime, limit: DateFormatLimitType = NONE) 25 | : DateTimeFormatter = when { 26 | limit == YEAR || baseDateTime.year != targetDateTime.year -> DateTimeFormatter.ofPattern("yyyy년 M월 d일 ${targetDateTime.amPm} hh:mm") 27 | limit == MONTH || baseDateTime.month != targetDateTime.month -> DateTimeFormatter.ofPattern("M월 d일 ${targetDateTime.amPm} hh:mm") 28 | limit == DAY || baseDateTime.dayOfYear != targetDateTime.dayOfYear -> DateTimeFormatter.ofPattern("d일 ${targetDateTime.amPm} hh:mm") 29 | else -> DateTimeFormatter.ofPattern("${targetDateTime.amPm} hh:mm") 30 | } 31 | 32 | fun relativeDateFormat(baseDate: LocalDate, targetDate: LocalDate): DateTimeFormatter = when { 33 | baseDate.year != targetDate.year -> DateTimeFormatter.ofPattern("yyyy년 M월 d일") 34 | else -> DateTimeFormatter.ofPattern("M월 d일") 35 | } 36 | 37 | enum class DateFormatLimitType { 38 | YEAR, 39 | MONTH, 40 | DAY, 41 | NONE 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/HorizontalInsetDividerDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Rect 6 | import androidx.recyclerview.widget.DividerItemDecoration 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.drunkenboys.calendarun.util.extensions.dp2px 9 | import kotlin.math.roundToInt 10 | 11 | class HorizontalInsetDividerDecoration( 12 | private val context: Context, 13 | private val orientation: Int, 14 | private var leftInset: Float = 0f, 15 | private var rightInset: Float = 0f, 16 | private val ignoreLast: Boolean = false 17 | ) : DividerItemDecoration(context, orientation) { 18 | 19 | private val mBounds = Rect() 20 | 21 | override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { 22 | if (parent.layoutManager == null || drawable == null) { 23 | return 24 | } 25 | if (orientation == VERTICAL) { 26 | drawVertical(c, parent) 27 | } else { 28 | super.onDraw(c, parent, state) 29 | } 30 | } 31 | 32 | private fun drawVertical(canvas: Canvas, parent: RecyclerView) { 33 | val drawable = drawable ?: return 34 | 35 | canvas.save() 36 | var left: Int 37 | var right: Int 38 | if (parent.clipToPadding) { 39 | left = parent.paddingLeft 40 | right = parent.width - parent.paddingRight 41 | canvas.clipRect( 42 | left, parent.paddingTop, right, 43 | parent.height - parent.paddingBottom 44 | ) 45 | } else { 46 | left = 0 47 | right = parent.width 48 | } 49 | left += context.dp2px(leftInset).toInt() 50 | right -= context.dp2px(rightInset).toInt() 51 | 52 | val childCount = if (ignoreLast) parent.childCount - 1 else parent.childCount 53 | for (i in 0 until childCount) { 54 | val child = parent.getChildAt(i) 55 | parent.getDecoratedBoundsWithMargins(child, mBounds) 56 | val bottom = mBounds.bottom + child.translationY.roundToInt() 57 | val top = bottom - drawable.intrinsicHeight 58 | drawable.setBounds(left, top, right, bottom) 59 | drawable.draw(canvas) 60 | } 61 | canvas.restore() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/AnimationExt.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import android.view.animation.Animation 4 | 5 | fun Animation.setAnimationListener( 6 | onStart: ((Animation?) -> Unit)? = null, 7 | onEnd: ((Animation?) -> Unit)? = null, 8 | onRepeat: ((Animation?) -> Unit)? = null 9 | ) { 10 | this.setAnimationListener(object : Animation.AnimationListener { 11 | override fun onAnimationStart(animation: Animation?) { 12 | onStart?.invoke(animation) 13 | } 14 | 15 | override fun onAnimationEnd(animation: Animation?) { 16 | onEnd?.invoke(animation) 17 | } 18 | 19 | override fun onAnimationRepeat(animation: Animation?) { 20 | onRepeat?.invoke(animation) 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | 6 | fun Context.dp2px(dp: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/FlowExt.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.collect 5 | import kotlinx.coroutines.flow.flow 6 | 7 | // from https://proandroiddev.com/from-rxjava-to-kotlin-flow-throttling-ed1778847619 8 | fun Flow.throttleFirst(periodMillis: Long): Flow { 9 | require(periodMillis > 0) { "period should be positive" } 10 | return flow { 11 | var lastTime = 0L 12 | collect { value -> 13 | val currentTime = System.currentTimeMillis() 14 | if (currentTime - lastTime >= periodMillis) { 15 | lastTime = currentTime 16 | emit(value) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/NavigationExt.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavDirections 5 | 6 | /** 7 | * NavController의 현재 destination에서 해당 action으로 8 | * 이동할 수 있는 destination이 있는지 확인 후 navigate. 9 | */ 10 | fun NavController.navigateSafe(action: NavDirections) { 11 | currentDestination?.getAction(action.actionId) ?: return 12 | navigate(action) 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/NetworkStateUtil.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.net.NetworkCapabilities 6 | import android.os.Build 7 | 8 | fun Context.getNetworkConnectState(): Boolean { 9 | var isConnected = false 10 | val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 12 | val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) 13 | if (capabilities != null) { 14 | isConnected = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) 15 | || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) 16 | } 17 | } else { 18 | val activeNetwork = cm.activeNetworkInfo 19 | isConnected = activeNetwork?.isConnectedOrConnecting == true 20 | } 21 | return isConnected 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/PendingIntentExt.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import android.app.PendingIntent 4 | import android.os.Build 5 | 6 | object PendingIntentExt { 7 | 8 | val FLAG_UPDATE_CURRENT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 9 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE 10 | } else { 11 | PendingIntent.FLAG_UPDATE_CURRENT 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.view.View 5 | import android.view.animation.Animation 6 | import android.view.animation.AnimationUtils 7 | import android.widget.TextView 8 | 9 | fun View.startAnimation(animationResId: Int) { 10 | val anim = AnimationUtils.loadAnimation(context, animationResId) 11 | this.startAnimation(anim) 12 | } 13 | 14 | fun View.startAnimation(animationResId: Int, onAnimationEnd: () -> Unit) { 15 | val anim = AnimationUtils.loadAnimation(context, animationResId) 16 | anim.setAnimationListener(object : Animation.AnimationListener { 17 | override fun onAnimationStart(animation: Animation?) { 18 | } 19 | 20 | override fun onAnimationEnd(animation: Animation?) { 21 | onAnimationEnd.invoke() 22 | } 23 | 24 | override fun onAnimationRepeat(animation: Animation?) { 25 | } 26 | }) 27 | this.startAnimation(anim) 28 | } 29 | 30 | fun TextView.updateCompoundDrawablesRelativeWithIntrinsicBounds( 31 | start: Drawable? = compoundDrawablesRelative[0], 32 | top: Drawable? = compoundDrawablesRelative[1], 33 | end: Drawable? = compoundDrawablesRelative[2], 34 | bottom: Drawable? = compoundDrawablesRelative[3] 35 | ) { 36 | setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/util/extensions/ViewHolderExt.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.util.extensions 2 | 3 | import android.content.Context 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | fun RecyclerView.ViewHolder.context(): Context { 7 | return this.itemView.context 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/view/ColorIndicatorTextView.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.view 2 | 3 | import android.content.Context 4 | import android.content.res.ColorStateList 5 | import android.content.res.TypedArray 6 | import android.util.AttributeSet 7 | import androidx.appcompat.widget.AppCompatTextView 8 | import androidx.core.content.ContextCompat 9 | import com.drunkenboys.calendarun.R 10 | import com.drunkenboys.calendarun.util.extensions.updateCompoundDrawablesRelativeWithIntrinsicBounds 11 | 12 | class ColorIndicatorTextView @JvmOverloads constructor( 13 | context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 14 | ) : AppCompatTextView(context, attrs, defStyle) { 15 | 16 | init { 17 | getAttrs(attrs, defStyle) 18 | } 19 | 20 | private fun getAttrs(attrs: AttributeSet?, defStyle: Int) { 21 | attrs ?: return 22 | 23 | val typedArray = context.obtainStyledAttributes( 24 | attrs, R.styleable.ColorIndicatorTextView, defStyle, 0 25 | ) 26 | 27 | setTypedArray(typedArray) 28 | } 29 | 30 | private fun setTypedArray(typedArray: TypedArray) { 31 | val drawableEndTint = typedArray.getColor(R.styleable.ColorIndicatorTextView_indicatorTint, 0) 32 | 33 | val drawableEnd = compoundDrawablesRelative[2] 34 | 35 | drawableEnd.setTintList(ColorStateList.valueOf(drawableEndTint)) 36 | 37 | typedArray.recycle() 38 | } 39 | 40 | fun setIndicatorTint(color: Int) { 41 | when (color) { 42 | ContextCompat.getColor(context, R.color.white) -> { 43 | val bg = ContextCompat.getDrawable(context, R.drawable.bg_tag_color_white_stroke_grey) 44 | updateCompoundDrawablesRelativeWithIntrinsicBounds(end = bg) 45 | } 46 | ContextCompat.getColor(context, R.color.black) -> { 47 | val bg = ContextCompat.getDrawable(context, R.drawable.bg_tag_color_black_stroke_grey) 48 | updateCompoundDrawablesRelativeWithIntrinsicBounds(end = bg) 49 | } 50 | else -> { 51 | updateCompoundDrawablesRelativeWithIntrinsicBounds(end = ContextCompat.getDrawable(context, R.drawable.bg_tag_color)) 52 | compoundDrawablesRelative[2].setTintList(ColorStateList.valueOf(color)) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/view/ErrorGuideEditText.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.view 2 | 3 | import android.animation.AnimatorSet 4 | import android.animation.ObjectAnimator 5 | import android.content.Context 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import androidx.appcompat.widget.AppCompatEditText 9 | import androidx.core.content.ContextCompat 10 | import androidx.core.widget.doOnTextChanged 11 | import com.drunkenboys.calendarun.R 12 | import com.drunkenboys.calendarun.util.extensions.dp2px 13 | 14 | class ErrorGuideEditText @JvmOverloads constructor( 15 | context: Context, 16 | attrs: AttributeSet? = null, 17 | defStyle: Int = R.attr.editTextStyle 18 | ) : AppCompatEditText(context, attrs, defStyle) { 19 | 20 | private val defaultHintTextColor = currentHintTextColor 21 | private val errorColor = ContextCompat.getColor(context, R.color.onError) 22 | 23 | var isError = false 24 | set(value) { 25 | if (value) startErrorAnimation() 26 | field = value 27 | } 28 | 29 | init { 30 | doOnTextChanged { _, _, _, _ -> 31 | if (isError) { 32 | setHintTextColor(defaultHintTextColor) 33 | isError = false 34 | } 35 | } 36 | } 37 | 38 | private fun startErrorAnimation() { 39 | val leftTranslationX = context.dp2px(-2f) 40 | val rightTranslationX = context.dp2px(2f) 41 | 42 | val hintTextColorAnimator = ObjectAnimator.ofArgb( 43 | this, 44 | HINT_TEXT_COLOR, 45 | currentHintTextColor, 46 | errorColor 47 | ) 48 | val translationAnimator = ObjectAnimator.ofFloat( 49 | this, 50 | View.TRANSLATION_X, 51 | rightTranslationX, 52 | leftTranslationX, 53 | rightTranslationX, 54 | leftTranslationX, 55 | 0f 56 | ) 57 | 58 | AnimatorSet().apply { 59 | duration = DURATION 60 | play(hintTextColorAnimator).with(translationAnimator) 61 | }.start() 62 | } 63 | 64 | companion object { 65 | 66 | private const val DURATION = 300L 67 | private const val HINT_TEXT_COLOR = "hintTextColor" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/drunkenboys/calendarun/view/ErrorGuideTextView.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.view 2 | 3 | import android.animation.AnimatorSet 4 | import android.animation.ObjectAnimator 5 | import android.content.Context 6 | import android.graphics.Rect 7 | import android.util.AttributeSet 8 | import android.view.View 9 | import androidx.appcompat.widget.AppCompatTextView 10 | import androidx.core.content.ContextCompat 11 | import androidx.core.widget.doOnTextChanged 12 | import com.drunkenboys.calendarun.R 13 | import com.drunkenboys.calendarun.util.extensions.dp2px 14 | 15 | class ErrorGuideTextView @JvmOverloads constructor( 16 | context: Context, 17 | attrs: AttributeSet? = null, 18 | defStyle: Int = R.attr.autoCompleteTextViewStyle 19 | ) : AppCompatTextView(context, attrs, defStyle) { 20 | 21 | private val defaultHintTextColor = currentHintTextColor 22 | private val errorColor = ContextCompat.getColor(context, R.color.onError) 23 | 24 | var isError = false 25 | set(value) { 26 | if (value) startErrorAnimation() 27 | field = value 28 | } 29 | 30 | init { 31 | doOnTextChanged { _, _, _, _ -> 32 | if (isError) { 33 | setHintTextColor(defaultHintTextColor) 34 | isError = false 35 | } 36 | } 37 | } 38 | 39 | override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { 40 | super.onFocusChanged(focused, direction, previouslyFocusedRect) 41 | if (focused) performClick() 42 | } 43 | 44 | 45 | private fun startErrorAnimation() { 46 | val leftTranslationX = context.dp2px(-2f) 47 | val rightTranslationX = context.dp2px(2f) 48 | 49 | val hintTextColorAnimator = ObjectAnimator.ofArgb( 50 | this, 51 | HINT_TEXT_COLOR, 52 | currentHintTextColor, 53 | errorColor 54 | ) 55 | val translationAnimator = ObjectAnimator.ofFloat( 56 | this, 57 | View.TRANSLATION_X, 58 | rightTranslationX, 59 | leftTranslationX, 60 | rightTranslationX, 61 | leftTranslationX, 62 | 0f 63 | ) 64 | 65 | AnimatorSet().apply { 66 | duration = DURATION 67 | play(hintTextColorAnimator).with(translationAnimator) 68 | }.start() 69 | } 70 | 71 | companion object { 72 | 73 | private const val DURATION = 300L 74 | private const val HINT_TEXT_COLOR = "hintTextColor" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/res/anim/hide_scale_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/anim/show_scale_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/color/navigation_item_background_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/navigation_item_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/app_widget_background.xml: -------------------------------------------------------------------------------- 1 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/app_widget_inner_view_background.xml: -------------------------------------------------------------------------------- 1 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_check_box_normal_18.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_on_surface_radius_8dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_tag_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_tag_color_black_stroke_grey.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_tag_color_white_stroke_grey.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_white_radius_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_rect_black_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_circle_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_assignment.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_check_box_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_check_box_outline_blank_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_calendar_today.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_calendar_today_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_box_pressed_18.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_license.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notifications.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_palette.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_text_fields.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selector_check_box_18.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/font/inter_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/calendarun_app_widget.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 23 | 24 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/drawer_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 28 | 29 | 39 | 40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_app_widget_schedule.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_date.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_drop_down_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_schedule.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 17 | 23 | 24 | 33 | 34 | 47 | 48 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main_calendar_nav_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 13 | 14 | 19 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main_calendar_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_manage_calendar_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_save_calendar_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_save_schedule_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_theme_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_main_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-hdpi/ic_main.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_main_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-hdpi/ic_main_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_main_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-hdpi/ic_main_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-mdpi/ic_main.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_main_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-mdpi/ic_main_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_main_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-mdpi/ic_main_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xhdpi/ic_main.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_main_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xhdpi/ic_main_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_main_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xhdpi/ic_main_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxhdpi/ic_main.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_main_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxhdpi/ic_main_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_main_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxhdpi/ic_main_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxxhdpi/ic_main.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_main_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxxhdpi/ic_main_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_main_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/app/src/main/res/mipmap-xxxhdpi/ic_main_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night-v31/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #121212 4 | #ffffff 5 | #4D4D4D 6 | #F1F1F1 7 | 8 | @color/light_grey 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/saveSchedule_notificationNone 5 | @string/saveSchedule_notificationTenMinutesAgo 6 | @string/saveSchedule_notificationAHourAgo 7 | @string/saveSchedule_notificationADayAgo 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | #80FFFFFF 6 | #FFFFFFFF 7 | #FF000000 8 | 9 | #cccccc 10 | @color/white_alpha_50 11 | 12 | #4D4D4D 13 | #F1F1F1 14 | #FBC4FF 15 | #FB60C4 16 | 17 | @color/black 18 | 19 | #e20000 20 | #FFE1F5FE 21 | #FF81D4FA 22 | #FF039BE5 23 | #FF01579B 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 0dp 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml-v31/calenda_run_app_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/calenda_run_app_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/drunkenboys/calendarun/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/test/java/com/drunkenboys/calendarun/data/calendar/local/FakeCalendarLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.calendar.local 2 | 3 | import com.drunkenboys.calendarun.data.calendar.entity.Calendar 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flow 6 | 7 | class FakeCalendarLocalDataSource : CalendarLocalDataSource { 8 | 9 | private val database = mutableListOf() 10 | 11 | override suspend fun insertCalendar(calendar: Calendar): Long { 12 | database.add(calendar) 13 | return database.size.toLong() 14 | } 15 | 16 | override fun fetchAllCalendar(): Flow> = flow { emit(database.toList()) } 17 | 18 | override suspend fun fetchCalendar(id: Long): Calendar = database.find { it.id == id } ?: throw IllegalArgumentException() 19 | 20 | override fun fetchCustomCalendar(): Flow> = flow { emit(database.filter { it.id != 1 }) } 21 | 22 | override suspend fun deleteCalendar(calendar: Calendar) { 23 | database.remove(calendar) 24 | } 25 | 26 | override suspend fun updateCalendar(calendar: Calendar) { 27 | val targetIndex = database.indexOfFirst { it.id == calendar.id } 28 | database.removeAt(targetIndex) 29 | database.add(targetIndex, calendar) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/test/java/com/drunkenboys/calendarun/data/schedule/local/FakeScheduleLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.schedule.local 2 | 3 | import com.drunkenboys.calendarun.data.schedule.entity.Schedule 4 | import com.drunkenboys.calendarun.util.defaultZoneOffset 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flow 7 | import java.time.LocalDateTime 8 | 9 | class FakeScheduleLocalDataSource : ScheduleLocalDataSource { 10 | 11 | private val database = mutableListOf() 12 | 13 | override suspend fun insertSchedule(schedule: Schedule): Long { 14 | database.add(schedule) 15 | return database.size.toLong() 16 | } 17 | 18 | override suspend fun fetchAllSchedule(): List { 19 | return database 20 | } 21 | 22 | override suspend fun fetchSchedule(id: Long): Schedule { 23 | return database.find { it.id == id } ?: throw IllegalArgumentException() 24 | } 25 | 26 | override fun fetchCalendarSchedules(calendarId: Long): Flow> { 27 | return flow { emit(database.filter { it.calendarId == calendarId }) } 28 | } 29 | 30 | override suspend fun updateSchedule(schedule: Schedule) { 31 | val targetIndex = database.indexOfFirst { it.id == schedule.id } 32 | database.removeAt(targetIndex) 33 | database.add(targetIndex, schedule) 34 | } 35 | 36 | override suspend fun deleteSchedule(schedule: Schedule) { 37 | database.remove(schedule) 38 | } 39 | 40 | override suspend fun fetchMatchedScheduleAfter(word: String, time: Long) = 41 | database.filter { it.startDate.toEpochSecond(defaultZoneOffset) > time && word in it.name } 42 | .sortedBy { it.startDate } 43 | .take(ScheduleDao.SCHEDULE_PAGING_SIZE) 44 | 45 | override suspend fun fetchMatchedScheduleBefore(word: String, time: Long) = 46 | database.filter { it.startDate.toEpochSecond(defaultZoneOffset) < time && word in it.name } 47 | .sortedBy { it.startDate } 48 | .takeLast(ScheduleDao.SCHEDULE_PAGING_SIZE) 49 | 50 | override fun fetchDateSchedule(date: LocalDateTime): Flow> { 51 | TODO("Not yet implemented") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/test/java/com/drunkenboys/calendarun/data/slice/local/FakeSliceLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.calendarun.data.slice.local 2 | 3 | import com.drunkenboys.calendarun.data.slice.entity.Slice 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flow 6 | 7 | class FakeSliceLocalDataSource : SliceLocalDataSource { 8 | 9 | private val database = mutableListOf() 10 | 11 | override suspend fun insertSlice(slice: Slice): Long { 12 | database.add(slice) 13 | return database.size.toLong() 14 | } 15 | 16 | override suspend fun fetchAllSlice(): List = database 17 | 18 | override suspend fun fetchSlice(id: Long) = database.find { it.id == id } ?: throw IllegalArgumentException() 19 | 20 | override fun fetchCalendarSliceList(calendarId: Long): Flow> = 21 | flow { emit(database.filter { it.calendarId == calendarId }) } 22 | 23 | override suspend fun updateSlice(slice: Slice) { 24 | val targetIndex = database.indexOfFirst { it.id == slice.id } 25 | database.removeAt(targetIndex) 26 | database.add(targetIndex, slice) 27 | } 28 | 29 | override suspend fun deleteSliceList(calendarId: Long) { 30 | database.removeAll { it.calendarId == calendarId } 31 | } 32 | 33 | override suspend fun deleteSlice(slice: Slice) { 34 | 35 | } 36 | 37 | override suspend fun deleteSliceById(id: Long) { 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | gradle_version = "7.0.3" 5 | kotlin_version = "1.5.10" 6 | 7 | google_services_version = "4.3.10" 8 | crashlytics_version = "2.8.0" 9 | 10 | compose_version = "1.0.0-rc01" 11 | 12 | mockito_version = "4.0.0" 13 | lifecycle_version = "2.4.0" 14 | nav_version = "2.3.5" 15 | room_version = "2.3.0" 16 | hilt_version = "2.40" 17 | retrofit_version = "2.9.0" 18 | coroutine_version = "1.5.2" 19 | lottieVersion = "4.2.1" 20 | } 21 | 22 | repositories { 23 | google() 24 | mavenCentral() 25 | } 26 | 27 | dependencies { 28 | classpath "com.android.tools.build:gradle:$gradle_version" 29 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 30 | // NOTE: Do not place your application dependencies here; they belong 31 | // in the individual module build.gradle files 32 | classpath "com.google.gms:google-services:$google_services_version" 33 | classpath "com.google.firebase:firebase-crashlytics-gradle:$crashlytics_version" 34 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" 35 | classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" 36 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 37 | classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' 38 | } 39 | } 40 | 41 | task clean(type: Delete) { 42 | delete rootProject.buildDir 43 | } 44 | -------------------------------------------------------------------------------- /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=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec: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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Oct 26 18:17:28 KST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/library/consumer-rules.pro -------------------------------------------------------------------------------- /library/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 -------------------------------------------------------------------------------- /library/src/androidTest/java/com/drunkenboys/ckscalendar/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.drunkenboys.ckscalendar.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /library/src/main/ic_main-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/android01-CalendaRun/368860083a7ac69c2637dc0a4e7df88c9c399aa2/library/src/main/ic_main-playstore.png -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/data/CalendarDate.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.data 2 | 3 | import java.time.LocalDate 4 | 5 | data class CalendarDate( 6 | val date: LocalDate, 7 | val dayType: DayType, 8 | var isSelected: Boolean = false 9 | ) 10 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/data/CalendarDesignObject.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.data 2 | 3 | import android.graphics.Color 4 | import android.view.Gravity 5 | import androidx.annotation.ColorInt 6 | import androidx.annotation.DrawableRes 7 | import com.drunkenboys.ckscalendar.R 8 | 9 | /** 10 | * CalendarView의 디자인을 위한 설정 11 | */ 12 | data class CalendarDesignObject( 13 | @ColorInt var weekDayTextColor: Int = Color.BLACK, 14 | @ColorInt var holidayTextColor: Int = ScheduleColorType.RED.color, 15 | @ColorInt var saturdayTextColor: Int = ScheduleColorType.BLUE.color, 16 | @ColorInt var sundayTextColor: Int = ScheduleColorType.RED.color, 17 | @ColorInt var selectedFrameColor: Int = ScheduleColorType.GRAY.color, 18 | @ColorInt var backgroundColor: Int = Color.WHITE, 19 | @DrawableRes var selectedFrameDrawable: Int = R.drawable.bg_month_date_selected, 20 | var textSize: Float = 10f, 21 | var textAlign: Int = Gravity.CENTER, 22 | val weekSimpleStringSet: List = listOf("일", "월", "화", "수", "목", "금", "토"), 23 | val weekFullStringSet: List = listOf("일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"), 24 | var visibleScheduleCount: Int = 3 25 | ) { 26 | companion object { 27 | 28 | fun getDefaultDesign() = CalendarDesignObject() 29 | 30 | fun getDarkDesign() = getDefaultDesign().apply { 31 | backgroundColor = Color.BLACK 32 | weekDayTextColor = Color.WHITE 33 | selectedFrameColor = Color.WHITE 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/data/CalendarScheduleObject.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.data 2 | 3 | import android.graphics.Color 4 | import com.drunkenboys.ckscalendar.utils.TimeUtils 5 | import java.time.LocalDateTime 6 | 7 | // CalendarView에 스케쥴 등을 표시하기 위한 오브젝트 8 | // TODO: 9 | data class CalendarScheduleObject( 10 | val id: Int, 11 | val color: Int = TimeUtils.getColorInt(255,245,246), 12 | val text: String, 13 | val startDate: LocalDateTime, 14 | val endDate: LocalDateTime, 15 | val isHoliday: Boolean = false 16 | ) 17 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/data/CalendarSet.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.data 2 | 3 | import android.content.Context 4 | import com.drunkenboys.ckscalendar.R 5 | import com.drunkenboys.ckscalendar.utils.toLong 6 | import java.time.LocalDate 7 | 8 | data class CalendarSet( 9 | val id: Int, 10 | val name: String, 11 | val startDate: LocalDate, 12 | val endDate: LocalDate, 13 | ) { 14 | companion object { 15 | 16 | fun generateCalendarOfYear(context: Context, year: Int): List { 17 | return (1..12).map { month -> 18 | val start = LocalDate.of(year, month, 1) 19 | val end = LocalDate.of(year, month, start.lengthOfMonth()) 20 | CalendarSet( 21 | id = start.toLong().toInt(), 22 | name = "${year}${context.getString(R.string.common_year)} ${month}${context.getString(R.string.common_month)}", 23 | startDate = start, 24 | endDate = end 25 | ) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/data/DayType.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.data 2 | 3 | enum class DayType { 4 | WEEKDAY, 5 | HOLIDAY, 6 | SATURDAY, 7 | SUNDAY, 8 | PADDING 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/data/ScheduleColorType.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.data 2 | 3 | import com.drunkenboys.ckscalendar.utils.TimeUtils 4 | 5 | enum class ScheduleColorType(val color: Int) { 6 | RED(TimeUtils.getColorInt(220, 34, 39)), 7 | ORANGE(TimeUtils.getColorInt(255, 113, 67)), 8 | YELLOW(TimeUtils.getColorInt(254, 193, 7)), 9 | GREEN(TimeUtils.getColorInt(139, 194, 74)), 10 | BLUE(TimeUtils.getColorInt(75, 181, 222)), 11 | NAVY(TimeUtils.getColorInt(3, 169, 245)), 12 | PURPLE(TimeUtils.getColorInt(156, 40, 177)), 13 | GRAY(TimeUtils.getColorInt(225, 225, 225)), 14 | CYAN(TimeUtils.getColorInt(71, 214, 220)), 15 | MAGENTA(TimeUtils.getColorInt(255, 184, 120)) 16 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/listener/OnDayClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.listener 2 | 3 | import java.time.LocalDate 4 | 5 | fun interface OnDayClickListener { 6 | 7 | fun onDayClick(date: LocalDate, position: Int) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/listener/OnDaySecondClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.listener 2 | 3 | import java.time.LocalDate 4 | 5 | fun interface OnDaySecondClickListener { 6 | 7 | fun onDayClick(date: LocalDate, position: Int) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/monthcalendar/MonthCellPositionStore.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.monthcalendar 2 | 3 | data class MonthCellPositionStore( 4 | val savedLineIndex: Int, 5 | val lastSelectedPosition: Int 6 | ) 7 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/monthcalendar/MonthState.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.monthcalendar 2 | 3 | import android.os.Parcelable 4 | import android.view.View 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | class MonthState( 9 | val parcelable: Parcelable, 10 | val lastPageName: String = "", 11 | val lastPagePosition: Int = -1, 12 | val lastCalendarType: CalendarType = CalendarType.NONE 13 | ) : View.BaseSavedState(parcelable) { 14 | 15 | enum class CalendarType { 16 | DEFAULT, CUSTOM, NONE 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/monthcalendar/OnDaySelectStateListener.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.monthcalendar 2 | 3 | fun interface OnDaySelectStateListener { 4 | 5 | fun onDaySelectChange(pagePosition: Int, dayPosition: Int) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/utils/CalendarSetConverter.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import com.drunkenboys.ckscalendar.data.CalendarDate 4 | import com.drunkenboys.ckscalendar.data.CalendarScheduleObject 5 | import com.drunkenboys.ckscalendar.data.CalendarSet 6 | import com.drunkenboys.ckscalendar.data.DayType 7 | import java.time.DayOfWeek 8 | 9 | fun calendarSetToCalendarDatesList(calendarSet: CalendarSet, schedules: List): List> { 10 | val holidaySchedule = schedules.filter { schedule -> schedule.isHoliday } 11 | .map { schedule -> schedule.startDate.toLocalDate() } 12 | .sortedDescending() 13 | 14 | // n주 15 | val weekList = mutableListOf>() 16 | var oneDay = calendarSet.startDate 17 | var paddingPrev = calendarSet.startDate 18 | var paddingNext = calendarSet.endDate 19 | 20 | // 앞쪽 패딩 21 | if (paddingPrev.dayOfWeek != DayOfWeek.SUNDAY) weekList.add(mutableListOf()) 22 | while (paddingPrev.dayOfWeek != DayOfWeek.SUNDAY) { 23 | paddingPrev = paddingPrev.minusDays(1) 24 | weekList.last().add(CalendarDate(paddingPrev, DayType.PADDING)) 25 | } 26 | 27 | // n일 추가 28 | while (oneDay <= calendarSet.endDate) { 29 | 30 | // 일요일에는 1주일 갱신 31 | if (oneDay.dayOfWeek == DayOfWeek.SUNDAY) weekList.add(mutableListOf()) 32 | 33 | // 1일 추가 34 | weekList.last() 35 | .add( 36 | CalendarDate( 37 | date = oneDay, 38 | dayType = if (oneDay in holidaySchedule) DayType.HOLIDAY 39 | else TimeUtils.parseDayWeekToDayType(oneDay.dayOfWeek) 40 | ) 41 | ) 42 | 43 | oneDay = oneDay.plusDays(1L) 44 | } 45 | 46 | // 뒤쪽 패딩 47 | while (paddingNext.dayOfWeek != DayOfWeek.SATURDAY) { 48 | paddingNext = paddingNext.plusDays(1) 49 | weekList.last().add(CalendarDate(paddingNext, DayType.PADDING)) 50 | } 51 | 52 | return weekList 53 | } 54 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/utils/ComposeStateExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import androidx.compose.foundation.lazy.LazyListState 4 | import androidx.compose.runtime.* 5 | import kotlinx.coroutines.flow.collect 6 | import java.time.DayOfWeek 7 | import java.time.LocalDate 8 | import java.util.* 9 | 10 | 11 | 12 | // 참고: https://ichi.pro/ko/jetpack-compose-ui-muhan-moglog-peiji-moglog-194681336448872 13 | @Composable 14 | fun LazyListState.ShouldNextScroll( 15 | onLoadMore : () -> Unit 16 | ) { 17 | 18 | // emit 19 | val shouldLoadMore = remember { 20 | derivedStateOf { 21 | with(layoutInfo) { 22 | // 화면에 보이는 마지막 인덱스 23 | val lastIndex = visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true 24 | 25 | lastIndex.index >= totalItemsCount - 1 26 | } 27 | } 28 | } 29 | 30 | // job 31 | LaunchedEffect(shouldLoadMore) { 32 | // collect 33 | snapshotFlow { shouldLoadMore.value }.collect { should -> 34 | if (should) { onLoadMore() } 35 | } 36 | } 37 | } 38 | 39 | @Composable 40 | fun LazyListState.ShouldPrevScroll( 41 | onLoadMore: () -> Unit 42 | ) { 43 | 44 | val shouldLoadMore = remember { 45 | derivedStateOf { 46 | with(layoutInfo) { 47 | //화면에 보이는 첫번째 인덱스 48 | val firstIndex = visibleItemsInfo.firstOrNull() ?: return@derivedStateOf true 49 | 50 | firstIndex.index <= 1 51 | } 52 | } 53 | } 54 | 55 | // job 56 | LaunchedEffect(shouldLoadMore) { 57 | // collect 58 | snapshotFlow { shouldLoadMore.value }.collect { should -> 59 | if (should) { onLoadMore() } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/utils/DateConverter.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import java.time.Instant 4 | import java.time.LocalDate 5 | import java.time.ZoneOffset 6 | import java.time.format.DateTimeFormatter 7 | 8 | val defaultZoneOffset: ZoneOffset = ZoneOffset.systemDefault().rules.getOffset(Instant.now()) 9 | 10 | fun stringToLocalDate(date: String): LocalDate = 11 | LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy.M.d")) 12 | 13 | fun LocalDate?.localDateToString(): String = this?.format(DateTimeFormatter.ofPattern("yyyy.M.d")) ?: "" 14 | 15 | fun LocalDate.toLong() = toEpochDay() * 24 * 60 * 60 * 1000 16 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/utils/GravityMapper.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import android.text.Layout 4 | import android.view.Gravity 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.text.style.TextAlign 7 | 8 | object GravityMapper { 9 | fun toTextAlign(gravity: Int): TextAlign = when (gravity) { 10 | Gravity.CENTER -> TextAlign.Center 11 | Gravity.END -> TextAlign.End 12 | Gravity.RIGHT -> TextAlign.Right 13 | Gravity.LEFT -> TextAlign.Left 14 | Gravity.START -> TextAlign.Start 15 | else -> TextAlign.Center 16 | } 17 | 18 | fun toColumnAlign(gravity: Int): Alignment.Horizontal = when (gravity) { 19 | Gravity.CENTER -> Alignment.CenterHorizontally 20 | Gravity.END -> Alignment.End 21 | Gravity.START -> Alignment.Start 22 | else -> Alignment.CenterHorizontally 23 | } 24 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/utils/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import com.drunkenboys.ckscalendar.data.DayType 4 | import java.time.DayOfWeek 5 | import java.time.LocalDate 6 | import java.time.Period 7 | 8 | object TimeUtils { 9 | fun getColorInt(R: Int, G: Int, B: Int): Int { 10 | return 0xff shl 24 or (R and 0xff shl 16) or (G and 0xff shl 8) or (B and 0xff) 11 | } 12 | 13 | fun parseDayWeekToDayType(week: DayOfWeek): DayType { 14 | return when (week) { 15 | DayOfWeek.SATURDAY -> DayType.SATURDAY 16 | DayOfWeek.SUNDAY -> DayType.SUNDAY 17 | else -> DayType.WEEKDAY 18 | } 19 | } 20 | 21 | // 일요일 시작 기준 요일값 22 | fun DayOfWeek.dayValue() = this.value % 7 23 | 24 | fun LocalDate.isSameWeek(endDate: LocalDate): Boolean { 25 | val weekDuration = (endDate.dayOfWeek.dayValue()) - (this.dayOfWeek.dayValue()) 26 | val dayDuration = Period.between(this, endDate).days 27 | return weekDuration == dayDuration 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/utils/ViewExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.DrawableContainer 5 | import android.graphics.drawable.GradientDrawable 6 | import android.graphics.drawable.StateListDrawable 7 | import android.util.TypedValue 8 | import android.view.View 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.platform.LocalDensity 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.recyclerview.widget.RecyclerView 13 | 14 | fun Context.dp2px(dp: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) 15 | 16 | fun View.tintStroke(color: Int, width: Float) { 17 | this.resources.displayMetrics.density.let { scale -> 18 | this.background?.constantState?.let { 19 | val origin = (it.newDrawable().mutate()) 20 | when (origin) { 21 | is GradientDrawable -> origin.setStroke((width * scale).toInt(), color) 22 | is StateListDrawable -> { 23 | val drawableContainerState = origin.constantState as DrawableContainer.DrawableContainerState 24 | val children = drawableContainerState.children 25 | children.filterNotNull() 26 | .forEach { 27 | it as GradientDrawable 28 | it.setStroke((width * scale).toInt(), color) 29 | } 30 | } 31 | } 32 | this.background = origin 33 | } 34 | } 35 | } 36 | 37 | fun RecyclerView.ViewHolder.context(): Context { 38 | return this.itemView.context 39 | } 40 | 41 | @Composable 42 | fun Int.dp() = with(LocalDensity.current) { Dp(this@dp.toFloat()).toSp() } 43 | 44 | @Composable 45 | fun Float.dp() = with(LocalDensity.current) { Dp(this@dp).toSp() } 46 | -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/yearcalendar/CustomTheme.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.yearcalendar 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.material.lightColors 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | import com.drunkenboys.ckscalendar.data.CalendarDesignObject 8 | 9 | /** 10 | * FIXME: View에서 Theme과 design의 color를 혼용하고 있음. 11 | * Theme 12 | */ 13 | @Composable 14 | fun CustomTheme( 15 | design: CalendarDesignObject, 16 | content: @Composable () -> Unit 17 | ) { 18 | val lightColors = lightColors( 19 | primary = Color(design.weekDayTextColor), 20 | primaryVariant = Color(design.selectedFrameColor), 21 | secondary = Color(design.saturdayTextColor), 22 | secondaryVariant = Color(design.sundayTextColor), 23 | background = Color(design.backgroundColor), 24 | ) 25 | 26 | // 존재하는 MaterialTheme에서 color set을 커스텀. 27 | // View를 선언할 때 MaterialTheme로 감싸면 테마를 적용할 수 있다. 28 | MaterialTheme( 29 | colors = lightColors, 30 | content = content 31 | ) 32 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/yearcalendar/composeView/BaseText.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.yearcalendar.composeView 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.text.style.TextAlign 10 | import com.drunkenboys.ckscalendar.data.CalendarDesignObject 11 | import com.drunkenboys.ckscalendar.data.DayType 12 | import com.drunkenboys.ckscalendar.utils.dp 13 | 14 | @Composable 15 | fun BaseText( 16 | text: String = " ", 17 | type: DayType, 18 | modifier: Modifier, 19 | fontWeight: FontWeight? = null, 20 | textAlign: TextAlign? = null, 21 | design: CalendarDesignObject 22 | ) { 23 | Text( 24 | text = text, 25 | modifier = modifier, 26 | fontSize = design.textSize.dp(), 27 | color = when (type) { 28 | DayType.PADDING -> Color.Transparent 29 | DayType.HOLIDAY -> Color(design.holidayTextColor) 30 | DayType.SATURDAY -> Color(design.saturdayTextColor) 31 | DayType.SUNDAY -> Color(design.sundayTextColor) 32 | else -> MaterialTheme.colors.primary 33 | }, 34 | fontWeight = fontWeight, 35 | textAlign = textAlign 36 | ) 37 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/yearcalendar/composeView/DayText.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.yearcalendar.composeView 2 | 3 | import androidx.compose.foundation.border 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.CircleShape 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import com.drunkenboys.ckscalendar.data.CalendarDate 15 | import com.drunkenboys.ckscalendar.data.DayType 16 | import com.drunkenboys.ckscalendar.utils.GravityMapper 17 | import com.drunkenboys.ckscalendar.utils.dp 18 | import com.drunkenboys.ckscalendar.yearcalendar.CustomTheme 19 | import com.drunkenboys.ckscalendar.yearcalendar.YearCalendarViewModel 20 | import java.time.LocalDate 21 | 22 | /** 23 | * 하루 날짜를 표시합니다. 24 | */ 25 | @Composable 26 | fun DayText( 27 | day: CalendarDate, 28 | viewModel: YearCalendarViewModel, 29 | isFirstOfCalendarSet: Boolean 30 | ) { 31 | BaseText( 32 | text = if (isFirstOfCalendarSet) { "${day.date.monthValue}. " } else { "" } + "${day.date.dayOfMonth}", 33 | type = day.dayType, 34 | modifier = Modifier.border( 35 | width = 1.dp, 36 | shape = CircleShape, 37 | color = if (LocalDate.now() == day.date) MaterialTheme.colors.primary else Color.Transparent 38 | ).padding(5.dp), 39 | textAlign = GravityMapper.toTextAlign(viewModel.design.value.textAlign), 40 | fontWeight = if (isFirstOfCalendarSet) FontWeight.Bold else null, 41 | design = viewModel.design.value 42 | ) 43 | } 44 | 45 | @Preview 46 | @Composable 47 | fun PreviewDayText() { 48 | val viewModel = YearCalendarViewModel() 49 | CustomTheme(design = viewModel.design.value) { 50 | DayText(day = CalendarDate(date = LocalDate.now(), dayType = DayType.WEEKDAY), viewModel = viewModel, true) 51 | } 52 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/yearcalendar/composeView/MonthHeader.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.yearcalendar.composeView 2 | 3 | import androidx.compose.animation.core.Spring 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.spring 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.lazy.LazyListState 9 | import androidx.compose.material.Card 10 | import androidx.compose.material.MaterialTheme 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.alpha 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.zIndex 20 | import com.drunkenboys.ckscalendar.yearcalendar.CustomTheme 21 | import com.drunkenboys.ckscalendar.yearcalendar.YearCalendarViewModel 22 | 23 | /** 24 | * 월 표시 25 | */ 26 | @Composable 27 | fun AnimatedMonthHeader( 28 | listState: LazyListState, 29 | monthName: String 30 | ) { 31 | // 0 ~ 1을 부드럽게 오가는 애니메이션 32 | // 스크롤을 하는 중이면 1이 된다. 33 | val density: Float by animateFloatAsState( 34 | targetValue = if(listState.isScrollInProgress) 1f else 0f, 35 | animationSpec = spring( 36 | dampingRatio = Spring.DampingRatioNoBouncy, 37 | stiffness = Spring.StiffnessMedium 38 | ) 39 | ) 40 | 41 | // 스크롤 할 때 투명도가 사라지는 애니메이션 42 | Card( 43 | modifier = Modifier.alpha(density).zIndex(10f), 44 | backgroundColor = MaterialTheme.colors.background, 45 | elevation = 10.dp 46 | ) { 47 | Text( 48 | text = monthName, 49 | color = MaterialTheme.colors.primary, 50 | modifier = Modifier.padding(start = 20.dp, end = 20.dp,top = 5.dp, bottom = 5.dp) 51 | ) 52 | } 53 | } 54 | 55 | @Preview 56 | @Composable 57 | fun PreviewMonthHeader() { 58 | val viewModel = YearCalendarViewModel() 59 | CustomTheme(design = viewModel.design.value) { 60 | Card( 61 | modifier = Modifier.wrapContentSize().background(color = Color.Black), 62 | elevation = 200.dp, 63 | ) { 64 | Text( 65 | text = "1월", 66 | color = MaterialTheme.colors.primary, 67 | modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 5.dp, bottom = 5.dp) 68 | ) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/yearcalendar/composeView/README.md: -------------------------------------------------------------------------------- 1 | # Year Calendar View Hierarchy 2 | 3 | ``` 4 | |-- YearCalendarView(LinearLayout) 5 | | |-- WeekHeader(요일 표시) 6 | | |-- CalendarLazyColumn(전체 달력) 7 | | | |-- MonthHeader(월 표시) 8 | | | |-- YearHeader(년 표시) 9 | | | |-- WeekCalendar(1주일 달력) 10 | | | | |-- DayText(날짜 표시) 11 | | | | |-- ScheduleText(일정 표시) 12 | ``` -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/yearcalendar/composeView/WeekHeader.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.yearcalendar.composeView 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.style.TextAlign 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import com.drunkenboys.ckscalendar.utils.GravityMapper 14 | import com.drunkenboys.ckscalendar.yearcalendar.CustomTheme 15 | import com.drunkenboys.ckscalendar.yearcalendar.YearCalendarViewModel 16 | 17 | /** 18 | * 요일 표시 19 | * 일월화수목금토 를 표시한다. 20 | */ 21 | @Composable 22 | fun WeekHeader( 23 | viewModel: YearCalendarViewModel 24 | ) { 25 | Row( 26 | modifier = Modifier.fillMaxWidth().background(MaterialTheme.colors.background), 27 | horizontalArrangement = Arrangement.SpaceEvenly 28 | ) { 29 | 30 | // design 속성에 있는 Simple String Set을 모두 표시한다. 31 | viewModel.design.value.weekSimpleStringSet.forEach { dayId -> 32 | Text( 33 | text = dayId, 34 | color = MaterialTheme.colors.primary, 35 | textAlign = GravityMapper.toTextAlign(viewModel.design.value.textAlign) 36 | ) 37 | } 38 | } 39 | } 40 | 41 | @Preview 42 | @Composable 43 | fun PreviewWeekHeader() { 44 | val viewModel = YearCalendarViewModel() 45 | 46 | CustomTheme(design = viewModel.design.value) { 47 | WeekHeader(viewModel = viewModel) 48 | } 49 | } -------------------------------------------------------------------------------- /library/src/main/java/com/drunkenboys/ckscalendar/yearcalendar/composeView/YearHeader.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.yearcalendar.composeView 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.unit.dp 11 | import com.drunkenboys.ckscalendar.data.CalendarDate 12 | import com.drunkenboys.ckscalendar.data.DayType 13 | import java.time.LocalDate 14 | 15 | @Composable 16 | fun YearHeader(week: List) { 17 | val startDate = week.first { day -> day.dayType != DayType.PADDING }.date 18 | val endDate = week.last { day -> day.dayType != DayType.PADDING }.date 19 | val firstOfYear = LocalDate.of(endDate.year, 1, 1) 20 | 21 | // 해가 갱신될 때마다 상단에 연표시 22 | if (firstOfYear in startDate..endDate) 23 | Text( 24 | text = "${startDate.year}년", 25 | color = MaterialTheme.colors.primary, 26 | modifier = Modifier 27 | .fillMaxWidth() 28 | .padding(top = 16.dp, bottom = 16.dp), 29 | textAlign = TextAlign.Center 30 | ) 31 | } -------------------------------------------------------------------------------- /library/src/main/res/drawable/bg_calendar_line_05px.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/bg_calendar_today.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/bg_month_date_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/layout/item_month_cell.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 17 | 18 | 26 | 27 | 28 | 37 | 38 | 47 | 48 | -------------------------------------------------------------------------------- /library/src/main/res/layout/item_month_page.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /library/src/main/res/layout/layout_week_calendar.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /library/src/main/res/layout/layout_year_calendar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /library/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | #FFFFFFFF 6 | #FF000000 7 | #CFCFCF 8 | #80F7F7F7 9 | #9C27B0 10 | 11 | -------------------------------------------------------------------------------- /library/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10dp 4 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/test/java/com/drunkenboys/ckscalendar/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /library/src/test/java/com/drunkenboys/ckscalendar/utils/CalendarSetConverterTest.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import com.drunkenboys.ckscalendar.data.CalendarSet 4 | import org.junit.Assert.* 5 | 6 | import org.junit.Test 7 | import java.time.LocalDate 8 | 9 | class CalendarSetConverterTest { 10 | private val id = 0 11 | private val name = "test" 12 | 13 | private val month1DayStart = LocalDate.of(2021, 1, 1) 14 | private val month1Day2 = LocalDate.of(2021, 1, 2) 15 | private val month1DayMid = LocalDate.of(2021, 1, 15) 16 | private val month1DayEnd = LocalDate.of(2021, 1, 31) 17 | 18 | private val month2DayStart = LocalDate.of(2021, 2, 1) 19 | private val month2DayMid = LocalDate.of(2021, 2, 15) 20 | 21 | @Test 22 | fun `6주일이_한_달인_슬라이스를_변환하면_6줄`() { 23 | // Given 1.1(금) ~ 1.31(일) 24 | val calendarSet = CalendarSet( 25 | id, 26 | name, 27 | startDate = month1DayStart, 28 | endDate = month1DayEnd 29 | ) 30 | 31 | // When 변환 32 | val calendarDateList = calendarSetToCalendarDatesList(calendarSet, listOf()) 33 | 34 | assertEquals(6, calendarDateList.size) 35 | } 36 | 37 | @Test 38 | fun `6주일이_두_달_걸친_슬라이스를_변환하면_6줄`() { 39 | // Given 1.15(목) ~ 2.15(월) 40 | val calendarSet = CalendarSet( 41 | id, 42 | name, 43 | startDate = month1DayMid, 44 | endDate = month2DayMid 45 | ) 46 | 47 | // When convert 48 | val calendarDateList = calendarSetToCalendarDatesList(calendarSet, listOf()) 49 | 50 | // Then 6줄 51 | assertEquals(6, calendarDateList.size) 52 | } 53 | 54 | @Test 55 | fun `1주일_보다_짧은_슬라이스를_변환하면_1줄`() { 56 | // Given 1.1 ~ 1.2 57 | val calendarSet = CalendarSet( 58 | id, 59 | name, 60 | startDate = month1DayStart, 61 | endDate = month1Day2 62 | ) 63 | 64 | // When convert 65 | val calendarDateList = calendarSetToCalendarDatesList(calendarSet, listOf()) 66 | 67 | // Then list is not empty 68 | assertEquals(1, calendarDateList.size) 69 | assertTrue(calendarDateList.isNotEmpty()) 70 | assertTrue(calendarDateList[0].isNotEmpty()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /library/src/test/java/com/drunkenboys/ckscalendar/utils/UtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.drunkenboys.ckscalendar.utils 2 | 3 | import com.drunkenboys.ckscalendar.utils.TimeUtils.isSameWeek 4 | import org.junit.Assert.* 5 | 6 | import org.junit.After 7 | import org.junit.Before 8 | import org.junit.Test 9 | import java.time.LocalDate 10 | 11 | class UtilsTest { 12 | 13 | @Before 14 | fun setUp() { 15 | } 16 | 17 | @After 18 | fun tearDown() { 19 | 20 | } 21 | 22 | @Test 23 | fun `저번주_목요일과_이번주_화요일이_다른주인지_테스트`() { 24 | // Given 저번 목요일, 이번 수요일 25 | val prev = LocalDate.of(2021, 11, 4) //목 26 | val next = LocalDate.of(2021, 11, 9) //화 27 | 28 | // Then 다른 주 29 | assertEquals(false, prev.isSameWeek(next)) 30 | } 31 | 32 | @Test 33 | fun `이번주_일요일과_이번주_토요일이_같은주인지_테스트`() { 34 | // Given 이번 일요일, 이번 토요일 35 | val prev = LocalDate.of(2021, 11, 7) //일 36 | val next = LocalDate.of(2021, 11, 13) //토 37 | 38 | // Then 같은 주 39 | assertEquals(true, prev.isSameWeek(next)) 40 | } 41 | 42 | @Test 43 | fun `같은날이_같은주인지_테스트`() { 44 | // Given 이전 목요일, 이후 수요일 45 | val prev = LocalDate.of(2021, 11, 7) 46 | val next = LocalDate.of(2021, 11, 7) 47 | 48 | // Then 같은 주 49 | assertEquals(true, prev.isSameWeek(next)) 50 | } 51 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | } 8 | } 9 | rootProject.name = "달려달력" 10 | include ':app' 11 | include ':library' 12 | --------------------------------------------------------------------------------