From 0d288974e830e942b49f7d09ede7f6edccd3801e Mon Sep 17 00:00:00 2001 From: taetae98coding Date: Mon, 4 Nov 2024 14:43:38 +0900 Subject: [PATCH] 1.0.0-beta01 --- .editorconfig | 13 + .github/actions/ci-setup/action.yml | 10 + .github/workflows/build.yml | 53 + .github/workflows/check_code_style.yml | 30 + .github/workflows/dependency_guard.yml | 37 + .gitignore | 11 + Diary/Diary.xcodeproj/project.pbxproj | 580 ++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Dev.xcscheme | 78 + .../xcshareddata/xcschemes/RealDebug.xcscheme | 78 + .../xcschemes/RealRelease.xcscheme | 78 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/100.png | Bin 0 -> 4849 bytes .../AppIcon.appiconset/1024.png | Bin 0 -> 37749 bytes .../AppIcon.appiconset/114.png | Bin 0 -> 5782 bytes .../AppIcon.appiconset/120.png | Bin 0 -> 6085 bytes .../AppIcon.appiconset/128.png | Bin 0 -> 6644 bytes .../AppIcon.appiconset/144.png | Bin 0 -> 7597 bytes .../AppIcon.appiconset/152.png | Bin 0 -> 8060 bytes .../Assets.xcassets/AppIcon.appiconset/16.png | Bin 0 -> 555 bytes .../AppIcon.appiconset/167.png | Bin 0 -> 9065 bytes .../AppIcon.appiconset/180.png | Bin 0 -> 9731 bytes .../Assets.xcassets/AppIcon.appiconset/20.png | Bin 0 -> 707 bytes .../AppIcon.appiconset/256.png | Bin 0 -> 15003 bytes .../Assets.xcassets/AppIcon.appiconset/29.png | Bin 0 -> 1132 bytes .../Assets.xcassets/AppIcon.appiconset/32.png | Bin 0 -> 1245 bytes .../Assets.xcassets/AppIcon.appiconset/40.png | Bin 0 -> 1617 bytes .../Assets.xcassets/AppIcon.appiconset/50.png | Bin 0 -> 2112 bytes .../AppIcon.appiconset/512.png | Bin 0 -> 35237 bytes .../Assets.xcassets/AppIcon.appiconset/57.png | Bin 0 -> 2521 bytes .../Assets.xcassets/AppIcon.appiconset/58.png | Bin 0 -> 2558 bytes .../Assets.xcassets/AppIcon.appiconset/60.png | Bin 0 -> 2619 bytes .../Assets.xcassets/AppIcon.appiconset/64.png | Bin 0 -> 2917 bytes .../Assets.xcassets/AppIcon.appiconset/72.png | Bin 0 -> 3271 bytes .../Assets.xcassets/AppIcon.appiconset/76.png | Bin 0 -> 3482 bytes .../Assets.xcassets/AppIcon.appiconset/80.png | Bin 0 -> 3697 bytes .../Assets.xcassets/AppIcon.appiconset/87.png | Bin 0 -> 4054 bytes .../AppIcon.appiconset/Contents.json | 1 + Diary/Diary/Assets.xcassets/Contents.json | 6 + Diary/Diary/ContentView.swift | 12 + Diary/Diary/DiaryApp.swift | 15 + Diary/Diary/Info.plist | 19 + .../Preview Assets.xcassets/Contents.json | 6 + Diary/Diary/appstore.png | Bin 0 -> 37749 bytes LICENSE | 201 ++ README.md | 1 + .../account-preferences-datastore/README.md | 3 + .../build.gradle.kts | 18 + .../datastore/AccountDataStorePreferences.kt | 47 + .../AccountDataStorePreferencesModule.kt | 17 + app/core/account-preferences-memory/README.md | 3 + .../build.gradle.kts | 19 + .../memory/AccountMemoryPreferences.kt | 36 + .../memory/AccountPreferencesMemoryModule.kt | 15 + app/core/account-preferences/README.md | 3 + app/core/account-preferences/build.gradle.kts | 13 + .../account/preferences/AccountPreferences.kt | 12 + app/core/calendar-compose/README.md | 3 + app/core/calendar-compose/build.gradle.kts | 47 + .../core/calendar/compose/CalendarPreview.kt | 44 + .../calendar/compose/CalendarTopBarPreview.kt | 15 + .../diary/core/calendar/compose/Calendar.kt | 102 + .../core/calendar/compose/CalendarDefaults.kt | 20 + .../core/calendar/compose/DayOfWeekRow.kt | 80 + .../calendar/compose/color/CalendarColors.kt | 12 + .../compose/day/CalendarDayOfMonth.kt | 78 + .../compose/day/CalendarDayOfMonthState.kt | 16 + .../compose/item/CalendarItemUiState.kt | 36 + .../modifier/CalendarDateRangeSelectable.kt | 84 + .../calendar/compose/month/CalendarMonth.kt | 69 + .../compose/month/CalendarMonthState.kt | 23 + .../calendar/compose/state/CalendarState.kt | 56 + .../compose/state/RememberCalendarState.kt | 20 + .../calendar/compose/topbar/CalendarTopBar.kt | 130 + .../compose/week/CalendarDayOfMonthRow.kt | 45 + .../compose/week/CalendarItemVerticalGrid.kt | 111 + .../calendar/compose/week/CalendarWeek.kt | 65 + .../compose/week/CalendarWeekState.kt | 34 + .../core/calendar/compose/week/WeekItem.kt | 22 + .../core/calendar/compose/week/WeekItemRow.kt | 96 + app/core/coroutines/README.md | 3 + app/core/coroutines/build.gradle.kts | 37 + .../coroutines/CoroutinesModuleExt.android.kt | 8 + .../diary/core/coroutines/CoroutinesModule.kt | 15 + .../core/coroutines/CoroutinesModuleExt.kt | 5 + .../core/coroutines/AppLifecycleOwner.kt | 36 + .../CoroutinesModuleExt.nonAndroid.kt | 7 + app/core/design-system/README.md | 3 + app/core/design-system/build.gradle.kts | 53 + .../color/PlatformColorScheme.android.kt | 17 + .../system/diary/DiaryComponentPreview.kt | 17 + .../design/system/preview/DiaryPreview.kt | 12 + .../design/system/chip/DiaryAssistChip.kt | 46 + .../core/design/system/color/DiaryColor.kt | 10 + .../design/system/color/DiaryColorPicker.kt | 114 + .../system/color/DiaryColorPickerDialog.kt | 64 + .../system/color/DiaryColorPickerState.kt | 49 + .../system/color/PlatformColorScheme.kt | 10 + .../color/RememberDiaryColorPickerState.kt | 15 + .../system/date/DiaryDatePickerDialog.kt | 58 + .../design/system/diary/color/DiaryColor.kt | 52 + .../system/diary/color/DiaryColorState.kt | 31 + .../diary/color/RememberDiaryColorState.kt | 20 + .../system/diary/component/DiaryComponent.kt | 114 + .../diary/component/DiaryComponentState.kt | 71 + .../component/RememberDiaryComponentState.kt | 22 + .../design/system/diary/date/DiaryDate.kt | 146 + .../system/diary/date/DiaryDateState.kt | 55 + .../diary/date/RememberDiaryDateState.kt | 22 + .../core/design/system/dimen/DiaryDimen.kt | 23 + .../diary/core/design/system/emoji/Emoji.kt | 6 + .../core/design/system/text/ClearTextField.kt | 64 + .../system/theme/CompositionLocalExt.kt | 12 + .../core/design/system/theme/DiaryTheme.kt | 57 + .../system/typography/DiaryTypography.kt | 10 + .../color/PlatformColorScheme.nonAndroid.kt | 16 + app/core/diary-database-memory/README.md | 3 + .../diary-database-memory/build.gradle.kts | 21 + .../memory/DiaryMemoryDatabaseModule.kt | 8 + .../database/memory/MemoBackupMemoryDao.kt | 79 + .../diary/database/memory/MemoMemoryDao.kt | 103 + app/core/diary-database-room/README.md | 3 + app/core/diary-database-room/build.gradle.kts | 20 + .../1.json | 136 + .../core/diary/database/room/DiaryDatabase.kt | 30 + .../database/room/DiaryRoomDatabaseModule.kt | 17 + .../core/diary/database/room/dao/EntityDao.kt | 19 + .../database/room/dao/MemoBackupEntityDao.kt | 24 + .../database/room/dao/MemoBackupRoomDao.kt | 53 + .../diary/database/room/dao/MemoEntityDao.kt | 81 + .../diary/database/room/dao/MemoRoomDao.kt | 67 + .../database/room/entity/MemoBackupEntity.kt | 21 + .../diary/database/room/entity/MemoEntity.kt | 34 + .../room/internal/DiaryDatabaseConstructor.kt | 6 + .../diary/database/room/mapper/MemoMapper.kt | 39 + app/core/diary-database/README.md | 3 + app/core/diary-database/build.gradle.kts | 15 + .../core/diary/database/MemoBackupDao.kt | 14 + .../diary/core/diary/database/MemoDao.kt | 20 + app/core/diary-service/README.md | 3 + app/core/diary-service/build.gradle.kts | 20 + .../core/diary/service/DiaryServiceModule.kt | 57 + .../diary/service/account/AccountService.kt | 50 + .../core/diary/service/ext/HttpClientExt.kt | 25 + .../core/diary/service/memo/MemoService.kt | 70 + .../service/plugin/AccountPreferencesOwner.kt | 7 + .../service/plugin/DiaryClientTokenPlugin.kt | 19 + app/core/holiday-database-memory/README.md | 3 + .../holiday-database-memory/build.gradle.kts | 19 + .../database/memory/HolidayMemoryDao.kt | 24 + .../memory/HolidayMemoryDatabaseModule.kt | 15 + app/core/holiday-database-room/README.md | 3 + .../holiday-database-room/build.gradle.kts | 20 + .../1.json | 48 + .../holiday/database/room/HolidayDatabase.kt | 18 + .../holiday/database/room/HolidayEntity.kt | 17 + .../holiday/database/room/HolidayEntityDao.kt | 38 + .../holiday/database/room/HolidayMapper.kt | 20 + .../holiday/database/room/HolidayRoomDao.kt | 23 + .../room/HolidayRoomDatabaseModule.kt | 17 + .../internal/HolidayDatabaseConstructor.kt | 8 + app/core/holiday-database/README.md | 3 + app/core/holiday-database/build.gradle.kts | 15 + .../diary/core/holiday/database/HolidayDao.kt | 11 + .../holiday-preferences-datastore/README.md | 3 + .../build.gradle.kts | 19 + .../datastore/HolidayDataStorePreferences.kt | 51 + .../HolidayDataStorePreferencesModule.kt | 23 + app/core/holiday-preferences-memory/README.md | 3 + .../build.gradle.kts | 19 + .../memory/HolidayMemoryPreferences.kt | 23 + .../memory/HolidayPreferencesMemoryModule.kt | 15 + app/core/holiday-preferences/README.md | 3 + app/core/holiday-preferences/build.gradle.kts | 14 + .../holiday/preferences/HolidayPreferences.kt | 9 + app/core/holiday-service/README.md | 3 + app/core/holiday-service/build.gradle.kts | 16 + .../core/holiday/service/HolidayService.kt | 45 + .../holiday/service/HolidayServiceModule.kt | 55 + .../holiday/service/entity/ApiResultEntity.kt | 10 + .../core/holiday/service/entity/BodyEntity.kt | 13 + .../holiday/service/entity/HolidayEntity.kt | 24 + .../service/entity/HolidayItemEntity.kt | 10 + .../service/entity/HolidayItemsEntity.kt | 10 + .../holiday/service/entity/ResponseEntity.kt | 10 + .../holiday/service/mapper/HolidayMapper.kt | 12 + app/core/model/README.md | 3 + app/core/model/build.gradle.kts | 13 + .../diary/core/model/account/Account.kt | 13 + .../diary/core/model/account/AccountToken.kt | 6 + .../diary/core/model/holiday/Holiday.kt | 9 + .../diary/core/model/mapper/MemoMapper.kt | 15 + .../diary/core/model/memo/Memo.kt | 12 + .../diary/core/model/memo/MemoDetail.kt | 11 + .../diary/core/model/memo/MemoDto.kt | 13 + app/core/navigation/README.md | 3 + app/core/navigation/build.gradle.kts | 15 + .../navigation/account/JoinDestination.kt | 6 + .../navigation/account/LoginDestination.kt | 6 + .../calendar/CalendarDestination.kt | 6 + .../navigation/memo/MemoAddDestination.kt | 13 + .../core/navigation/memo/MemoDestination.kt | 6 + .../navigation/memo/MemoDetailDestination.kt | 10 + .../core/navigation/more/MoreDestination.kt | 6 + app/core/resources/README.md | 3 + app/core/resources/build.gradle.kts | 28 + .../composeResources/values-ko/strings.xml | 47 + .../composeResources/values/strings.xml | 47 + .../diary/core/resources/icon/AccountIcon.kt | 18 + .../diary/core/resources/icon/AddIcon.kt | 18 + .../diary/core/resources/icon/CalendarIcon.kt | 18 + .../diary/core/resources/icon/ClearIcon.kt | 18 + .../diary/core/resources/icon/DeleteIcon.kt | 18 + .../diary/core/resources/icon/DropDownIcon.kt | 18 + .../diary/core/resources/icon/DropUpIcon.kt | 18 + .../diary/core/resources/icon/EmailIcon.kt | 18 + .../diary/core/resources/icon/FinishIcon.kt | 18 + .../diary/core/resources/icon/KeyIcon.kt | 18 + .../diary/core/resources/icon/LoginIcon.kt | 18 + .../diary/core/resources/icon/LogoutIcon.kt | 18 + .../diary/core/resources/icon/MarkdownIcon.kt | 18 + .../diary/core/resources/icon/MemoIcon.kt | 18 + .../diary/core/resources/icon/MoreIcon.kt | 18 + .../core/resources/icon/NavigateUpIcon.kt | 18 + .../diary/core/resources/icon/RefreshIcon.kt | 18 + .../core/resources/icon/TextFieldIcon.kt | 18 + .../core/resources/icon/VisibilityOffIcon.kt | 18 + .../core/resources/icon/VisibilityOnIcon.kt | 18 + app/data/account/README.md | 3 + app/data/account/build.gradle.kts | 15 + .../diary/data/account/AccountDataModule.kt | 8 + .../repository/AccountRepositoryImpl.kt | 48 + app/data/backup/README.md | 3 + app/data/backup/build.gradle.kts | 15 + .../diary/data/backup/BackupDataModule.kt | 8 + .../backup/repository/BackupRepositoryImpl.kt | 38 + app/data/fetch/README.md | 3 + app/data/fetch/build.gradle.kts | 15 + .../diary/data/fetch/FetchDataModule.kt | 9 + .../fetch/repository/FetchRepositoryImpl.kt | 29 + app/data/holiday/README.md | 3 + app/data/holiday/build.gradle.kts | 16 + .../diary/data/holiday/HolidayDataModule.kt | 8 + .../repository/HolidayRepositoryImpl.kt | 131 + app/data/memo/README.md | 3 + app/data/memo/build.gradle.kts | 14 + .../diary/data/memo/MemoDataModule.kt | 8 + .../memo/repository/MemoRepositoryImpl.kt | 70 + app/domain/account/README.md | 3 + app/domain/account/build.gradle.kts | 3 + .../domain/account/AccountDomainModule.kt | 8 + .../account/repository/AccountRepository.kt | 14 + .../account/usecase/GetAccountUseCase.kt | 39 + .../domain/account/usecase/JoinUseCase.kt | 19 + .../domain/account/usecase/LoginUseCase.kt | 18 + .../domain/account/usecase/LogoutUseCase.kt | 13 + app/domain/backup/README.md | 3 + app/domain/backup/build.gradle.kts | 13 + .../diary/domain/backup/BackupDomainModule.kt | 8 + .../backup/repository/BackupRepository.kt | 10 + .../domain/backup/usecase/BackupUseCase.kt | 39 + app/domain/fetch/README.md | 3 + app/domain/fetch/build.gradle.kts | 13 + .../diary/domain/fetch/FetchDomainModule.kt | 8 + .../fetch/repository/FetchRepository.kt | 5 + .../domain/fetch/usecase/FetchUseCase.kt | 31 + app/domain/holiday/README.md | 3 + app/domain/holiday/build.gradle.kts | 3 + .../domain/holiday/HolidayDomainModule.kt | 8 + .../holiday/repository/HolidayRepository.kt | 9 + .../holiday/usecase/FindHolidayUseCase.kt | 24 + app/domain/memo/README.md | 3 + app/domain/memo/build.gradle.kts | 13 + .../diary/domain/memo/MemoDomainModule.kt | 8 + .../domain/memo/repository/MemoRepository.kt | 16 + .../domain/memo/usecase/AddMemoUseCase.kt | 40 + .../domain/memo/usecase/DeleteMemoUseCase.kt | 21 + .../memo/usecase/FindCalendarMemoUseCase.kt | 33 + .../domain/memo/usecase/FindMemoUseCase.kt | 26 + .../domain/memo/usecase/FinishMemoUseCase.kt | 21 + .../domain/memo/usecase/RestartMemoUseCase.kt | 21 + .../domain/memo/usecase/UpdateMemoUseCase.kt | 27 + app/feature/account/README.md | 3 + app/feature/account/build.gradle.kts | 17 + .../feature/account/JoinScreenPreview.kt | 22 + .../feature/account/LoginScreenPreview.kt | 22 + .../feature/account/AccountFeatureModule.kt | 8 + .../feature/account/AccountNavigation.kt | 25 + .../account/common/BasePasswordTextField.kt | 53 + .../feature/account/common/BottomBarButton.kt | 61 + .../feature/account/common/EmailTextField.kt | 45 + .../diary/feature/account/join/JoinRoute.kt | 27 + .../diary/feature/account/join/JoinScreen.kt | 248 ++ .../feature/account/join/JoinViewModel.kt | 60 + .../join/state/JoinScreenButtonUiState.kt | 5 + .../account/join/state/JoinScreenState.kt | 88 + .../feature/account/join/state/JoinUiState.kt | 12 + .../join/state/RememberJoinScreenState.kt | 16 + .../diary/feature/account/login/LoginRoute.kt | 27 + .../feature/account/login/LoginScreen.kt | 198 ++ .../feature/account/login/LoginViewModel.kt | 51 + .../login/state/LoginScreenButtonUiState.kt | 5 + .../account/login/state/LoginScreenState.kt | 69 + .../account/login/state/LoginUiState.kt | 12 + .../login/state/RememberLoginScreenState.kt | 14 + app/feature/calendar/README.md | 3 + app/feature/calendar/build.gradle.kts | 20 + .../feature/calendar/CalendarFeatureModule.kt | 8 + .../feature/calendar/CalendarMemoViewModel.kt | 55 + .../feature/calendar/CalendarNavigation.kt | 19 + .../diary/feature/calendar/CalendarRoute.kt | 53 + .../diary/feature/calendar/CalendarScreen.kt | 54 + .../feature/calendar/CalendarScreenState.kt | 7 + .../feature/calendar/HolidayViewModel.kt | 53 + .../diary/feature/calendar/MemoKey.kt | 6 + .../calendar/RememberCalendarScreenState.kt | 16 + app/feature/memo/README.md | 3 + app/feature/memo/build.gradle.kts | 17 + .../diary/feature/memo/MemoFeatureModule.kt | 8 + .../diary/feature/memo/MemoNavigation.kt | 36 + .../diary/feature/memo/add/MemoAddRoute.kt | 56 + .../feature/memo/add/MemoAddViewModel.kt | 60 + .../memo/detail/MemoDetailActionButton.kt | 10 + .../memo/detail/MemoDetailFloatingButton.kt | 6 + .../memo/detail/MemoDetailNavigationButton.kt | 6 + .../feature/memo/detail/MemoDetailRoute.kt | 50 + .../feature/memo/detail/MemoDetailScreen.kt | 217 ++ .../memo/detail/MemoDetailScreenState.kt | 67 + .../memo/detail/MemoDetailScreenUiState.kt | 13 + .../memo/detail/MemoDetailViewModel.kt | 109 + .../detail/RememberMemoDetailScreenState.kt | 69 + app/feature/more/README.md | 3 + app/feature/more/build.gradle.kts | 17 + .../diary/feature/more/MoreAccountPreview.kt | 48 + .../diary/feature/more/MoreScreenPreview.kt | 18 + .../diary/feature/more/MoreFeatureModule.kt | 8 + .../diary/feature/more/MoreNavigation.kt | 19 + .../diary/feature/more/MoreRoute.kt | 25 + .../diary/feature/more/MoreScreen.kt | 65 + .../diary/feature/more/account/MoreAccount.kt | 205 ++ .../more/account/state/MoreAccountUiState.kt | 13 + .../more/viewmodel/MoreAccountViewModel.kt | 59 + app/platform/android/README.md | 3 + app/platform/android/build.gradle.kts | 111 + .../realReleaseRuntimeClasspath.txt | 269 ++ app/platform/android/keystore/dev.jks | Bin 0 -> 2552 bytes app/platform/android/keystore/real.jks | Bin 0 -> 2552 bytes .../android/src/main/AndroidManifest.xml | 35 + .../src/main/ic_launcher-playstore.png | Bin 0 -> 16890 bytes .../taetae98coding/diary/DiaryActivity.kt | 17 + .../taetae98coding/diary/DiaryApplication.kt | 29 + .../diary/initializer/KoinInitializer.kt | 55 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 1252 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 758 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 1490 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 2282 bytes .../ic_launcher_foreground.webp | Bin 0 -> 3334 bytes .../res/values/ic_launcher_background.xml | 4 + .../android/src/main/res/values/strings.xml | 4 + app/platform/common/README.md | 3 + app/platform/common/build.gradle.kts | 41 + .../io/github/taetae98coding/diary/app/App.kt | 103 + .../taetae98coding/diary/app/AppModule.kt | 52 + .../taetae98coding/diary/app/AppNavHost.kt | 28 + .../diary/app/manager/BackupManager.kt | 22 + .../diary/app/manager/FetchManager.kt | 22 + .../diary/app/navigation/AppNavigation.kt | 30 + .../diary/app/state/AppState.kt | 28 + .../diary/app/state/RememberAppState.kt | 16 + app/platform/ios/README.md | 3 + app/platform/ios/build.gradle.kts | 43 + .../initializer/KoinInitializer.iosArm64.kt | 20 + .../io/github/taetae98coding/diary/IosApp.kt | 24 + .../AppLifecycleOwnerInitializer.kt | 8 + .../initializer/BackupManagerInitializer.kt | 14 + .../initializer/FetchManagerInitializer.kt | 14 + .../diary/initializer/IosInitializer.kt | 9 + .../diary/initializer/KoinInitializer.kt | 43 + .../KoinInitializer.iosSimulatorArm64.kt | 19 + .../initializer/KoinInitializer.iosX64.kt | 19 + app/platform/jvm/README.md | 3 + app/platform/jvm/build.gradle.kts | 94 + .../io/github/taetae98coding/diary/JvmApp.kt | 37 + .../AppLifecycleOwnerInitializer.kt | 13 + .../initializer/BackupManagerInitializer.kt | 14 + .../initializer/FetchManagerInitializer.kt | 14 + .../diary/initializer/JvmInitializer.kt | 9 + .../diary/initializer/KoinInitializer.kt | 48 + app/platform/wasm/README.md | 3 + app/platform/wasm/build.gradle.kts | 73 + .../github/taetae98coding/diary/WasmJsApp.kt | 31 + .../AppLifecycleOwnerInitializer.kt | 8 + .../initializer/BackupManagerInitializer.kt | 14 + .../initializer/FetchManagerInitializer.kt | 14 + .../diary/initializer/JsInitializer.kt | 9 + .../diary/initializer/KoinInitializer.kt | 43 + .../wasm/src/wasmJsMain/resources/index.html | 13 + asset/icon/app_icon_mac.icns | Bin 0 -> 120272 bytes asset/icon/app_icon_round.png | Bin 0 -> 37850 bytes asset/icon/app_icon_square.png | Bin 0 -> 34427 bytes build-logic/build.gradle.kts | 121 + build-logic/settings.gradle.kts | 21 + build-logic/src/main/kotlin/Build.kt | 9 + .../src/main/kotlin/ext/DependencyExt.kt | 83 + .../main/kotlin/ext/KotlinMultiplatformExt.kt | 17 + .../src/main/kotlin/ext/ProjectAndroidExt.kt | 29 + .../src/main/kotlin/ext/ProjectComposeExt.kt | 24 + build-logic/src/main/kotlin/ext/ProjectExt.kt | 79 + .../src/main/kotlin/ext/VersionCatalogExt.kt | 26 + .../android/AndroidApplicationPlugin.kt | 25 + .../plugin/android/AndroidLibraryPlugin.kt | 17 + .../kotlin/plugin/android/AndroidPlugin.kt | 17 + .../kotlin/plugin/compose/ComposePlugin.kt | 34 + .../kotlin/plugin/convention/AppDataPlugin.kt | 30 + .../plugin/convention/AppDomainPlugin.kt | 51 + .../plugin/convention/AppFeaturePlugin.kt | 86 + .../plugin/convention/ServerDataPlugin.kt | 37 + .../plugin/convention/ServerDomainPlugin.kt | 40 + .../plugin/convention/ServerFeaturePlugin.kt | 32 + .../plugin/datastore/DataStorePlugin.kt | 42 + .../main/kotlin/plugin/koin/KoinAllPlugin.kt | 39 + .../kotlin/plugin/koin/KoinCommonPlugin.kt | 39 + .../kotlin/plugin/koin/KoinDataStorePlugin.kt | 41 + .../main/kotlin/plugin/koin/KoinRoomPlugin.kt | 47 + .../plugin/kotlin/KotlinAndroidPlugin.kt | 17 + .../kotlin/plugin/kotlin/KotlinJvmPlugin.kt | 17 + .../kotlin/KotlinMultiplatformAllPlugin.kt | 31 + .../kotlin/KotlinMultiplatformCommonPlugin.kt | 29 + .../kotlin/KotlinMultiplatformPlugin.kt | 17 + .../main/kotlin/plugin/kotlin/KotlinPlugin.kt | 15 + .../src/main/kotlin/plugin/room/RoomPlugin.kt | 60 + build.gradle.kts | 67 + common/exception/README.md | 3 + common/exception/build.gradle.kts | 3 + .../diary/common/exception/ApiException.kt | 6 + .../common/exception/NetworkException.kt | 6 + .../account/AccountNotFoundException.kt | 6 + .../exception/account/ExistEmailException.kt | 6 + .../account/InvalidEmailException.kt | 6 + .../exception/memo/MemoTitleBlankException.kt | 6 + common/model/README.md | 3 + common/model/build.gradle.kts | 15 + .../diary/common/model/memo/MemoEntity.kt | 30 + .../model/request/account/JoinRequest.kt | 12 + .../model/request/account/LoginRequest.kt | 12 + .../common/model/response/DiaryResponse.kt | 28 + .../model/response/account/LoginResponse.kt | 12 + compose-stability-configuration-file.txt | 3 + ...dep_graph_app_core_account_preferences.svg | 0 ...app_core_account_preferences_datastore.svg | 0 ...ph_app_core_account_preferences_memory.svg | 0 .../dep_graph_app_core_calendar_compose.svg | 0 .../graphs/dep_graph_app_core_coroutines.svg | 0 .../dep_graph_app_core_design_system.svg | 0 .../dep_graph_app_core_diary_database.svg | 17 + ...p_graph_app_core_diary_database_memory.svg | 41 + ...dep_graph_app_core_diary_database_room.svg | 57 + .../dep_graph_app_core_diary_service.svg | 41 + .../dep_graph_app_core_holiday_database.svg | 17 + ...graph_app_core_holiday_database_memory.svg | 25 + ...p_graph_app_core_holiday_database_room.svg | 57 + ...dep_graph_app_core_holiday_preferences.svg | 0 ...app_core_holiday_preferences_datastore.svg | 0 ...ph_app_core_holiday_preferences_memory.svg | 0 .../dep_graph_app_core_holiday_service.svg | 17 + .../graphs/dep_graph_app_core_model.svg | 9 + .../graphs/dep_graph_app_core_navigation.svg | 0 .../graphs/dep_graph_app_core_resources.svg | 0 .../graphs/dep_graph_app_data_account.svg | 101 + .../graphs/dep_graph_app_data_backup.svg | 137 + .../graphs/dep_graph_app_data_fetch.svg | 137 + .../graphs/dep_graph_app_data_holiday.svg | 97 + .../images/graphs/dep_graph_app_data_memo.svg | 105 + .../graphs/dep_graph_app_domain_account.svg | 49 + .../graphs/dep_graph_app_domain_backup.svg | 77 + .../graphs/dep_graph_app_domain_fetch.svg | 77 + .../graphs/dep_graph_app_domain_holiday.svg | 49 + .../graphs/dep_graph_app_domain_memo.svg | 77 + .../graphs/dep_graph_app_feature_account.svg | 141 + .../graphs/dep_graph_app_feature_calendar.svg | 221 ++ .../graphs/dep_graph_app_feature_memo.svg | 169 + .../graphs/dep_graph_app_feature_more.svg | 141 + .../graphs/dep_graph_app_platform_android.svg | 813 +++++ .../graphs/dep_graph_app_platform_common.svg | 697 ++++ .../graphs/dep_graph_app_platform_ios.svg | 817 +++++ .../graphs/dep_graph_app_platform_jvm.svg | 825 +++++ .../graphs/dep_graph_app_platform_wasm.svg | 773 +++++ .../graphs/dep_graph_common_exception.svg | 0 docs/images/graphs/dep_graph_common_model.svg | 0 .../images/graphs/dep_graph_library_color.svg | 0 .../graphs/dep_graph_library_coroutines.svg | 0 .../graphs/dep_graph_library_datetime.svg | 0 .../dep_graph_library_koin_datastore.svg | 0 .../graphs/dep_graph_library_koin_room.svg | 0 .../graphs/dep_graph_library_kotlin.svg | 0 .../graphs/dep_graph_library_navigation.svg | 0 docs/images/graphs/dep_graph_library_room.svg | 0 .../graphs/dep_graph_library_shimmer_m3.svg | 0 docs/images/graphs/dep_graph_server_app.svg | 189 ++ .../graphs/dep_graph_server_core_database.svg | 17 + .../graphs/dep_graph_server_core_model.svg | 0 .../graphs/dep_graph_server_data_account.svg | 57 + .../graphs/dep_graph_server_data_memo.svg | 57 + .../dep_graph_server_domain_account.svg | 0 .../graphs/dep_graph_server_domain_memo.svg | 0 .../dep_graph_server_feature_account.svg | 0 .../graphs/dep_graph_server_feature_home.svg | 0 .../graphs/dep_graph_server_feature_memo.svg | 0 generateModuleGraph.sh | 62 + gradle.properties | 12 + gradle/libs.versions.toml | 170 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++ gradlew.bat | 89 + kotlin-js-store/yarn.lock | 2845 +++++++++++++++++ library/color/README.md | 3 + library/color/build.gradle.kts | 19 + .../diary/library/color/ColorExt.kt | 39 + library/coroutines/README.md | 3 + library/coroutines/build.gradle.kts | 13 + .../diary/library/coroutines/FlowExt.kt | 32 + library/datetime/README.md | 3 + library/datetime/build.gradle.kts | 13 + .../diary/library/datetime/DayOfWeekExt.kt | 15 + .../diary/library/datetime/LocalDateExt.kt | 43 + library/koin-datastore/README.md | 3 + library/koin-datastore/build.gradle.kts | 18 + .../koin/datastore/DataStoreExt.android.kt | 11 + .../library/koin/datastore/DataStoreExt.kt | 15 + .../koin/datastore/DataStoreExt.ios.kt | 23 + .../koin/datastore/DataStoreExt.jvm.kt | 11 + library/koin-room/README.md | 3 + library/koin-room/build.gradle.kts | 18 + .../library/koin/room/RoomExt.android.kt | 16 + .../diary/library/koin/room/RoomExt.kt | 16 + .../diary/library/koin/room/RoomExt.ios.kt | 26 + .../diary/library/koin/room/RoomExt.jvm.kt | 15 + library/kotlin/README.md | 3 + library/kotlin/build.gradle.kts | 3 + .../diary/library/kotlin/regex/RegexExt.kt | 5 + library/navigation/README.md | 3 + library/navigation/build.gradle.kts | 19 + .../library/navigation/LocalDateNavType.kt | 19 + library/room/README.md | 3 + library/room/build.gradle.kts | 17 + .../diary/library/room/InstantConverter.kt | 16 + .../diary/library/room/LocalDataConverter.kt | 16 + library/shimmer-m3/README.md | 3 + library/shimmer-m3/build.gradle.kts | 20 + .../library/shimmer/m3/ShimmerModifier.kt | 12 + server/app/README.md | 3 + server/app/build.gradle.kts | 39 + server/app/dependencies/runtimeClasspath.txt | 119 + .../taetae98coding/diary/Application.kt | 23 + .../taetae98coding/diary/plugin/AuthPlugin.kt | 37 + .../taetae98coding/diary/plugin/CORSPlugin.kt | 13 + .../diary/plugin/ContentNegotiationPlugin.kt | 19 + .../diary/plugin/DatabasePlugin.kt | 22 + .../taetae98coding/diary/plugin/KoinPlugin.kt | 21 + .../diary/plugin/RoutingPlugin.kt | 15 + .../app/src/main/resources/application.yaml | 11 + server/core/database/README.md | 3 + server/core/database/build.gradle.kts | 11 + .../diary/core/database/AccountTable.kt | 51 + .../diary/core/database/MemoTable.kt | 83 + server/core/model/README.md | 3 + server/core/model/build.gradle.kts | 7 + .../diary/core/model/Account.kt | 7 + .../taetae98coding/diary/core/model/Memo.kt | 17 + server/data/account/README.md | 3 + server/data/account/build.gradle.kts | 11 + .../diary/data/account/AccountDataModule.kt | 8 + .../repository/AccountRepositoryImpl.kt | 17 + server/data/memo/README.md | 3 + server/data/memo/build.gradle.kts | 11 + .../diary/data/memo/MemoDataModule.kt | 8 + .../memo/repository/MemoRepositoryImpl.kt | 20 + server/domain/account/README.md | 3 + server/domain/account/build.gradle.kts | 3 + .../domain/account/AccountDomainModule.kt | 8 + .../account/repository/AccountRepository.kt | 11 + .../account/usecase/FindAccountUseCase.kt | 19 + .../account/usecase/HashingPasswordUseCase.kt | 21 + .../domain/account/usecase/JoinUseCase.kt | 36 + server/domain/memo/README.md | 3 + server/domain/memo/build.gradle.kts | 3 + .../diary/domain/memo/MemoDomainModule.kt | 8 + .../domain/memo/repository/MemoRepository.kt | 13 + .../domain/memo/usecase/FetchMemoUseCase.kt | 23 + .../domain/memo/usecase/UpsertMemoUseCase.kt | 41 + server/feature/account/README.md | 3 + server/feature/account/build.gradle.kts | 7 + .../diary/feature/account/AccountRouting.kt | 62 + server/feature/home/README.md | 3 + server/feature/home/build.gradle.kts | 3 + .../diary/feature/home/HomeRouting.kt | 11 + server/feature/memo/README.md | 3 + server/feature/memo/build.gradle.kts | 7 + .../diary/feature/memo/MemoRouting.kt | 90 + settings.gradle.kts | 105 + 602 files changed, 23883 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/actions/ci-setup/action.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/check_code_style.yml create mode 100644 .github/workflows/dependency_guard.yml create mode 100644 .gitignore create mode 100644 Diary/Diary.xcodeproj/project.pbxproj create mode 100644 Diary/Diary.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Diary/Diary.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme create mode 100644 Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealDebug.xcscheme create mode 100644 Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealRelease.xcscheme create mode 100644 Diary/Diary/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/100.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/1024.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/114.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/120.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/128.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/144.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/152.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/16.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/167.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/180.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/20.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/256.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/29.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/32.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/40.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/50.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/512.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/57.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/58.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/60.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/64.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/72.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/76.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/80.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/87.png create mode 100644 Diary/Diary/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Diary/Diary/Assets.xcassets/Contents.json create mode 100644 Diary/Diary/ContentView.swift create mode 100644 Diary/Diary/DiaryApp.swift create mode 100644 Diary/Diary/Info.plist create mode 100644 Diary/Diary/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Diary/Diary/appstore.png create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/core/account-preferences-datastore/README.md create mode 100644 app/core/account-preferences-datastore/build.gradle.kts create mode 100644 app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferences.kt create mode 100644 app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferencesModule.kt create mode 100644 app/core/account-preferences-memory/README.md create mode 100644 app/core/account-preferences-memory/build.gradle.kts create mode 100644 app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountMemoryPreferences.kt create mode 100644 app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountPreferencesMemoryModule.kt create mode 100644 app/core/account-preferences/README.md create mode 100644 app/core/account-preferences/build.gradle.kts create mode 100644 app/core/account-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/AccountPreferences.kt create mode 100644 app/core/calendar-compose/README.md create mode 100644 app/core/calendar-compose/build.gradle.kts create mode 100644 app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarPreview.kt create mode 100644 app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarTopBarPreview.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/Calendar.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarDefaults.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/DayOfWeekRow.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/color/CalendarColors.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonth.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonthState.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/item/CalendarItemUiState.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/modifier/CalendarDateRangeSelectable.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonth.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonthState.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/CalendarState.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/RememberCalendarState.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarDayOfMonthRow.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarItemVerticalGrid.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeek.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeekState.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItem.kt create mode 100644 app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItemRow.kt create mode 100644 app/core/coroutines/README.md create mode 100644 app/core/coroutines/build.gradle.kts create mode 100644 app/core/coroutines/src/androidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.android.kt create mode 100644 app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt create mode 100644 app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.kt create mode 100644 app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/AppLifecycleOwner.kt create mode 100644 app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.nonAndroid.kt create mode 100644 app/core/design-system/README.md create mode 100644 app/core/design-system/build.gradle.kts create mode 100644 app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.android.kt create mode 100644 app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/DiaryComponentPreview.kt create mode 100644 app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/preview/DiaryPreview.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/chip/DiaryAssistChip.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColor.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPicker.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerDialog.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/RememberDiaryColorPickerState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/date/DiaryDatePickerDialog.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColor.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColorState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/RememberDiaryColorState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponent.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponentState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/RememberDiaryComponentState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDate.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDateState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/RememberDiaryDateState.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/dimen/DiaryDimen.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/emoji/Emoji.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/text/ClearTextField.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt create mode 100644 app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt create mode 100644 app/core/design-system/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.nonAndroid.kt create mode 100644 app/core/diary-database-memory/README.md create mode 100644 app/core/diary-database-memory/build.gradle.kts create mode 100644 app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/DiaryMemoryDatabaseModule.kt create mode 100644 app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt create mode 100644 app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoMemoryDao.kt create mode 100644 app/core/diary-database-room/README.md create mode 100644 app/core/diary-database-room/build.gradle.kts create mode 100644 app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/1.json create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryRoomDatabaseModule.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/EntityDao.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupEntityDao.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoEntityDao.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoBackupEntity.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoEntity.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/internal/DiaryDatabaseConstructor.kt create mode 100644 app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt create mode 100644 app/core/diary-database/README.md create mode 100644 app/core/diary-database/build.gradle.kts create mode 100644 app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt create mode 100644 app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt create mode 100644 app/core/diary-service/README.md create mode 100644 app/core/diary-service/build.gradle.kts create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/DiaryServiceModule.kt create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/account/AccountService.kt create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/AccountPreferencesOwner.kt create mode 100644 app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/DiaryClientTokenPlugin.kt create mode 100644 app/core/holiday-database-memory/README.md create mode 100644 app/core/holiday-database-memory/build.gradle.kts create mode 100644 app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDao.kt create mode 100644 app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDatabaseModule.kt create mode 100644 app/core/holiday-database-room/README.md create mode 100644 app/core/holiday-database-room/build.gradle.kts create mode 100644 app/core/holiday-database-room/schemas/io.github.taetae98coding.diary.core.holiday.database.room.HolidayDatabase/1.json create mode 100644 app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayDatabase.kt create mode 100644 app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntity.kt create mode 100644 app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntityDao.kt create mode 100644 app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayMapper.kt create mode 100644 app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDao.kt create mode 100644 app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDatabaseModule.kt create mode 100644 app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/internal/HolidayDatabaseConstructor.kt create mode 100644 app/core/holiday-database/README.md create mode 100644 app/core/holiday-database/build.gradle.kts create mode 100644 app/core/holiday-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/HolidayDao.kt create mode 100644 app/core/holiday-preferences-datastore/README.md create mode 100644 app/core/holiday-preferences-datastore/build.gradle.kts create mode 100644 app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferences.kt create mode 100644 app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferencesModule.kt create mode 100644 app/core/holiday-preferences-memory/README.md create mode 100644 app/core/holiday-preferences-memory/build.gradle.kts create mode 100644 app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayMemoryPreferences.kt create mode 100644 app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayPreferencesMemoryModule.kt create mode 100644 app/core/holiday-preferences/README.md create mode 100644 app/core/holiday-preferences/build.gradle.kts create mode 100644 app/core/holiday-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/HolidayPreferences.kt create mode 100644 app/core/holiday-service/README.md create mode 100644 app/core/holiday-service/build.gradle.kts create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayService.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayServiceModule.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ApiResultEntity.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/BodyEntity.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayEntity.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemEntity.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemsEntity.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ResponseEntity.kt create mode 100644 app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/mapper/HolidayMapper.kt create mode 100644 app/core/model/README.md create mode 100644 app/core/model/build.gradle.kts create mode 100644 app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt create mode 100644 app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountToken.kt create mode 100644 app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/holiday/Holiday.kt create mode 100644 app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt create mode 100644 app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/Memo.kt create mode 100644 app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt create mode 100644 app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDto.kt create mode 100644 app/core/navigation/README.md create mode 100644 app/core/navigation/build.gradle.kts create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/JoinDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/LoginDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/calendar/CalendarDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDetailDestination.kt create mode 100644 app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/more/MoreDestination.kt create mode 100644 app/core/resources/README.md create mode 100644 app/core/resources/build.gradle.kts create mode 100644 app/core/resources/src/commonMain/composeResources/values-ko/strings.xml create mode 100644 app/core/resources/src/commonMain/composeResources/values/strings.xml create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AccountIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AddIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/CalendarIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/ClearIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DeleteIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropDownIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropUpIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/EmailIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/FinishIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/KeyIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LoginIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LogoutIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MarkdownIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MemoIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MoreIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/NavigateUpIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/RefreshIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/TextFieldIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOffIcon.kt create mode 100644 app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOnIcon.kt create mode 100644 app/data/account/README.md create mode 100644 app/data/account/build.gradle.kts create mode 100644 app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt create mode 100644 app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt create mode 100644 app/data/backup/README.md create mode 100644 app/data/backup/build.gradle.kts create mode 100644 app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/BackupDataModule.kt create mode 100644 app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt create mode 100644 app/data/fetch/README.md create mode 100644 app/data/fetch/build.gradle.kts create mode 100644 app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/FetchDataModule.kt create mode 100644 app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/FetchRepositoryImpl.kt create mode 100644 app/data/holiday/README.md create mode 100644 app/data/holiday/build.gradle.kts create mode 100644 app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/HolidayDataModule.kt create mode 100644 app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/repository/HolidayRepositoryImpl.kt create mode 100644 app/data/memo/README.md create mode 100644 app/data/memo/build.gradle.kts create mode 100644 app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt create mode 100644 app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt create mode 100644 app/domain/account/README.md create mode 100644 app/domain/account/build.gradle.kts create mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt create mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt create mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt create mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt create mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt create mode 100644 app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt create mode 100644 app/domain/backup/README.md create mode 100644 app/domain/backup/build.gradle.kts create mode 100644 app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/BackupDomainModule.kt create mode 100644 app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt create mode 100644 app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt create mode 100644 app/domain/fetch/README.md create mode 100644 app/domain/fetch/build.gradle.kts create mode 100644 app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/FetchDomainModule.kt create mode 100644 app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/repository/FetchRepository.kt create mode 100644 app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt create mode 100644 app/domain/holiday/README.md create mode 100644 app/domain/holiday/build.gradle.kts create mode 100644 app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/HolidayDomainModule.kt create mode 100644 app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/repository/HolidayRepository.kt create mode 100644 app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/usecase/FindHolidayUseCase.kt create mode 100644 app/domain/memo/README.md create mode 100644 app/domain/memo/build.gradle.kts create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindCalendarMemoUseCase.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt create mode 100644 app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt create mode 100644 app/feature/account/README.md create mode 100644 app/feature/account/build.gradle.kts create mode 100644 app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/JoinScreenPreview.kt create mode 100644 app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/LoginScreenPreview.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountFeatureModule.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountNavigation.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BasePasswordTextField.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BottomBarButton.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/EmailTextField.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinRoute.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenButtonUiState.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenState.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinUiState.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/RememberJoinScreenState.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginRoute.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenButtonUiState.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenState.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginUiState.kt create mode 100644 app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/RememberLoginScreenState.kt create mode 100644 app/feature/calendar/README.md create mode 100644 app/feature/calendar/build.gradle.kts create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarFeatureModule.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarMemoViewModel.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarNavigation.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarRoute.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreen.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreenState.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/HolidayViewModel.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/MemoKey.kt create mode 100644 app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/RememberCalendarScreenState.kt create mode 100644 app/feature/memo/README.md create mode 100644 app/feature/memo/build.gradle.kts create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoFeatureModule.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoNavigation.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt create mode 100644 app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenState.kt create mode 100644 app/feature/more/README.md create mode 100644 app/feature/more/build.gradle.kts create mode 100644 app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreAccountPreview.kt create mode 100644 app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreenPreview.kt create mode 100644 app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreFeatureModule.kt create mode 100644 app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreNavigation.kt create mode 100644 app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreRoute.kt create mode 100644 app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreen.kt create mode 100644 app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/MoreAccount.kt create mode 100644 app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/state/MoreAccountUiState.kt create mode 100644 app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt create mode 100644 app/platform/android/README.md create mode 100644 app/platform/android/build.gradle.kts create mode 100644 app/platform/android/dependencies/realReleaseRuntimeClasspath.txt create mode 100644 app/platform/android/keystore/dev.jks create mode 100644 app/platform/android/keystore/real.jks create mode 100644 app/platform/android/src/main/AndroidManifest.xml create mode 100644 app/platform/android/src/main/ic_launcher-playstore.png create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt create mode 100644 app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt create mode 100644 app/platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/platform/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/platform/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/platform/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/platform/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/platform/android/src/main/res/values/ic_launcher_background.xml create mode 100644 app/platform/android/src/main/res/values/strings.xml create mode 100644 app/platform/common/README.md create mode 100644 app/platform/common/build.gradle.kts create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/BackupManager.kt create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FetchManager.kt create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/AppState.kt create mode 100644 app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/RememberAppState.kt create mode 100644 app/platform/ios/README.md create mode 100644 app/platform/ios/build.gradle.kts create mode 100644 app/platform/ios/src/iosArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosArm64.kt create mode 100644 app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/IosApp.kt create mode 100644 app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt create mode 100644 app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt create mode 100644 app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt create mode 100644 app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt create mode 100644 app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt create mode 100644 app/platform/ios/src/iosSimulatorArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosSimulatorArm64.kt create mode 100644 app/platform/ios/src/iosX64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosX64.kt create mode 100644 app/platform/jvm/README.md create mode 100644 app/platform/jvm/build.gradle.kts create mode 100644 app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt create mode 100644 app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt create mode 100644 app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt create mode 100644 app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt create mode 100644 app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/JvmInitializer.kt create mode 100644 app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt create mode 100644 app/platform/wasm/README.md create mode 100644 app/platform/wasm/build.gradle.kts create mode 100644 app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmJsApp.kt create mode 100644 app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt create mode 100644 app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt create mode 100644 app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt create mode 100644 app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/JsInitializer.kt create mode 100644 app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt create mode 100644 app/platform/wasm/src/wasmJsMain/resources/index.html create mode 100644 asset/icon/app_icon_mac.icns create mode 100644 asset/icon/app_icon_round.png create mode 100644 asset/icon/app_icon_square.png create mode 100644 build-logic/build.gradle.kts create mode 100644 build-logic/settings.gradle.kts create mode 100644 build-logic/src/main/kotlin/Build.kt create mode 100644 build-logic/src/main/kotlin/ext/DependencyExt.kt create mode 100644 build-logic/src/main/kotlin/ext/KotlinMultiplatformExt.kt create mode 100644 build-logic/src/main/kotlin/ext/ProjectAndroidExt.kt create mode 100644 build-logic/src/main/kotlin/ext/ProjectComposeExt.kt create mode 100644 build-logic/src/main/kotlin/ext/ProjectExt.kt create mode 100644 build-logic/src/main/kotlin/ext/VersionCatalogExt.kt create mode 100644 build-logic/src/main/kotlin/plugin/android/AndroidApplicationPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/android/AndroidLibraryPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/android/AndroidPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/compose/ComposePlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/convention/AppDataPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/convention/AppDomainPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/convention/ServerDataPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/convention/ServerDomainPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/convention/ServerFeaturePlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/koin/KoinAllPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/koin/KoinCommonPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/koin/KoinDataStorePlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/koin/KoinRoomPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/kotlin/KotlinAndroidPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/kotlin/KotlinJvmPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/kotlin/KotlinPlugin.kt create mode 100644 build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt create mode 100644 build.gradle.kts create mode 100644 common/exception/README.md create mode 100644 common/exception/build.gradle.kts create mode 100644 common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/ApiException.kt create mode 100644 common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/NetworkException.kt create mode 100644 common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/AccountNotFoundException.kt create mode 100644 common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/ExistEmailException.kt create mode 100644 common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/InvalidEmailException.kt create mode 100644 common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/memo/MemoTitleBlankException.kt create mode 100644 common/model/README.md create mode 100644 common/model/build.gradle.kts create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/JoinRequest.kt create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/LoginRequest.kt create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt create mode 100644 common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/account/LoginResponse.kt create mode 100644 compose-stability-configuration-file.txt create mode 100644 docs/images/graphs/dep_graph_app_core_account_preferences.svg create mode 100644 docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg create mode 100644 docs/images/graphs/dep_graph_app_core_account_preferences_memory.svg create mode 100644 docs/images/graphs/dep_graph_app_core_calendar_compose.svg create mode 100644 docs/images/graphs/dep_graph_app_core_coroutines.svg create mode 100644 docs/images/graphs/dep_graph_app_core_design_system.svg create mode 100644 docs/images/graphs/dep_graph_app_core_diary_database.svg create mode 100644 docs/images/graphs/dep_graph_app_core_diary_database_memory.svg create mode 100644 docs/images/graphs/dep_graph_app_core_diary_database_room.svg create mode 100644 docs/images/graphs/dep_graph_app_core_diary_service.svg create mode 100644 docs/images/graphs/dep_graph_app_core_holiday_database.svg create mode 100644 docs/images/graphs/dep_graph_app_core_holiday_database_memory.svg create mode 100644 docs/images/graphs/dep_graph_app_core_holiday_database_room.svg create mode 100644 docs/images/graphs/dep_graph_app_core_holiday_preferences.svg create mode 100644 docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg create mode 100644 docs/images/graphs/dep_graph_app_core_holiday_preferences_memory.svg create mode 100644 docs/images/graphs/dep_graph_app_core_holiday_service.svg create mode 100644 docs/images/graphs/dep_graph_app_core_model.svg create mode 100644 docs/images/graphs/dep_graph_app_core_navigation.svg create mode 100644 docs/images/graphs/dep_graph_app_core_resources.svg create mode 100644 docs/images/graphs/dep_graph_app_data_account.svg create mode 100644 docs/images/graphs/dep_graph_app_data_backup.svg create mode 100644 docs/images/graphs/dep_graph_app_data_fetch.svg create mode 100644 docs/images/graphs/dep_graph_app_data_holiday.svg create mode 100644 docs/images/graphs/dep_graph_app_data_memo.svg create mode 100644 docs/images/graphs/dep_graph_app_domain_account.svg create mode 100644 docs/images/graphs/dep_graph_app_domain_backup.svg create mode 100644 docs/images/graphs/dep_graph_app_domain_fetch.svg create mode 100644 docs/images/graphs/dep_graph_app_domain_holiday.svg create mode 100644 docs/images/graphs/dep_graph_app_domain_memo.svg create mode 100644 docs/images/graphs/dep_graph_app_feature_account.svg create mode 100644 docs/images/graphs/dep_graph_app_feature_calendar.svg create mode 100644 docs/images/graphs/dep_graph_app_feature_memo.svg create mode 100644 docs/images/graphs/dep_graph_app_feature_more.svg create mode 100644 docs/images/graphs/dep_graph_app_platform_android.svg create mode 100644 docs/images/graphs/dep_graph_app_platform_common.svg create mode 100644 docs/images/graphs/dep_graph_app_platform_ios.svg create mode 100644 docs/images/graphs/dep_graph_app_platform_jvm.svg create mode 100644 docs/images/graphs/dep_graph_app_platform_wasm.svg create mode 100644 docs/images/graphs/dep_graph_common_exception.svg create mode 100644 docs/images/graphs/dep_graph_common_model.svg create mode 100644 docs/images/graphs/dep_graph_library_color.svg create mode 100644 docs/images/graphs/dep_graph_library_coroutines.svg create mode 100644 docs/images/graphs/dep_graph_library_datetime.svg create mode 100644 docs/images/graphs/dep_graph_library_koin_datastore.svg create mode 100644 docs/images/graphs/dep_graph_library_koin_room.svg create mode 100644 docs/images/graphs/dep_graph_library_kotlin.svg create mode 100644 docs/images/graphs/dep_graph_library_navigation.svg create mode 100644 docs/images/graphs/dep_graph_library_room.svg create mode 100644 docs/images/graphs/dep_graph_library_shimmer_m3.svg create mode 100644 docs/images/graphs/dep_graph_server_app.svg create mode 100644 docs/images/graphs/dep_graph_server_core_database.svg create mode 100644 docs/images/graphs/dep_graph_server_core_model.svg create mode 100644 docs/images/graphs/dep_graph_server_data_account.svg create mode 100644 docs/images/graphs/dep_graph_server_data_memo.svg create mode 100644 docs/images/graphs/dep_graph_server_domain_account.svg create mode 100644 docs/images/graphs/dep_graph_server_domain_memo.svg create mode 100644 docs/images/graphs/dep_graph_server_feature_account.svg create mode 100644 docs/images/graphs/dep_graph_server_feature_home.svg create mode 100644 docs/images/graphs/dep_graph_server_feature_memo.svg create mode 100644 generateModuleGraph.sh create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 kotlin-js-store/yarn.lock create mode 100644 library/color/README.md create mode 100644 library/color/build.gradle.kts create mode 100644 library/color/src/commonMain/kotlin/io/github/taetae98coding/diary/library/color/ColorExt.kt create mode 100644 library/coroutines/README.md create mode 100644 library/coroutines/build.gradle.kts create mode 100644 library/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/library/coroutines/FlowExt.kt create mode 100644 library/datetime/README.md create mode 100644 library/datetime/build.gradle.kts create mode 100644 library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/DayOfWeekExt.kt create mode 100644 library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/LocalDateExt.kt create mode 100644 library/koin-datastore/README.md create mode 100644 library/koin-datastore/build.gradle.kts create mode 100644 library/koin-datastore/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.android.kt create mode 100644 library/koin-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.kt create mode 100644 library/koin-datastore/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt create mode 100644 library/koin-datastore/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.jvm.kt create mode 100644 library/koin-room/README.md create mode 100644 library/koin-room/build.gradle.kts create mode 100644 library/koin-room/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.android.kt create mode 100644 library/koin-room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.kt create mode 100644 library/koin-room/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt create mode 100644 library/koin-room/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.jvm.kt create mode 100644 library/kotlin/README.md create mode 100644 library/kotlin/build.gradle.kts create mode 100644 library/kotlin/src/commonMain/kotlin/io/github/taetae98coding/diary/library/kotlin/regex/RegexExt.kt create mode 100644 library/navigation/README.md create mode 100644 library/navigation/build.gradle.kts create mode 100644 library/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/library/navigation/LocalDateNavType.kt create mode 100644 library/room/README.md create mode 100644 library/room/build.gradle.kts create mode 100644 library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/InstantConverter.kt create mode 100644 library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/LocalDataConverter.kt create mode 100644 library/shimmer-m3/README.md create mode 100644 library/shimmer-m3/build.gradle.kts create mode 100644 library/shimmer-m3/src/commonMain/kotlin/io/github/taetae98coding/diary/library/shimmer/m3/ShimmerModifier.kt create mode 100644 server/app/README.md create mode 100644 server/app/build.gradle.kts create mode 100644 server/app/dependencies/runtimeClasspath.txt create mode 100644 server/app/src/main/kotlin/io/github/taetae98coding/diary/Application.kt create mode 100644 server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/AuthPlugin.kt create mode 100644 server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/CORSPlugin.kt create mode 100644 server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/ContentNegotiationPlugin.kt create mode 100644 server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt create mode 100644 server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt create mode 100644 server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt create mode 100644 server/app/src/main/resources/application.yaml create mode 100644 server/core/database/README.md create mode 100644 server/core/database/build.gradle.kts create mode 100644 server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt create mode 100644 server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt create mode 100644 server/core/model/README.md create mode 100644 server/core/model/build.gradle.kts create mode 100644 server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Account.kt create mode 100644 server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt create mode 100644 server/data/account/README.md create mode 100644 server/data/account/build.gradle.kts create mode 100644 server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt create mode 100644 server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt create mode 100644 server/data/memo/README.md create mode 100644 server/data/memo/build.gradle.kts create mode 100644 server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt create mode 100644 server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt create mode 100644 server/domain/account/README.md create mode 100644 server/domain/account/build.gradle.kts create mode 100644 server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt create mode 100644 server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt create mode 100644 server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt create mode 100644 server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/HashingPasswordUseCase.kt create mode 100644 server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt create mode 100644 server/domain/memo/README.md create mode 100644 server/domain/memo/build.gradle.kts create mode 100644 server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt create mode 100644 server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt create mode 100644 server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FetchMemoUseCase.kt create mode 100644 server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt create mode 100644 server/feature/account/README.md create mode 100644 server/feature/account/build.gradle.kts create mode 100644 server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt create mode 100644 server/feature/home/README.md create mode 100644 server/feature/home/build.gradle.kts create mode 100644 server/feature/home/src/main/kotlin/io/github/taetae98coding/diary/feature/home/HomeRouting.kt create mode 100644 server/feature/memo/README.md create mode 100644 server/feature/memo/build.gradle.kts create mode 100644 server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt create mode 100644 settings.gradle.kts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..5186e0a6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[{*.kt,*.kts}] +max_line_length = 300 +insert_final_newline = true +trim_trailing_whitespace = true + +ij_kotlin_imports_layout = unset +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true + +ktlint_experimental = disabled +ktlint_standard_function-signature = disabled \ No newline at end of file diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml new file mode 100644 index 00000000..00e4e0de --- /dev/null +++ b/.github/actions/ci-setup/action.yml @@ -0,0 +1,10 @@ +name: CI setup + +runs: + using: composite + steps: + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..16cb440a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,53 @@ +name: Build + +on: [ push, pull_request ] + +jobs: + Linux-Build: + runs-on: ubuntu-latest + strategy: + matrix: + command: [ + './gradlew :app:platform:jvm:assemble', + './gradlew :app:platform:wasm:assemble', + './gradlew :app:platform:android:assembleRealRelease', + ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: CI setup + uses: './.github/actions/ci-setup' + + - name: Set local.properties + run: | + echo diary.dev.api.base.url=${{ secrets.DIARY_DEV_API_BASE_URL }} >> local.properties + echo diary.real.api.base.url=${{ secrets.DIARY_REAL_API_BASE_URL }} >> local.properties + echo holiday.dev.api.url=${{ secrets.HOLIDAY_DEV_API_URL }} >> local.properties + echo holiday.dev.api.key=${{ secrets.HOLIDAY_DEV_API_KEY }} >> local.properties + echo holiday.real.api.url=${{ secrets.HOLIDAY_REAL_API_URL }} >> local.properties + echo holiday.real.api.key=${{ secrets.HOLIDAY_REAL_API_KEY }} >> local.properties + echo android.dev.store.password=${{ secrets.ANDROID_DEV_STORE_PASSWORD }} >> local.properties + echo android.dev.key.alias=${{ secrets.ANDROID_DEV_KEY_ALIAS }} >> local.properties + echo android.dev.key.password=${{ secrets.ANDROID_DEV_KEY_PASSWORD }} >> local.properties + echo android.real.store.password=${{ secrets.ANDROID_REAL_STORE_PASSWORD }} >> local.properties + echo android.real.key.alias=${{ secrets.ANDROID_REAL_KEY_ALIAS }} >> local.properties + echo android.real.key.password=${{ secrets.ANDROID_REAL_KEY_PASSWORD }} >> local.properties + + - name: Build ${{ matrix.command }} + run: ${{ matrix.command }} + + Mac-Build: + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: CI setup + uses: './.github/actions/ci-setup' + +# - name: Change Directory to iOS Project +# run: cd Diary + + - name: Build iOS + run: xcodebuild -destination 'platform=iOS Simulator,name=iPhone 15,OS=18.0' \ No newline at end of file diff --git a/.github/workflows/check_code_style.yml b/.github/workflows/check_code_style.yml new file mode 100644 index 00000000..f855de37 --- /dev/null +++ b/.github/workflows/check_code_style.yml @@ -0,0 +1,30 @@ +name: Check Code Style + +on: [ push, pull_request ] + +jobs: + Spotless: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: CI setup + uses: './.github/actions/ci-setup' + + - name: Set local.properties + run: | + echo diary.dev.api.base.url=${{ secrets.DIARY_DEV_API_BASE_URL }} >> local.properties + echo diary.real.api.base.url=${{ secrets.DIARY_REAL_API_BASE_URL }} >> local.properties + echo holiday.dev.api.url=${{ secrets.HOLIDAY_DEV_API_URL }} >> local.properties + echo holiday.dev.api.key=${{ secrets.HOLIDAY_DEV_API_KEY }} >> local.properties + echo holiday.real.api.url=${{ secrets.HOLIDAY_REAL_API_URL }} >> local.properties + echo holiday.real.api.key=${{ secrets.HOLIDAY_REAL_API_KEY }} >> local.properties + echo android.dev.store.password=${{ secrets.ANDROID_DEV_STORE_PASSWORD }} >> local.properties + echo android.dev.key.alias=${{ secrets.ANDROID_DEV_KEY_ALIAS }} >> local.properties + echo android.dev.key.password=${{ secrets.ANDROID_DEV_KEY_PASSWORD }} >> local.properties + echo android.real.store.password=${{ secrets.ANDROID_REAL_STORE_PASSWORD }} >> local.properties + echo android.real.key.alias=${{ secrets.ANDROID_REAL_KEY_ALIAS }} >> local.properties + echo android.real.key.password=${{ secrets.ANDROID_REAL_KEY_PASSWORD }} >> local.properties + + - run: ./gradlew :spotlessCheck \ No newline at end of file diff --git a/.github/workflows/dependency_guard.yml b/.github/workflows/dependency_guard.yml new file mode 100644 index 00000000..d564cc14 --- /dev/null +++ b/.github/workflows/dependency_guard.yml @@ -0,0 +1,37 @@ +name: Dependency Guard + +on: [ push, pull_request ] + +jobs: + Dependency-Guard: + runs-on: ubuntu-latest + strategy: + matrix: + command: [ + './gradlew :app:platform:android:dependencyGuard', + './gradlew :server:app:dependencyGuard' + ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: CI setup + uses: './.github/actions/ci-setup' + + - name: Set local.properties + run: | + echo diary.dev.api.base.url=${{ secrets.DIARY_DEV_API_BASE_URL }} >> local.properties + echo diary.real.api.base.url=${{ secrets.DIARY_REAL_API_BASE_URL }} >> local.properties + echo holiday.dev.api.url=${{ secrets.HOLIDAY_DEV_API_URL }} >> local.properties + echo holiday.dev.api.key=${{ secrets.HOLIDAY_DEV_API_KEY }} >> local.properties + echo holiday.real.api.url=${{ secrets.HOLIDAY_REAL_API_URL }} >> local.properties + echo holiday.real.api.key=${{ secrets.HOLIDAY_REAL_API_KEY }} >> local.properties + echo android.dev.store.password=${{ secrets.ANDROID_DEV_STORE_PASSWORD }} >> local.properties + echo android.dev.key.alias=${{ secrets.ANDROID_DEV_KEY_ALIAS }} >> local.properties + echo android.dev.key.password=${{ secrets.ANDROID_DEV_KEY_PASSWORD }} >> local.properties + echo android.real.store.password=${{ secrets.ANDROID_REAL_STORE_PASSWORD }} >> local.properties + echo android.real.key.alias=${{ secrets.ANDROID_REAL_KEY_ALIAS }} >> local.properties + echo android.real.key.password=${{ secrets.ANDROID_REAL_KEY_PASSWORD }} >> local.properties + + - name: Build ${{ matrix.command }} + run: ${{ matrix.command }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..47eb0056 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +**/.gradle +**/.idea +**/.kotlin + +**/local.properties + +**/build + +**/xcuserdata +**/dev.xcconfig +**/real.xcconfig \ No newline at end of file diff --git a/Diary/Diary.xcodeproj/project.pbxproj b/Diary/Diary.xcodeproj/project.pbxproj new file mode 100644 index 00000000..7f4f8b90 --- /dev/null +++ b/Diary/Diary.xcodeproj/project.pbxproj @@ -0,0 +1,580 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 3D604D262CCFF7B500D1CC30 /* dev.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 3D604D252CCFF7B500D1CC30 /* dev.xcconfig */; }; + 3D604D282CCFF83200D1CC30 /* real.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 3D604D272CCFF83200D1CC30 /* real.xcconfig */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3D604D252CCFF7B500D1CC30 /* dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = dev.xcconfig; sourceTree = ""; }; + 3D604D272CCFF83200D1CC30 /* real.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = real.xcconfig; sourceTree = ""; }; + 3DDC71F62CCD5903001193A2 /* Diary.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Diary.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 3DDC72092CCD5A0B001193A2 /* Exceptions for "Diary" folder in "Diary" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 3DDC71F52CCD5903001193A2 /* Diary */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 3DDC71F82CCD5903001193A2 /* Diary */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 3DDC72092CCD5A0B001193A2 /* Exceptions for "Diary" folder in "Diary" target */, + ); + path = Diary; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3DDC71F32CCD5903001193A2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3DDC71ED2CCD5903001193A2 = { + isa = PBXGroup; + children = ( + 3D604D272CCFF83200D1CC30 /* real.xcconfig */, + 3D604D252CCFF7B500D1CC30 /* dev.xcconfig */, + 3DDC71F82CCD5903001193A2 /* Diary */, + 3DDC71F72CCD5903001193A2 /* Products */, + ); + sourceTree = ""; + }; + 3DDC71F72CCD5903001193A2 /* Products */ = { + isa = PBXGroup; + children = ( + 3DDC71F62CCD5903001193A2 /* Diary.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3DDC71F52CCD5903001193A2 /* Diary */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3DDC72042CCD5905001193A2 /* Build configuration list for PBXNativeTarget "Diary" */; + buildPhases = ( + 3DDC72072CCD5943001193A2 /* ShellScript */, + 3DDC71F22CCD5903001193A2 /* Sources */, + 3DDC71F32CCD5903001193A2 /* Frameworks */, + 3DDC71F42CCD5903001193A2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 3DDC71F82CCD5903001193A2 /* Diary */, + ); + name = Diary; + packageProductDependencies = ( + ); + productName = Diary; + productReference = 3DDC71F62CCD5903001193A2 /* Diary.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3DDC71EE2CCD5903001193A2 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 3DDC71F52CCD5903001193A2 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 3DDC71F12CCD5903001193A2 /* Build configuration list for PBXProject "Diary" */; + developmentRegion = ko; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ko, + ); + mainGroup = 3DDC71ED2CCD5903001193A2; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 3DDC71F72CCD5903001193A2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3DDC71F52CCD5903001193A2 /* Diary */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3DDC71F42CCD5903001193A2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D604D282CCFF83200D1CC30 /* real.xcconfig in Resources */, + 3D604D262CCFF7B500D1CC30 /* dev.xcconfig in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3DDC72072CCD5943001193A2 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew :app:platform:ios:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3DDC71F22CCD5903001193A2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 3D709E182CDB092800F8132A /* RealDebug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D272CCFF83200D1CC30 /* real.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = RealDebug; + }; + 3D709E192CDB092800F8132A /* RealDebug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D272CCFF83200D1CC30 /* real.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; + DEVELOPMENT_TEAM = 4TV6L66XZ8; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Diary/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + KOTLIN_FRAMEWORK_BUILD_TYPE = debug; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0-beta01"; + PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.debug; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = RealDebug; + }; + 3D709E1A2CDB093800F8132A /* RealRelease */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D272CCFF83200D1CC30 /* real.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = RealRelease; + }; + 3D709E1B2CDB093800F8132A /* RealRelease */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D272CCFF83200D1CC30 /* real.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; + DEVELOPMENT_TEAM = 4TV6L66XZ8; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Diary/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + KOTLIN_FRAMEWORK_BUILD_TYPE = release; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0-beta01"; + PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = RealRelease; + }; + 3D709E1C2CDB096E00F8132A /* DevDebug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D252CCFF7B500D1CC30 /* dev.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = DevDebug; + }; + 3D709E1D2CDB096E00F8132A /* DevDebug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D252CCFF7B500D1CC30 /* dev.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; + DEVELOPMENT_TEAM = 4TV6L66XZ8; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Diary/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + KOTLIN_FRAMEWORK_BUILD_TYPE = debug; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0-beta01"; + PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev.debug; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DevDebug; + }; + 3D709E1E2CDB097D00F8132A /* DevRelease */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D252CCFF7B500D1CC30 /* dev.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = DevRelease; + }; + 3D709E1F2CDB097D00F8132A /* DevRelease */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D604D252CCFF7B500D1CC30 /* dev.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Diary/Preview Content\""; + DEVELOPMENT_TEAM = 4TV6L66XZ8; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Diary/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + KOTLIN_FRAMEWORK_BUILD_TYPE = release; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = "1.0.0-beta01"; + PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DevRelease; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3DDC71F12CCD5903001193A2 /* Build configuration list for PBXProject "Diary" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D709E1C2CDB096E00F8132A /* DevDebug */, + 3D709E182CDB092800F8132A /* RealDebug */, + 3D709E1E2CDB097D00F8132A /* DevRelease */, + 3D709E1A2CDB093800F8132A /* RealRelease */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DevDebug; + }; + 3DDC72042CCD5905001193A2 /* Build configuration list for PBXNativeTarget "Diary" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3D709E1D2CDB096E00F8132A /* DevDebug */, + 3D709E192CDB092800F8132A /* RealDebug */, + 3D709E1F2CDB097D00F8132A /* DevRelease */, + 3D709E1B2CDB093800F8132A /* RealRelease */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DevDebug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3DDC71EE2CCD5903001193A2 /* Project object */; +} diff --git a/Diary/Diary.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Diary/Diary.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Diary/Diary.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Diary/Diary.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme new file mode 100644 index 00000000..4b29e5a3 --- /dev/null +++ b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/Dev.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealDebug.xcscheme b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealDebug.xcscheme new file mode 100644 index 00000000..8c2380b1 --- /dev/null +++ b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealDebug.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealRelease.xcscheme b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealRelease.xcscheme new file mode 100644 index 00000000..4fbddea6 --- /dev/null +++ b/Diary/Diary.xcodeproj/xcshareddata/xcschemes/RealRelease.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Diary/Diary/Assets.xcassets/AccentColor.colorset/Contents.json b/Diary/Diary/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Diary/Diary/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/100.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000000000000000000000000000000000000..0f5b1440eaec033aacd7f697e89820bbb9662def GIT binary patch literal 4849 zcmVPx{t4TybRCr$PU3qX*)f)e$P1kgTvbVH=Ahf7FHXk5Q)ImUYMGz1WL54+~aYP*) zcl|>haoiAD1cpTs5Kvh}KtzyL9;@uDqU^LG-PgSLJMd+0L((LrH%al{nM~T`=H7F@ z-?x9?IoD*j+wJi3;Hf1gB`7W~R_~>yrQm1s^71mbCvkCc*Mdj_lAD{0f`S4= zIs$|Xr6eLE0@2aYFqM{;+G#r|C5E~R3?c-C(m`8dDk>_n(|#C7;3+^mV#>?Qvm35% z;K87j&;>SSXJ^}us603TVf1dY*=%-ZaSi(sSWqY-%m$mRR;xD|_`r_#A98n9PcZ-q z9<&u8nVFehR}6z^=(p}j3_yYmTLTaSkRYQ#kPXL>5Cf1Pqd<@i$B+;MkRYQ#kPXK# z5+c1xRVh8ghCn&k?B=gScR)BXCe3&-i7B~%e@(C=_mP4Tf#BaP-fpH9m_|Cu44G>l! z6A}`zYSk({_0&@ekmk*sqeqV(7&U5?;yAv7NsY;vw{G2vZr!>$#&`GKcjJpMzCcn^ zk^)6fNznxmNhPy1%*p)y_upk0VaSjnm^EvbYCF<%a=vh^s8TixcJJODd-m*61tT&I zRaS!r4X|d-8U+gb*Yp~H{N(dwdF{YIc*@Br+@$cSg>FLtX8XTs;klgA*Ye>oM4+aZImvi zS`x2C3}x-wwXu8mZq;cK=mrZJGXdz(p#x5yJgFQJ88gSv?`Q*m`|UR+IeLw`(gEQl zNaX$d_oH3Ac8*E8;@T}+wxCt3R-l^DO9gl%EgUmDJ~nOIgwCBiD?{$Q&Vy1-b+v8V zHg&FERn|%eL=5BV)vM97XHP|FtxvBd(67J#s-!5;fXJAaEnBA4l1QzLl@dZgC?N!d zO>I>U5PDaA`t&KIuC!p#edOfsxZ@7pJr_?>q6K8YfB_DbC&Y*apHus75CkDAKrcUT@`HUITmR1Lwrn3Q8C!!Y)s^FcS5Dp+qYNHbjl5m5Lp#$2+d z&Xd!wf9JV+O`c4lEeB{9{EKAq92o=8<=FI2Dmpy6K=);dv8V4(P9!1pT+K!97qCfa z+A-~>2)utFgH+um#GO7D`yuP=4j(>@Lx&FG(xpr2+O;cc)~u<^61#;97b-7`xhkgB z`ASYsM)m5|6%e!&vKk-|!1x^yQ2{jgDeq_zTANwv#K=yk_~o_^vm0xMYglh zGJ@o)C<)9}9Y21&44+Us+qP|s-o1OPOmDMh%~VF0lIrVTB{${-10^2=gytVW%gzpC`wa$dO99`^id2QIuy)5@%m!3MBT}|085Z$ z5kFPh2OoTZsZ*ySEiDa9Iuc}mBtdnR5Kfj{OJmKzYTdeZN)2$L_uO-j0|;k5P2wdc zCMs@~@~_rzxHGoSfU)NC#1l_o!h{J*?Rm(w@gsd&@!+T_qaTRF485jh5UxhK*zz`>ZI7BTD59{ z`77GCPe1)ssW5M^;f=j;b&hTlA{{48SCg=#M~_xw&+;b;D=EhaY~ZRG4<( zE_V|aFH7F33dfEeiz!p4_)JWWfs(R!?_On_s#UAzV2p|q+|%!L1B5r0m8Fs{q2csq zS2g)9iAtBWU%!4TCnl+S=UweYGN{T#DrDlU08&L#eN|-Z5Me9{9`(u~=&rKl29mn2 zh(Awa5I|mos&2`WB@PCY>xoM$#+dcTcNBAAm^X1JgGInkf{$da<aY$8HTk#3)LBsWLcBTiakZmtGcgO?m*r z)=~n()Vw#To12}V?h-3NU>vq9qC zd+$}nFW!TNB%UMt*Gn^=F91p381KVAU%I{f_U%*gh(9_lbyHc|6gtx!R9#lZx8?X+LaZ%Ia2 zTQkvKK&y9aFQ&Kvg3ryk4Y>_kr0N`!$t!( zH?Xno=bwL8-;ubC3n&S+9a1ad^+{~1LV80i8?(2IHiidP76D`_mQ_E-E?l|ACOMH# zN)o$USbt|RjOAvIk9I?E&nh61x~L&?^Tv%ERck-Fng{!^sQ<;-6AZQs@nD?6MhC_N zO`0@OAds`!mZNQ2;Pu(rLftQm{Lh>@qf{K%4iX@y?X?FREqF~5oKnbgGY^(~X_E-B zFZSnV4Z7bXL_1J&B6TzgNN%PlLrGxMnv__{xm~(+QC!S8gB}%C3|}H`uDDjZB1P>H zAgVqoR>?j7`s*)chqw-sDSKX%OhM4;;x=vCRM%BfT8eh_8dy<9q?hAGHo|#{Z88#2X@M&k;w0n;qM)@2MqV zO3LMCz%>Bz0g;mHt+A!We9)!|;SmvRK69ky)e7UHBIM@fA+&;80IY__1 zGm=wMuC0HB9doBn#lR6GU=9mMOiUCuuKh_Fje9#kfb1+v2r%dKDHt+(49wXnhCkX5;ZaeJVA7Q@p#U)+3RadhBqG<)GZUN<8jc4a>Vk|+8xpF;;n(#Wka^__ zdiU>#qEcYT|H}yZd3os6tt-qXVDY>!Q70)09Ur(4S+*R6g@+^4YQ^Ryi!o;8lgP=< zbL#?2hT3>+o(onbrxq6#;j>w@F<{(yn7OHENhu;C%$PlSGWrZ0jCyrzW80Rk$}R5O zrx&*WcQd~vl z!-?Iqt=N-!^XDVW9FE=tA49k~6swkfhlJ{hs8PEXjvP3Ei12Wv-qsim8sDahYu3z6 zESxqC{knF>UH@w7SOV_uPEtTq4<|Mr-(7xoktvjf_;@T`vIKu+=b-Q4!LVMN@_>7P?U?38ay95q zk)0);n=k=gh7CtrdO8Y=t}RVaDffZ`gocJ7(t5+_0ud^^HYaBGnbSjm^QMC$M?hH<<9u zGst76*Qp86`PQ;Zi}r(u0}x)AD_D)geOz+z{6*Z^vIVS}?7GZH^Omhtq`q(GZ^$@* zUKOQiKhjf@(WP@|uQ!Mm=_msT|6#wA@oaV1EJi1-_z(47-@`Pl92&ql*Qs$%tICD z&GYFc5M=wR_Hd{y?S}>^uGvYIa+N=I{b26tdVxyIz@C`d9T+YpMD9iPRhg@vZbgal zbxzWFP8Fq9QywKmPM}VtP0_Bc^Mt;ygS^$x7xkvRu&^*?W6JA_bg8nOiNsb;?cTQ9 zn-~Cm3G_K_XcuTe3uplE{=wMQE`k%d|_15?RvBw9(1ZhLTV!%6iJ0 zZCXw$g%C|ig`tS*Hg!R=`%K{(OC!y#VAytHQdIM$g&Z`@6C~TL*`C(gzZG;sUR(xi-zBO zEuR_7^Dk8~j=iiPN&9z7zmkW7prO=s$`rr55#AIenEeEsH)qIlL$BEW^OE8}hEesc z;xJMeSr9!TaPIur7L@f>AkKTRY|en%P^OoB*yQpwI%5~RkHuY}C6n7Q;vLOGxccok zN_X8f-m=u|%#Sf~J4uh|#s3IbFCx*?Ph`4$Yuwdvvd@0Yqou;sAL|{P>!C2^7(PDe zLvfIzA`YrAvsYZ@TH=@MuXQ)i67Vzscf&O5W`+p*VI}p!asO<4Bt6@M^(9i6S7wZl zm2bKy^gm8Xoizwx^I11%P(wZB^vt4y1gbAZ-x zo{Fd><_a@UD77n1kd^?!mO?kvxDjkAAJS2A1k-vSt!8=YxYtok9`q!tU-3qV+)qp~ig`B0 zZYI@Vi-MrlWxYJs>dIZeHgb|e5%@e~6GFY^tv0?w69nlXS8J4`-e{>V& z^$(T+5#yp*v^d$_;R3?QJ>68}ptHE&GJlC*1E?mPPEtVCI;1>tni?;Jdp+Ve_;*kn za>f^FM9vNGD>IILY3%e1YYO$b7LB4`ID)cHzS=M*cEVfs*KV zXUoU@26Z-ukwzmoBI%d@K8stVPc!==7<3k=96iArf2oNiy6yk$JhST44>^E!*UqI; z5}mzCbk)t418G>R?(ewhOH#3_S<_HfNQ4@17OB(X7fgh$iK=4iX{`;MYGDdeoqOGU z%=YoSkY5;)P)%~Opq=nw_D2lS9Q)8*gub5lJM=^q8pTd%E@H=Rx`(4$*M6fPongw3 z)Nx+^LBfjHsb@(?bjw=5-owq+ul-U)8l6igC6StTiXy3YxoyTw-FFv%Up)l~QqxFA z%47n@hI&42XY!GmR$j!Sa))Jw5rv+)mv{??2bF%!_rMh@GWPmu0gw0i7vq80c4(tCdVJ_p;NJ$BHnVw)iig*)o@Vm<7aH0a!IHo(Gxr|5?LcC@ zU_QH%TJ}3xhfHYguB;=(w}D=FB#;u+p))^-8p5}s@F!JBWY zu5hp%I%Vd>OdYFlYQXF$IGvAu}J7(Li*E2luG*vC1BeKaD)W zU{dCpoG(8uC-Dm>NRxDif|&CPelw}?fuY0RXoH>-gD-=!pF@As6_6n16pSKIkZIxX z{9Ezw3U={FF-W5YeRhT?*M}|%12E|c~cR?9^=;Fn+T94Fm05Ko_2cg#Y31#G$U{+rQCs=APJ2iX%SMk#BV&y^%(G5=JAa1= zL{6w*$x20$!EygUe#Wb#1x8|V*APwxzVS@XasH1g<(079q+IYV$JoMlI!JC&LOdGq z#3v3#d>`W|V?$rUUeAsaLQ;846a{2|=N<`NCfeU^$ht%DuT=Tfr{9T5kDJA=&PEqa zMYP7d_Q<(XerE!uL#y0Riv9rMyv47ZMm*uuP5Mz(n<*8y4(V>KOsFglQ_RymCHT7y zgj2SFZ2wWDTm@0TgiD}4e|&U9Qx1~#4UzPId)Vsfg8GrNp?nvz6Xv+1Ie01f|QU5fwRuASf<1Qp>@&QiI6B6k$XXtTcIc>Jq=yu~7dMv{sc^k|UmI z#H?LFAN+`qTJ8P=`$7nlum+YvXD@#$X0g ziMb+i_6TE`nGnDF0YA_TdWtn-jUpwuNtkK~pU$#F$*w7Aq6CsN($KT^%_2UoQFN@n z%poly$PlP#YQV2x*F~#xrZPMUG@IjB_3^KmPY7Lz9?n*1?w1(ubHQW0!!#vVS4 zPCv*_Ng^gi;LQrh{4Du4SS;e^E5mI=IKBAJa~*Dc+@do|4j?G6 z6k5E=LOu(?P-kamlafUcEnLyHqh$&okueE7|5==S2=_~ISb}Bja`NXlTlnZg(>=p^ zUx(NbsD{mV=|91@@D7pWt>kolBz?@diCgZZ+L?fV*0z63Uo20xQW2tn5)i4{|d(@Z>_wW zjQr_vU^^&=gL14n0*DwAywM>=g@2QmID-vLrUq{KT>_Dr!%rIqmE8EWK&nE%z$T5D?IPsc*YivMHi!tBsS3yj?Dka*RpXqEeYH392ej|l6 zHsdeL5I;VX348n-n}7)?AM#8iwfYZX^kgeC$9goWB-Z>tgi%n^r(-GNOh$IMGIPnC za}(tH#m1nyAZOj8$oz9j{Q3mmt53%YmU9Y1MP&FwEE=7b#n~x_j%g4R|2~&Iz(*1~ z!v^6>BU7gHbC&AN?TthV?MXBw$a$C1ADoQ@a7qL?L4HSrDr3TZhj0XtV)gq@zpy4| zvrC-Op&euv!a-*9b8{!fVL7Gy9Kz87UhgXSV<`|W1;mB*2e3m}DN~qxP70**&|k@a zhyeEu0ZT^ak9}&f`v8S}6k>o1rW+L`DSukkepQ$SWkp{9&Fj1*sv(5?MCJ z_KqAAWl14xMEOL9Dr3eSOF)c;5HJ{q;myeZp!GW`#CpP<`F|xz^JB?%V2sS=lxad9 z?0=xGB^QWfiGHE<&Tq`^1q}b z2z!5)i&9D?gWdomqcPc`X3lTK_sScmFC+R3v@>255U5h0?K0IgG_w9 zGQm>#VbVE%F`^}KN+Na0$WpVk8on>**MSAR`+kJq0`7wYcb{3>m;Wn6xwDa>?oUs; zt{SAI@hwQF#RCg>z8(0LC?w2Z1!R9J!W{zni1Odi|0wVu1^%PJe-!wS0{>CqKMMR` zq5vb4lXtYsrfak+G|j2t-f?pw@CG@z!9%+OMVklo;gz08j~>;)V+w;leE48EHr!tn zuwp)gaQ$dWoZ;rLPJ)e|-Xk5_%{c`HAKSM;VSv68yz0j{+ZV9S*m}+`rA2W=hj*A0 zzB;D`ZG6x>&|dTU;c(OIYk{7luMXItbJTQCu@FYkf;Nmjfa(>Mn>$;q*Js@3j6~I5 zBK)Nh$)VPS*g}`8bJ9BZ7WZa19KRNj-|MSMx$=6q35u11YkNk3{STl5U;F45m|&BX-j_D5VE-8Y|BY z>YJwQ6d}2;GnmL$CQG#pwBeq!>BA3t8T6v353<_6E9({lAI4W&*5E+k1H1ZN2 z6Nww!7$uQ{_n}yfBm`iJnBWYV#hNm86i;t1jo5c*&EgH=79Vu=!a=sBl={#qMOKRuSv61#bkP{ zkJl%jLMF?8ac4L|m+%*>DONjA=?k(CfiQll+GI&NHbdr6E56&;@gH3-;poSmq4)dq znq?@YCiQn`eiBZJJ>?R9H~JuX?V@zAaM;o^%C0M8#+eeFy)DxOUd|TAsE~$Lzo;f* zr=(l48BZIiOF?Q*jZM@K4OP$Y>ouMU!JtkoQ>}b&;-DZq8iYC8tEPPgd4c12?D`hb zS8*q|5}{i91CB34p)-M(RX#S5#vISjt82|#`#zFx4m^DIaGxGK4O?KrJB`#+h#)3? z4R>te!pg5jcH`gc+!O9gF#K|*7_TY!#n?HpL{4VqVdx3iWj}&%X$?5rbBBoM*!$5) z_VCe$fTIT@Kk+`KAWy=#1X5njpdzpC+@H-pPF}-%QFOa)CF}N%EXZ)v=doYe;$@nh zHY|rPPVU0yD2nW4(>>U)%y1|uF89%k1%gb)N^M^SjCN)xNb5K)a#=mP`Jq%?z|;&D zT@(-(^=ekJ+DJ#b_3wluQbJcvz|JtJq!PSH>((}s;LO^GD;R8$YEr}o;eK^)Jzkvj zxrXE?m-2oU;G2a;;LJ|uaZOfte&^5;Z^-abkWee{Y3MTHoDtkGS!cnrsfK}2Jf&L^ zFz0WO;#Z8nASF|gARSj|F&Bc-3W2!( zAnPiiGt>7U0QtIO1uY6d+W(WWB$L&M*S&O?;w(1y2lGR_2uPMB4G<;3G~ieImupMh zEwZz-OT>1Fq1ZzZ$s>*mArpjSj0SmFe(^RTvb}VS-6!P<2ZrM*wI%NH&ZvtMI+d25 z2NHC2#~hlKB@nnX;-En7!4^VGS{#WjOGzNfo)4tiYk_8K-&Rytp^cB2jU<*+9ri&m z{dHMAj0}QYi=C}1iDR(DUpzi&?wj+C<>vrLil1N#;Q)Aazh-g+UW^jJvzR%c2fRRc$e0#v^Wq@xGkQjz5B2Xol! zuyghBiqb0d8=ki9IEn6PX-b%M9r$;HjWB6~D+CFGtW*z}gG?tHV`CEZfX$}hZP53V z266W!An~=>0^wC)?^Y#VTzp3wst@C{VW@js5TNLy4GSKC+_hWZgC%SVEJ(4gc-9X; zM@;C5RYR3H2zyC zssK{JOC6-P`0Yjl(t7S#U++KXq!ZkW#O2!lk2?yS>XND`BuEl)1|TTfni7Uf6M+AZE3*4y7p)K5C=R3(ueU1c%-QKCGMyC+yrG~BtgJ}bQ@9h0QTJe8tkAOog@cbteV1tfMNczWe zaY{e9JuL+*-$y#ZAIm^WI=-|e5`v)$`pU~vD&?0%zv<40$3gi zEMIa2LkoZ@ere~w--f{*3qeT!-*FH~1U*v!XJi^N;l53>O&A>=9TD=LXfJe8157Rc zC+LHUV93A*`3r>D02}HwCmI?EYvvKR#JxWHpu2mvVwE_^!Fmu8>!pj%aB6@(Bk_+T zqekw8LOGFLO|Xm5nLoE(rAUnYeb7*2q_aiiVpgw4_RDZ7PjGI2k9%?%UY_(0%E9lhUC*YGGDYj|)tYwP=K^fDM3 zlN5gdvq-pv#C_B4zI`V(A)c^)(jX#P>vM4CgdKpRa@F0--gf}~D(!+%yxq@$=tuN} zzq=(p9qwgjzdZ0DV+lJ3Xk%n+T1T-XSp`fP+O+w46^T&b(;!O$Ghd#>UkV@zk=q)p zx4pmgn_MR&iOaBKcki~hx)7=amNC6hA_7k%%$WJ^12G!|GxS$v-Q|;Q4;(M^ z@$BJu*FciQu~OrOB_gc*5}fvZa6X4jLx(}mhFw#OAHh#Akv+pqc~et}A%IcMu~!cY z{>Rhh72lnfel91}~D(jFRZU(!r%v?-h?Eb|_ zTm1Cb3#JdVB<;Xh*d5#>hHe1I%dYfbZ_$TW3czQX6`UtXZ0gM4#t#ZAnC+G_k=vA!aa6)a+^R-Wf314 zZjH53(TI+U;-RW`Np?GIy?spo?b7yog{0M%hK} z*5y>Tg1jsnC;a~sZIv?J-hm68O7NoNPtxfmF@a+bT?%V4x$-Dzd1hZHwsgdxs0;ts z0$tCLk3fE$q-#)V)P`iZTm|~ctO+-dyNTAPftcY1iWKO2f~ZOoJ0X%EF#7$-(WBBC z0|VC%^g2%-y1FMgyIpvl*$pvH`9AYk*2Hu^Y5dyI{Iy%PW(E2HgzCW+w^ebLBr$9( zeqLc!|I+1|gbQ0haX8Zy9|%2&1;K`$wDMT6GDT4?;?PWdkgPi#Yh*`sM6kc2TZw;K zE-&|rsCTmkQRP&02uNT-ROa?>SmAq; z-FO*00SC~D>FXB?zmk$!iou?iiMW{(ach}ybQ(JJufIQKs`v2*W^SAFa-9NLqWcQ? z@;Nl!I8M-PXpCbCSVt1a^IFEOShAKa4|KP6;M0@9HTR$?jE=#fe@bJYGIWe!M$2 zI`(!ywY%1ud5QVuD#BI;daBxIsZzJl>?vH*niWD(dkBX-k+U=Q64Czbq(B|2wZ?mW zx2BGb<`%^XSUwP51jqRz`qA~^AH9^0?$xke<`-~vY|wsYCzxGMEn0TEakES}*vHMo z&Cer~c&4t)q}e@S_`lTHi3uBOSVb(0VAdBg@wXMlHoMgDl&{w#fy|g1dudYBdP;YK z@p!9fM${ut{}#LfN{r_M$HFBawz(%u(9#LZzq+gt6O_1OBa|V6ne&8X%_q1@!kRFW z>CwuQywDj(m!n^RbkCCRDu^dSlRi~lsID)9WV_(r<&qTy#i9MWI=g&0`NQ8i(>Bnq zf&^}TyC4WMLGdK|0)5mm!Jh+k;u0jAa8iwHOJw=c4hP~`g~l>&UDY|+}l z*Y-<7_XElHH+D?O;4c^`Yz&n$+f4BF=W;BLJsijOOGn{k{`E(MO$Ywc|Ec(++3G)( zf203@RQ@G0|DO|#IA!+Hu6m=4JtKov8Fh7aq4sy7*IP?Uy{k1ZFVD20!7NPg0N(aC zsq*#f*H3UQ+>zGv|~6FkkV=soC3sP+5HoI^q$XxezppyOq7*kq-XqYL(9I>8KWD ziXj(eYQc-_uWDlx<`YaZ6BgvT-dv4As`pB2OJrj~nB&K^*H(o&4Gj%7%agx|)2y~~ zJL|GzBgbu*_sMi5#N+HTGOA$repl^M=%7Cfe!$tk zQj#DYKBv46THTu=rKCUY@l^n$Qr`XqpW$`$@iUYy5;ScE5Ze@npCJQI8Y2CMJ+Tkx zB8he$fl5|eY3(AcW|`BZaG|SD#A)Zj@jm}>%)pXG*Je{B@e12)r0O2rXs9)f8*|(Q z?TJR(G^(xv*w3G?9yt3=pN@N)Z7kH!EA~!}=%i$b%Y3p$LN7`R%Bgl5Lg1qWO;Wcn zj;vrotMJCuA?KTRJZ6jTp#^9X;Z6SYJ^gWOQ`4H~xuIncCaHj9t}-{{pajjzl-BmN zsK5R#Zm^THks>#nyA`Rse411@*0F)bA+n~2_9yMdC|e|g3|nI<<~+@*f`PL=;#kzU zX<2nxo4$bv3Ec@k!?VPAXf{hfNgwVV=Z%jBj*i6_8ushBHfCJoFhwpGoqMS{(qL*- zwMFK=iBtv~VMhS$h>_9_EP5n*p{qz+lGm%@d^iFBCb2ahB4j1q)57lr$Vq6_6yrIn z^f&?V8iZDtV^CV5ws_se7hTX1%l7Ng8;474ieJ`hLXPc<%~GSR_j8}z6G%dDhS+tg z(i!K#RX-OKAWW568JB&QdG)!aZGw4Qpv(?~6$}ddjr(dUHf6rK*9qdD6N;FeLf9Fo zkr88mRR6W^1o=<{3)(G?%MECJ`>I>zf?_jZqca%qE+g#um|yN^xaIcDrvg0N!s+VF z;+O^ttAonwX=}x{NMSP285j&n9huDgBdb}Qc+|*YU`fvsePQZz4G)EBA3lEcD&uZv ze~er!AWXGW2hX3f#}mbNh+G*ePQGR%Nz={Z91&e{S(p3$yLZQl7I_l-1ibGvI)6`- zc&^R(9Gl03PG-6mo5E(*3G4`&o1#kRnt`h+4ia!4RpXgNSSP@}FO_mn85*{B5!EZ4 z!vKmu61lewyDxfWXs;q^#FE8HM2+B%BWt7Swd3M4wh*lxJTMSU*I+BebaZ_>e)mjt zsRXUuDlS`>PA&J))z>4T=RnNQ<+zy>p`{u)-mCmUQazICmfKl0ZteSJvq)$O9N9#s zlSQ9{YDbP*+K!5@9(LCu?1_s`d9UZE{J|5f?>kqB!VOLbI>OjRrBN;78m*ZnV^5|^ zPgSGSZqRj-P>duj6gy9{1%{OSvKNIRY#lO@gVi9>&qeAS)~>ovl-lYrKRYRI-_C7Q zJ3*F=KujhgU+`@fwCY^K#jqBqDwJOT*+;%0J&L z(s31%s@X?uD4pI;OXSF|m@Y=cy` zNhdMvtvSB?MK4pixQkM`l26Rz2au>y3-I2AV!hxa^}clo6F#Ep z(SMOCi7!91vLx24!-Qt5`BJK^7jpae{BPA~iJC8oPi=*o$Yqx=!&rbg$N$bAFxiC1Acdx>m7B!ST z5a#W@ou2VmNQ2V~&X8WU3Az>JY^z2}EM_2~e|SO5`t_ra702BIzZ+Gbh(cp#T5YcU zJ4-}P9vhlcXFng>!9C^zkJRAA+QLoLv5Xj1w6C6H>Aj_3v?ax+Lz}T616D1Q((i2+ z2pP?_{u>m^6ISrN>}JB^>|raXp`-URaviLvdF*kZa&b-}ybUS+Wg8v8Dkj5`i=?GMf!X|Od~;}){+O^|iSyM*4skE4Uol)BJ8EwZPO zsOUadr6JF)-3wGb5;+$n>CVB^v^(EklHWR-6!Q0@ZnPeM5~*)4G|0;dYRwrvJA+v|8g^Q9$aPm&LF{G;dUXlh)M-+u950rz#0j4MNt>zG%^ z{0B7Co2BcI`={(K5WS)`;B@0qU3PQB*~i_7ibG<=Yt4Yq#QtUM;{JJB`F(ygl-vR0rwbDxDHF zhasT{H8C&Dm02oVjdL(f#+4T(aEalM}7H2b2pgc)G%r=Ty zV;ERvjW1n4KU*Am3WtobHn-3<{);+0N^GN-CTF9yrvkLhW60law==PO!Wj=gyMK00 zIrULt?L=Y4?KVsGt>&kWto3o#4L43CS&w-u>E^RaO7UU@nS2)jVGK#M)7WWbSEucE zpZBl(ERm8^mnG|b=${P36Re^lr7in{7e}xP$BZ5hsOG%aaABf5!QebgPI40W*%a4g zbjI^)z@ZpTy=UIg-q4mP?JRP|e5SamNYzNP_bko@YtMP;e86yi+pTcy9;Iqz<^1gV z5u;_{vitMwZPT7@dUH)2)4Hp0*IR}AwZLrW8oXZ?s|&V+~J>R<9m4Zm!m} z9Urt3eE?SYzIA>WFLf#@xMq1(f&-Z?w{62}Q{CyaG&NheLq`)<~bIM$qA zK{ihm)OKSXOxbGP*te9vUGD;&@O}i2B&d|HjQhUpl{G%YI{0X5!HEPP340(c!Z@Yz ztm8>Vna57OJ=Zg%IO1SKlrLXDNf$!RiVO{Fc8N)~#l^9sXyC|cZ3wX8+G zw{PE)yd!&jJv}`i78FcK8g0d zq_|OV8DyYjq)Qj~VskdJm(FIL#Wz{kl}S<2SK;>^iR^a0#!Vv_MoZrUCTGj5??hH4 zLadO9^KRHS)~_g`2x)jEIKDfqxjw+n&m~Q?TUawnH7+~V zr8@eOmmeECpWWv-aSh%C?93VUcne4~&|<(3bb7rYyH)^MXzSbnsR!LqpnbG`8xe^- z7WPq~=UlvfAbk|CjBqL@K3|5yT@B=IvD8-jw{~kbq(s0$F|Z-YSFVbV?GgoqDL6RWb&733D?;Ax@$} zxq!C%suz)|9T%mLM$$Gun=#T_6l`6y7iI)&g8P}B<+lc66Ax5DL?(5Y$&%Wy%$D9Z z;(F93P`phMjtPKgc^bI$4exjDySf{nIC3GSI|q&}TTdfEl3oLoYrTBW9CWxk6e+|D zXZla%SJ>j<<|*4)#{%uC4s;4NpGCUXb&tupxo-i$r)Yr<)KYsOK>QAI#SNCJ`Z81T z?uE=}6IU&F!O6AjZpLw9BM?r|7;jL$cNg&=d74BBXc8Z{fW!i4BUx02&b$0#y`Hjd z{={`-x;H4qckwtJK2vBg$m$ldVNs8mmC6R+OyMbH)m}XcGQj@UQDMv%*8T;H8 z5`Q8NAprs%-OEH{kfIIdGoZ?-02d}>;wsKzkcq?9j>Me~-3*Y40N=8%=D`f!_knVh zm`<|-rJZEol)2mp)|Zkt4ZNqR6q4QmJ_2+l!sXq>BN*_81u1lH&7z*8w{aQwy_2_% zeor!OCmyo6{PHPtvg8Z%{lQRvzvwlQc4)DnQ?1fe-0SlRepN4vBkD)QB6$!5dC&2l zz@}vbotj>#lsZBbz$Uimq78-^93+qsguMschT(oSUSEnEP#wPeLE5o@GCzyJvoGtZ zR!-0-7S@XiVrrV@;zi-6$$wV+*v zST9-JXYd=OllzE(m$}zBJc&0rIJos;QY=9$IxPhBbHiP)%f#YYBA4Hilmkf-qweyv z|9ui505&s{8dgp0jUI?rzGPj9naRjy1`2GrL;zsXcf)GCtpbVrg=`0Sn1Gzq7P~*c zO`BN#E9lqP)S`MY6fF8`_F6IOqMkH6pFkq>Lqb;or+z$@LN#MN+z^-XFku_nd~9ck zGQ^6+5IvAuJw5r!>urX!JrpPze&^#x$EFf3D-AteWgN#4`{1Q(rCQwJ%dx65;I(V;6f-yc^mK6Q1tIKkC zYmqd8`ybF(5WO`8eyf3AYcJ?cG*H5Qd8i`Jtew1JPoD~en7X=na-Cd2JhweE$Tz}O>7C0b8wM*Vn2}TBbr?Z-;cn~g9ZLCvMXVdO(%hN zf;#En2-mh%AIiqh|)V?wX&!?ah zwMc9p=>IcpLMwmjztsxHx35ftw-8dlI6#u(%AcT1fZ@8hvR#cB_!O+u)702%$t#-O zm6pb@Klb-|6J3h=Ve;T{foebBdb9vME`&{_>KNoo2UHV_a2k)b*e=Bpam%Bw1p~H_hZ$CWhL71I1F)xn*;4{9KG4h$yb~VsCLWdt8X~!m3|U+McFgK zy~iyBy=HNyFs^~}!PRYSLcM5^c4xdl`Y7lJ>pozjLT^T?t`y@=Bo@SW& zG{W1V8n^ps zxqBPhgXI^0d24>|XW!|BD_}snbn=L5&8J-edV<)p;8@8%S~lS{Q0-y9uF97r1Vw1+t2kd7XCn>> zKb7oO2(w+Ntwx1DX6oOr@68EyX$6uHmqafC=sB7>-y~dA;QYk8CLeTRoMpXuMBD}l zn!`0xNZVs?PP+-lddJ(0iL0g+!dK*OY35Fw1E4K!JU*(HuLVt{)bE=-swCLjmrjZ) zF4tNben6P&LQ8KnI?iM={RY~sPueU~C82MCL=#=|gk)^uCd3}M|L9KQxEd^z&^HGo zAd@L6z83t$wT{QH&mnY-GU-RZc=R5gT1$x@hbj;4>$1}KS1G$dPj{sj7EX|p!qz6y zx6W{g5Q4C$JR9VvYs5t!x@|Bde#<6fSdGrGV`sZJ#DGI?Ghm6N|Ml7enscC4-{RY; z-@`2E@a07KpKQqYB_ijLD#rt==Pei|8nGuD7snnkSraIn3=1dUd7wODRrJ>|%n)uX zx47#0R>rPw6EOcn@G@3Y8(tm#wU@1rb|sO>#(Sp<*rxmdup8W1s$o<-Lod9dI@PF~*V zj_hmKt_9)&{{DIW9$9nfTmAA&hNwAHG`lRbcI)a598e$@B-J(1&MM2-jHbYD;#9oZ=;qS^;me3hkR&fFPrPCV=4%~LK46mB;D=|1j*ljBE^2I6S-17tGaj>g( zCkEXI4(rn(ceHJJd_i_n_#-x0UMfOp^Il*^ix>8Jbg#}#z9wCZ`&*xFq?KYgSNtgt-;Fbd~UmtP8$on~OdwE}+K z_+YVy>%-Qzl{tkJ*Obf@b`Wv9st#`M95euA*f~Im)$n zx?!Qz+TL6(ME~Z>(iIE`KqM#g?Lst`oW-l|R=usGC@9VT0$eHHx=a$-la-%fHmZdi z-(3IuXlIJs_~Sz_T1l$}W!O7m(9zRg6FROXUfUZp1JSyW1*SR-lp$CYYS3B7TdAmu ztk%Q#aGnmPAaU0gxKh|cK%VCNMS~gaRV`h4Gb+Rs zk?X>4xr5F~`PTV*fp8&&L@$55Z!udNUCGk+`>yJd42LXSQczBh1mL|@YJ+%}wS2Ma zc{s$+Csg~b=ZdnUX0wulVPoT1mbq;HlEI{N%93_U>5Tz~*NaDe&*p$R_5m-8;YMAn zmF_k7^U~`&YG;Kmrgmwh@BYjU$lbqBE}G*;u#?1b+XGJm>6$=7PvC_bwnnPEj}>oY zQ?y2+cEKT#+eH%ns`mhqZr@mKJj?*ixJGvd@hEJ&p$8Ty2q3Z$QiH%Zj}ECq}k*b(E(NnGmUV z$=cO%-%Us!1~lxxX6Gu`=3Hm85wtPKM0+j zzFX;2!IEb(<>!3hR?EbB{T$5q^G*B4Rno!ap89gP@=B1FI%|V)w6L46F|X2iys|YG zAC6TV&=Q-`r$mn{q^H!iei(HwS`7WwoK_V?R|M*=TNov`OehR1@x(dl#r{3_y;S2g z)4);ln|f3Q6(+kDIG=7377CjusSn4n2Q{OT8l-O3e(sfTL%`j$Q1~0Bun&)J@ZEqP z$9dyVa9&?5KGt2fQD-}6V@2u0DEjL%b6ebWmi&lGoFYh_ab~#$>L^lW|56=mxt}ll z)`}6;((v(P!2BNf+FLpn+LV^t67R)V$5l7qjx${UPv)gxcf3r+|wW$pczTjD>^)by;AhD8hv>%X|O}qXf#-Z(Q6E?y61jft-L4N(bLD; ztnAp6=rm8rOEe8A5EMIYu^Uhm~$%vPOFIP}=+QH9~#u zBsgck>$2ZpV#fLhhL@ftE-PA~$P;jIpoCR~g`XMnV!drl{}PI-jkg~nqwSp??_%k% zcjLE@M(_r0cWNu?LgkIoFVe2azD{;!&Wws`|Lb|_Y0se~m|=T%hC6MFo%x=?P$+NJ z3+FG>NWBAX1#m)cadT8nkA$Mt>dng>XTD3?5juLAwGKbD^NEfyaZ9m|{Cz08Z)SGE zyikZr4ug$4ywD$dMT0mB&Y>^&9b2*B!}l4wtqt3TMtV+@YC_!JSTMrMmc$8xAH`ip!xu?#R8pC-7SUX=Nw_P){b8gt&lP?Z3+@-R@ISbG;? z_H69?(ETlp51LD#cDuYkJLM9uclp&(4TXZDJGyaW{_7h2x+1^-L#_SN_iFFf;P?Ag z*%!cTSGKinAp8c;n~k4XA%DsCXZVNyGef0PWe7_1=Y)ISzvxkrj5cz-6G#!kzHjmF zP~zGiBvLAC;9v;0-#V^T&W2sZtBKZUwOfF)~l99-@)v6rYxRh?bbN+=P%!0 zE^sP@2AI0RL&`dxUHcy64&j4WDDp=4?$G0+YYqsQwihpJP}o_&cT4`ExPP!UaOed) zZ68;LZPd?Z1!{O%eLWdP_4@4mtY!G{z{#9@(P|_2SZ3G_dieJFA6A(20yXbW6Q^~b zf|N=*bZk-C1tzQtVO#rYa~nU+Icv}wn`#hw=$+p_X2{Ku8CdkS=s*;1vgzf>DW7M( zV8-p(2iK3T0n*omOQ)6N4Vq<{ z9$~S=3x~su)G}h|YB1}LYr7`96bq%0)<>oYwj_5m_m^%QNPFjhi8mbe4rRASkZyb# zn_sb`q(BW{zSn+BB|JlGrwm?d{2oOVDyecyh5N}V_U{(Uw>*z>cWr6#UfkM{dPRGR zM;_<<{0GJ2@II}TmpO{vghT(PwYk6W6pi#6=?BF*7^zt7rc24#hLOQp%I_Ytl;%=v z-3?zG^*Y7QUvAlC!+|$<`{6twj3+eyw6|13a$9dntq+=0)^2CKqwgC2htm3;vU%-uRHXni{ zdHX&uA{(n&mAD?Ss@_;!y>WTibyNNBSPg&XnW1Etr4`q&A?%+SQN|MXj3m+>;m)t1D1biXh_ox zBD&Ct>tJywm0aM-C`-`{5mM>4%bjNZ>XN*QuuaP(c|E){;*{j1)7xzQ6hY6Mi3P>Jo?YbSlLSfp+Ud!q}WEO zaSi;4YU)^Bz_@=%M0Bx~h@s=4&31apx}q1%(juY$s9*9;A}a}z1R{1u&f9t-5Be~d z&(^J2fA#(97?j;dPkolMq~PPlugCHC%Edh(JL^DpHtS`8`)-X^N*Rjb_W#s0TBx<% zPZ2?o$zFhQ#|YKk@fe!>@?dXlQoWzq&(|S1aj1>JQ=R0q%rV)}dI2^kM{4n~;zXqq zS_Q%F6-2Q!@Fw|F&AVrrke2CDL~Upx^Ch5wpcpgMwHge@8!#R5wsts0Wd;@NcRXg< z+m*N6KHGuQ#%kP?GDuyXmCEXao}eGi6?Co#X$9xgSIZcWhmV`q@y4rGnSGbMg0c?_ zmZ`rh^X;B_#3%YwTX z#u$Z{SmQDGZorexa*$WhpVX?AoO|#HJL7RtY>IN&99=RRX{6Z|-^b}VruPV1gA^A_ zPtlqsrpBANwDZf?puC!%uE9{NT6pbhw>xq3Z5w9j&-E>6jZoG+pB21d6x?RWUlaF^ zpyqIme2P>fT-NnTXWQt?fRC?cV4*X*kG@l6F7d5?{zkbB)H{d}iS;l)`iTW&9}Zev%$0Qup!oI{IH zBjI2f!J%bI3F3EPn?LTkLQCLnj`|JO?fZ_?bV8KUljvV{Ik%WKH$HJO%C9m4=Wn$6 z27Rfgul-T?B=Rxe<=*D{v*4PklHscIhHV95N}bB|pZBW~_I7NZ0NY65L|nk3Ui=bD*qa zy8!wT|1Dfhe)-wm$Oq#w*ulr)<|ikzRX@M{a(jM<0p)lUYWbCnf4A}P@c&Sr$b0=q z<^KeOP>lc7@243d8hHLUErtVyxbXN1_l2;$^<&;q=iz+Fk$=MTa&vwA%kCHpfp>8n zfQ`G!|9asO{?{RuxA1S@Qr8>Xjgz?Rq;-QJ}6jnSF@AMAs zfirvJ?%R(klr56#k{xjQ#CBPIv_QD9*H)?S+V-(FUbNHWr&`G zx#wPV{4%1@g6nw+FTw+vhU4SLV^3O{K0|}X$I1N-c&D=>uL$pQ$72=`b>ZLL#qg!$ z!-@Ad2mQTE?@8i}k+&A$yp+~o{bi|7iTk+r8gCfGYw+G8_itvxBwihJ+&C$agqJXf z8x-E#JU-i3vvb8iyXT~mB~{DqBz7Eww$%rh1gO7~(LX4$4vUEzi#+QU0<;L;_L#Nr zagl9ZYWTQ)T~}9ElF`?0`)$zl<_VSio?qKxaW0|S@P1Fo)l^PoI4P{)&gS6+}{6rX9i=- zBvh76b%_?VAlW9}ZiTEBMNEo{P?n^OnW^8;U5(Ze-v0eVO-n zhK%m->reB(?>Xl=pJzGGc@~Pu^0g#+@1k-cgmhzL&%#9tcYSnWh%#Ajqs+TtThMSRW~^4~TI-h5>)@WS zT4xumO)K?lZCYcvl&ytV2}Gxc%Wmm_(b0Vdrd9m>HvJy_RR)n6;Gmg40y!7f`>dgz zB?$iB^ zVBSFn?}Q`=Z({vpb$=Shh2|vgCP^eWtY^#(T))#h`~+SjZ99wqmp4&jigNj~Nh)Wm zBVcTHX3f^l2T?I}YGudF1l8I9656n*!C6>i+_mN4o#SlR(>UvmDMqjQsjxsoL!R@# zMP4(z$$F0ETqN1HF!Ejh>EAV(E&zUPJEcKlZ)w_sxjw6AY-^0L3K%B5H%QQD?V8*r z<8eQfA8l^?6!75t_{;ocP6Hrz%g;#fszC<(c(~nw^*Tcs&*pk zBZfuC66vs#cu#JhB0bfVBqP#09YZ9Cn8~3kAT~CuK*YLvW zRE30)!QyPp;#o=W@=JSfq{DJBp%vw(eL|Q&!5)QAQ%?coX@&*NOj(eX^nY*;r_r>u zv_^|7wfWSOLSS4|X80cSDlo#_zz2`4Mo3z&(Jy%{bzQKaN#B6h|Kk?==7t2g>nSsx zIKW2ZrhZi2!{)HJ%8+9n8Y5SL)&@O89#PF=N$e&z3r+bT0c6`XTkA|q`3V@2270 z=E{C@9gJKqyP%G+WIrbDUZg>a5Iyf53PBg9kG3Vp$5Oiug@A=H)t8gxJ0rLqX?MS) zoTLOo4Zdjc#>ekC=;`4xe0`4yXFcw`@%q8K7Pii=FRGdHOs&+kb1>q$vO)`%SNS_S zj3;9^E0s@-vYV*W{L6u{`>xiz2DG(?Q`A2|AXP?r?V`vawPS`}_WomKXJPQCSxSoYG4% z)+a%E##uMl8du>oFpQps+s*#NaQ`CV1LH_;rYIkHaxn;d`7`(@Mc$uX5H>Sj^!$NqM%+nSFGpZC zudcG*yF3u%HcEAxpk-$%7fWYAf-+D#p|MZ%C0Ev}sH$$t8~K!2`{j%IdZ3g!7xZ*- zSIE2RZXmFtvNtciCQDaqO`VsEAefHebW|2<)Fo@T6x4wa$zAhgp9&D#_lI}jG}6SvbX zur%^!iWI32A>F#72}Se_z1BkZf2Sc1a5lHFADLk}?SN64x$xxWjbL(esAaC9;rMEW zBZiARhhSFYgCszu201BFFT5eEG6k~Ay9Zxs>vX7`Twg`7irBWEw?>=>4zlgE6VjR& z<9Ba6Fr-YgtMZeQljrPC!5D#}6mtn)M?oT${uM(DiwN_v|7)VTEnX3G@8N^IDo$H= z>Z!=I)xdF7?|-L0EX@i|Y-N6Gh)LcGl={-C;jUj@A5>ISv=0w&OVd7b!GHu9@;q)Q ztXgY!3=j@)_NMqks<122hrwrHeVbX@=F!Iiph+H`ES&4>>oeGUo`-V;<0$m>=~JDJ zm->^d;vJ<(u5cax)AE|6O#8RKBQaX~`l=%_Nx7AUg@qW}$2#jJz7rP=k^+_6gc&h_ zG0H^0Ftd?AjL`><;QlN$;il4WiSrBsiFIZU!0gjzTEEAQ`qTF3dcH>M&#eR!!zagF zw!%Id=^+J_ zKpjRa>&y)Z2XQxG|EZ0G->mEf!8TW!e|hjNviH!M;4AleZr%(VR%YC>`1W8y2?R@W0l-Mo` z)yVLaaV|zp1LIeXWr@)Ir2ja;Tg}Jioo=|$5r%JwbMdwArwvB+Cr?<}-=abcK#l0v zZVm|lW@lv;GrbIXmI?`KyM8?Ychk8U_SRdYP>NAfNa)m4c;Ii|z3s13J=Zy^ug|2F zJ!QYk zI>r9cw+TeOgxsNqC89Sh4HgA4-H$IDvW6Xhxu!6YlW#k&QM45l>F#*a{duZy$bHQ- zL!kEfT=vK)Ke&(u1Gc=Zyyt^57w{!1T$!<&yyLJ>N{CVHUpByOCL0~kNay62$ft}; z(#)sJwgVh+)QGX*9V%Man^ic`p4Uzs(hfKZviJa{hht=idEIyaHAg>^9Y!&Htquk^ zvM8%4OI=)9C+W2s*b;9kk_oIU6T{hOpTfz{wa<-;ic$;)e-}nAVB*j(m=ISJCP$2S zs+d|kIQ_$Y{ZSA->PH&vD4z^atg&?wl$!+J_%#U^X+iKAz8V;j$yq zon9M)**sNwB@U1eTB0re!=w5G>`2f*kf`&NXyHM4a zhcJBjp>->p+sp}K`7k*pXwYss@S_1D^!Z5+F*FrQZgub%A6*8$3P196^Dru9R-YI{VMac7ya z_UW_57s(V1l;)*1H8oMULvrOwY>6Q)?7b`uE&bqJ=8(Ny{y_>vP?&BGtO7o8mEsP$W*c{monHDd=w2DpiWUaVSh%>++e;&rx|S>jV# zp~gc7tlxb$=}RVu+9N`n6qo@o&*D!nL`ZW#LBLSLF~O@ao9Ew5$zY-}oT{;$ z-#~1Rh+Twa!e+(w5_d8;`nnxN5B4~~ z1JXCa(d$5A+0L9aU=grN8B5b@R14g*Z$M^tUluekJe4B0-Ids>r{~tJPNVD_Z~=8< zjrH|+b6+Me;e)7lh;c&`>m^VcH?bXd+#MCYZ5gn7&rESpWSz$3gAM3bEXzEJl`)2z zM?oHzL4xZ#4NVr!1G8XK*IHJ6I1(cSzQ3Nb&y2W`Sv{7Yz`mRJ^}{1H6iMlWHZ}W$ z^AaQ8BC*2}!^_h;9Zoc57&m*4li63C|BfoBzs@ufWouzsdcfNYlg<+@N_!Xsr9h!i zAke`qFR{(SHh!Fm@}rKk%VaG+UV{a(Qz&5BkBQ@pWI-@9f}xg3S0DC^ z+z_`fo;>8<^IcW1rx^90INcxV?>vV>NDm=1ptZ5!dKjP4^x{RY7dNSnSey^f~7A>C*={3s&n`3?%6DeX~siG0T8+1czOv7%^-5rEX+b zVNw@*4Flj9-(n4U4$PD_T%Z_*2J01Wron9s(?ok@W(!?7&EC}e**HZ(aJ2vu!CNGK zA*2{W5E7yCAYys0yvj8dzHuy)BQqOvA=32l$KySyKqRwF=urFel`{`x1nAQj#y zH_3hiF(SaZezvTMnm>~;YASXVE=1|={ zarp{VZcI?*2}d7xYZN0<NZ+Dq*vGaB z!2QqC%o^rFG+{Tn&VyFv&Q{;Lp2%F^21$mYDS!U!&P2w${HnROuLfNow|Y&ZFbCZc zbV--|;u7y&U%nDkFp=*&ljIAFsZ&fIkET(seT}m_x%1zqx7u^mSFaWJ_Gx9kL+hJ; zkEgIk%wG0*)UGo)bnqYGu{hHX&kyASQHoikD9} z+;kOmYaiPUHOl;~Q^oG+LL0fO7}@b6eu-SGHlFwP{#MWqd054=*3y;#O*` zel+LL?;`{I_U$Vj8XB^bY}5uRB4ky2d7eLos1{F3n!$JpBw79`9ssi$lw6<0C>eGC z(cgcx+)nlTbohAWG5xZU0qr?`=e3|HZOdbBQObqsv(C_6XE4UqHso@b8eXv)7sO%dlzS|+{HSUexA#q z{_iRMWp^R`cY&L#P;ORkdG$+g@Qur>GgMjPF6@(vtV3yH8Un>el?sO^)Fw?8Y|4BK zjZu9iNEt@N=O{%U2*!k4B`}rAx_LJ!IkSZ|I%D$g$Lcf-_2w3y&2W3E$9py%=4cPQ zo-myoaSg9G%-9bI_+X^~4J0wG-r<1;&vY@xuz*kL?WJnDThktf$nPj*yqYzyiN>_P>~z|9q*d}b!6&3WEY z1all&%krcA(?r$lkxusNtUwQ7XghC}0+(L9?%x;nNQx9akHE5^bsb}~sJlL?M|PDn z(MPa@ERv8V^GbVh#UxS8h7B7Yyc%*xIr60e&@z{Pm2pN+gtM|OxxLv^XD)Ixm*`>s z(d&4G+$7l<&Nm2s8SQ1%sUE|<%aX?}uc4nl86{6>KfR=WI<&b9J%x~-7Hy_|8`YKM zDg9V8Ry@LVuhi%Km?%Z(Mq8p*fvoe@44ju3IxY>!fQ(Kc|Cf2%OLw85f*w=IS%-ehMxfByVALu#h)U#ccL zWuI&me?+5=;w(s#upk5;v~)Zk7#D-?Q;2MU#pg^9+dw7r(CZg5e-^eJ?Upy4wa@jd z$@QOctu;t!>P^KFgE2bZ)na5mxf7=(nSRsyzJRb{)R6VpmL6`Uh@aZkNj#;Is@Qq& zQ*W3kL0(r^ml7&Tplaj)WCfX`k8l7+e;%B@u=q55EijUmaTaCgQaWy-BBkoT;cq%v zuTk4`@|6FrxJb?hLD2lt2|3mHNP=6WYJMDYIwrLpI=oBYH&HqLYQN!Ac91z2i@R=H z>d%?N#@u{?mYL>Ki_k(as6^+LY4^eWa|3541UmCnQnq{>8ykDg=QXw$XQ9J=rQjBH zpuUWJqqInLZUkNf6HTqL&uuSD%_ZRgb*8JZJz#>ZyYX%`_>@^HuCRY-RR1g>OR>Zo z3edv8UnP~ydg;vb0tRN%n5b`N znNyelmD%up*V9GdT|oGYmvIrC4Pp=rp)aPMEj9jR{g{WVi8ypF3)0r>!zfID*KJm} zEcE6~*MI^(_VcrMnL!kWW9w;hDNCwaDY=% zqz1VoFxl}Zj)o;tIQnH5yEYy6nWp-EkO3dh=BFw=gF@7WOY`dfrun?|1r7572B&?f zE1f&ogLX)jwOl^T4|cmqk=Do=FQl?_k@J21qEpB7`l9+C0}_WUdr)ELgj=_Fbu{VU zp6!LMHvbo|2XyWo1=_d#7NrRcsp5uM9ngC8tatIYD{90Bjl8QJzM?F3i0$NfS`;+@ zV<#MX(B6`;h&{R_-U~~M9fpn`E3=W=_*K_9JF~IqBB9xQ$qO{9=6*jColDX zQdt44h19NS=MWd+%eu`y*5Z-8UPAb_QFNw@86Z}GYIY34lUn=itlwv&Y**NCh zx&P!}&_92`2x)h7uKJlb4pZUMNcQ-75jR%nBer>n9e2OKVxY8Vw!f+;CYNJ5cKht) z*6PSB47QcxHT00KTJng>*0Fp^>&@UM##$)QYTqHA!9&mH%QiwZTpo2%C)^3Pqk)UBU6>roYcr6s>KOhpBuAFn4sJt?_RpaCM7ag2_~HJgeQwd_1~ zgOz95&~hy2>dmb(QhMr>nuEz1n@%51m5tyC7)v1}i2@tsbuUv0hHoy)Hw}88rsTPt z$)r+DW?#2F7R4(W_eCZiwW2<^6%WOLD zUvcukzePg#C&sS^DqtPSBW~;;?^j`QTf3jOA}}z6cjk;t%?HnjEenLcCfM zXWf#FZdML#Y0}quEdpMU$FbJ0bF$|X9v4|0e|LG2YXj;e_Zl9<1=M2Vrt`NlLOr>s~v_f#28m2{e7+IlQ?-gkI0*AwL^n zKaRjEIumY5eoIOU0z8u>cnx)ctxjSOB2|<gW_vAN;&aKH#pf(NYqb*Kk@h?}Tb_32!q{7= zQe!fC8-Q1{z9Vt}XT zwPAN-X&En;V+!PX?!N_&JHzKtE^o@II-j5jZhnx!Bs@2YUy6`{Ao zS0s`n=(lvdfL}-#PJqXPzN~CO!kf*wCUDE?a`88hnoGyvk)KlnI`P%A<);5>+WcK1 zCAgG>12(u`;glp9JD4xXd(E23l4j2O*j#>W1hVl4G;a=wg*8TbU+y7T_XJT9){42A z@$rmX(a3*xo1kP*?z80Z0qyCO8qmgWU2>Li=Vi&s@5Kg#(fXAJK%d*EfH$m?6)Onk zWUK%>UzmJITjSu8Uj}NxX`I7Hst898Lo=t4@M^b9xy9@k7fMtMr=@e=$CiRs?Ui^8 zzrhsGg5^@eNh0Mtb!P@3zxnoF2D1)77(>%7_v86SxzrNG>5Q@E4}+BaCpBPwiQjz9 zr^OL01)+{Gu>fkH`+nF2^U2aWVEN&t(W@<4mZT*TE z_F8%3@`Qy#2@4xymQJHnv)d)X%QQ*cZyDZ~e;KtKj%msc#?VGTT%|p^dz|OlgXkT` ziOhDmY)4pr-{EhG$9b^^@Ham`_?x>7!kTTrO1oZie983=6NCAByYmzB0r@BzP>N|e zgWA2>sWP99{*xl~V)*uk4RYQF2umqrhy8FnZAa2)p2Z1wavO%9^XZLy&MQDn9>1h? zk4Sv~hxu>6%$r|u)u@=mu~vA--&-AUymFX!!bI;E-eV1OzmWMZNgS4)5_B9M_=jdJ zZLsy19huxrF1J5&8v5$K*@;EpQ)_~)z{RKw#b|!=MyMr=K{e?+o2&przk80S31Xc# z`0QSqMf{300(0g=lkWStWo@?AC1zFz3m&QreYJE^MLvOC<6M^^zH8$Hf@YpOqt%E3 zN6cWJ#NtdG1w$L{k6h`qIfk(E9Qge68dO<%2%kp8x?fbAQn^V7l{1?aHz)YaQ!5=1 zLxMw7W&3&ooprZUWog+&EP~~TNS}DO4wgGEC~NzQq1zra_$5-scoUhWpLn|KG`v}D zyejxHUu!!n&vWMGBe@9Fl}FRvXV0XuTo_|0hs}M1Di^VGNA< zz3=-zeE?pv*!W+)?EYCIC6a|>nDF9hV4E?<%qe|6hm_>_yW9+S^O0w;026i>o+m8m zb9^GB6PhFU=5Vk(lJ69}D_t&9@P*UBnP^A#nnv@MfI)Pj=OO+Q)fKY|;fb&Q5d^QC zKfm5ov63*#$M0BW(ZySYvv}vSMoob!S6$IT7w4hZJ?nr{&fJmX)BPp%yeGIErPy0P)S5VRCr$Pod}_}|2#BBvZ-%CbB_P-kl_C(@0|itNP*9{p%1tHZe)HR5AcxBzAOS8fFVCC`oW7JsIfwreunv3$wr0(m!U?dx9zFpHa5*_SMgtw-d=+hA z*I9uD$jHb@18R9knLwg-Sy@>|134V6D^Hx2`*#HuAfuw9j0rC7C>2P+%goF)bDjfW z(CFxBiHL|ez-7kAQ}c@mIlA{yzKRQmu46p|&L>jt#yiIJeM!MZ@6DK;_7&>tLc{4x~XU4|H7KMgrBme@W1EN2o4E?o$36KD4 zfXvR$Hklg-$k2o1PYVE&%unswwTpnnQziNB0O^kyLw_wml4Qq&b>-*hdoZicQFSGN zd3ELFJLev>h`u8x7^T1Qc_X^;_Osoxm3xnIY3rP*iLP<1Ed3F zHIYCy>4u|`4v^JE0@b7&jz&5_Ruc(SlWsT~=>S`Z=FgumXPtGHTzTb{ za_gbI`kB|B_<}ygb5Sm=+UFLcHUnAxn2vo2M!#N8*jW(e*5h=1EiW(7TL_2 zHA{N->?x_KskZee=s?mzI=XM)KDqkptBu)Ff#_TU&|}7okr5+C*xLC(fMhDDSFfJ2 zB%giunOPlWo$H~NGG(k;v&L8*Xf$@rnL!6qwei_!pOqJ0c)`pi)|Jn6Qo-K5d9&#} zPQ&SFW^rTy?uQ?Kkj|Yum$+vFv3g0>7ac4@yCU>sy2G}h57!z`|guz)27+hm!JZvo*iCk z>(;I1@ZrNo)4X^707$?E#Ia+?+SYl0fmDs`(W8efUAolJ5K9`AEE2{EnmTo=ZG8zk zkWlsZ?Aaq%U3HZ(f0PC!qy4eR9y_gp)VQ&0*RHa5?OJ1LyoqZJN`M?RXi)XiNPyhE zd$+V{)22)y8QD)i{j@y){PVVrVt+MKfxO{{8;mnk8jzUQhaP%J-hA^-+xlXQM&hDl zTGfg1HmwBD0QvF9AKUJ>`vWH02Y3o3aj~~X(ieby@4ffrjyvv(({&Rn02X2y>|S{OQl2 z@%P%G1F3P>`|rOm{rdGY?K6bh6Wezo2uAk8g$s>m%gQjj(_6j@jR8hmne(r?<{GmU zQMHmjsm1x~tFNSO+qSm$CFnpRB)}xMipx&mPsT$Jem{5aT~^@PKV|qxQ`fAT^VNDf;7&KV<9HttQjM2qrH{&b2Cl zs6x{fP#ZUH6p|S9gX4-Tt}vQNxMjChu?K0yhI6&IIVv}`ZJI<2ct=pSgojN=6swK(FR*m#k z3lwNZI|+BR70nIy@;{9e2{AZm2Xi8(Q}?D-C(VvQ<*U3xqLF1aP61>t1;9W5{L`GI z_Ab8oVrkg0VbHda(ee}?>4ps(j1yD8etna~r(leO!@$xVGj(xrs5H=9j;Do2s)=G# z1H4uoh=~&?%Dj2=j3#oB^`UwatZIVBhz~#fuqh5(59V~@T*`3WyLXrCuDedoKKpD_ zqGC=gYkKfUt15DB$bm$FB%e8xlaoz|LWYNfB*OLAUoVXsHIiFyxkb)A^UPw6B&6}q z5b5^PWzDyxnMO_ULqk!Gq7|!UWt{`+2dsfkojMiof{36r;06qKvhX%*3<_^>t&WI##(2q^z_&g_ z>!J)QGw+<(-kaz0e!yI@Vug_0pd`mJc<^8uK76=bc;SU69aq+1s))vwcdzY0A}q9t zd5}2=@EF--bVv$}9z9w*bm(BNVcQtZWdecD37@A=pFYI^B2@3#Qqz6Ih7B{<2YT@S-o1Mp!Bdq1 z{PI`{8VV`h5jqF=eDTE>LVHMb@RO;5%$40vkZnLJh>Ye}Uwu_xd+jxY1K^RiYu2op zAzzgQpaRA-(1>fVy;gqy`DY_6YxSWtv#R&2c4{)4|M%+E%e04i*q`u5cM5~Q!(4jz z-FM5k-+pUIWR$S%&KcVRqz3c|3J`w%^;eU0e&(5Hj2B4qnK`RGX93}%36zJDl9Ep0 z|M;q*>hN$q&m1sdfXtXNBOs5MKH+Vng`^sxe~^M?PPJ1jZ2{6MU<${Ke)z4-EeOCW z*s{RTSdV+C(&Hp)Ez?&JxlZRqh*At91_lmHprH+efjRh>Uw$#>_M(d}@=Lq@X?tnc z+6p9qp-})rVi^JWn`2e2+_Y(v0h2h+o0(TqXL+}RZZ*M8n>Lkq-+i|@j0jYVw8X{z z>Je84K)yc1Rv^`sS+&B~hFY(~+a&9iGCPpP5 zJmZWriUa#V+N1aAd>!ausK~o*2eKlK^#&711I@%+T(xS|Nssm>c>z3xDA?hIAp9SI z83fF8p%8v$$%5xpSs+!riAT{;;yIY#Uw-+eai-L(Bxj4(KK0a7Ce6kihUwGffySqm zDb!Frt4afjz?9#kTSDWo96EH!aAwGx!j&bqL-TNKtcitC2s~KttnxsrU^Mp%P?%g& zXoNZ>+Mj&#N#l{0^aYz>3D)lCRvRF#fMRY}uU>7uT4GhZxUx7|cBkgF-_;ICOf6;> zAYtX%xtK<&o}mEK0kV>8AYL!o9Cg0z2Gaquf(R5{C*8dD*gMh6pxtQK)A93UmXD^_8V9asJ(@K`2P|eA+Sihf;kYF}7!n6|Ck!$5t-c3aqZ(1s& zA5>Jn{`zbA=9_N}am;Q<$M)(679^!qe05SbA%V6+HdEFxNowrxwXv`1$9b8*n(9 zH*anL1myMW*USF>`_1ps7#tId$`HQLC{3+tc}p#bH&Y|un7W3y+^mPLye z6<^0ZMePwnSCb}94C4;a(Gtw~2OoT3G*-a|46-mV+Brr&=V7f~Last{ z$>BohY}Kljc{VJAP%aM)36KDbNyBZ2)(Cy@yYId;Lc|0jREogxdpHmr&I&<3aT;uI zrSIb~q0N}*mMvQv01&8dxyhiQe5GFqXE#Tl!!!5^<4yMg;2IX}@RPZl0m0y)9khYD z5AA;Y?Y9kh#EV!K?%{9cu>b?nG+*xt8IUxPk&e(1HuK9HZ@eMfwrwl7?Epqd0hbiU z8d`^0Bjsi7?R?Yb9T) zi#QkvQ@u^DuZ-nvR$%pcybEyAUO?470sxG*(FUp;6$xJgz^cq!P8%S#mDc6l)=)%4 z1YgO_6%my8IuC7!f?gQ|s3Pqv>E6>0$ja{EU*&lYkU>T|*k!xZ#@Pw797Fbao!d1Y z)eMqmuRI6D-lRd;l;$pN?OV>d2{MJ5>>x}6yRawm4}B(PCaPvVuSYqS@O#FV8GL-@bjDe6wn`^m*VR zY1X`{q^1>p%th08#k-MsJW?|vLTc6sm#MFhle40uOnHxFM_zuu-(M$ifUE$JfJln& zzZ*8lr(Z3R*!Xy<-?)ji?cBxWXEU?1j3W~r9VyAnzmuHo9O>HqZ{p4IgYN1_&e|u;0$MpMH{=N%Li3;xLJC*g*0N#F)nd zzL9kz8x|EwX6#OnLOd2c;qV8u<9> z3XU*l5}O-;J_u^JeM$&w&O=jyiGC^2V4~qOws;Sn}uf)zV2gb$3$m?%Rk`A}^ zk;YA$7*DfyWG(r1=Pp^YaDgO@94Tq(nFh#hTeixkwQJ;_A%i3(#Vz&f#mbtM$@1f- zjWYO=1W8FRs;0!o)|PL-{8}Q?j`_^THC!XN+wJjZqo7KZMU{S(?!vWTev%yWgiEj6 zdP_=Lrr@#Ei;I@26DLXQwr%B#tFDs#0*~z8wNsLpEtUQc-Y=OMSuC}dNn^&y;Nin0 zCXVlD7MeJ-UY!`3J?%fz@sf+py; Wu*yAoh4UhW~N8?9<9=n4MF44AcX(#2@_<% z<0B<9qLzsPYt@R7{eSM0q&c(Xkx`?hpipGr?|WqF7hg#KgaoM(Ci0(2lchzgR?@L+ zXQQ>mns~X{xw$fT;&}PjutX{Jp-%uwKG&6#ljEsMAK~<&Z?;{c3x?JvC(EZRSIN-P zqa@v(DV{=+`gNjZ#yiucX^R%psdGo!_1hjG`I@^PW zOI}{SoKYu6-k$uHT;1$ExuIJ(N%!GPw*W~o-j$!9?@=C#Z4#(PFT%va#+x-`hV0KT zltF|3DF;(Cq;^zo*|&GEeEPvW8TPM{vU|^7`Rt<)CGoN0^7^ZG6--AYq>=G1!Ouol8oP074~QH+|YPIe0u@?tM7H%zNusCz~== z+fJP&Y0hlv)a^z|IeJuDUUIQelZ>cYQ|8T_DS3wu$grV9CFl5Y@%Z>jDc(seN`}b* zsT&$_rG18NvzjG=zK#nKVOaN4OLlJ2_d{w!_Y4=w6Op z29gc|q>~&73|hmz2@jX!+1c{?l((c+`|BktGE&?uOv=ud)@|EK!}xgFv3E_;<8tHEzl%@E`vBpBl!en{_nC0m%m-$J}(bod9w@KCao&qF)z?Lf|zGB__Yu>GlbYt>PeB~XW ztY5RW`XaviB|H~eNlBX{mI$P3rY?~2^+kV8*n;0{)oMU2ZHa!VKK>_V!+d-fTgvdOd$0pfF$!{-FCzQH8`MJi~Piiy6U>TxA_F5KEzDSd3ol!Vfc6VWYg2HY_2Zbj6pltYv=QF`)wPwnL{GV|fi9Z;fB4q! UAaSk+SO5S307*qoM6N<$f;L(<761SM literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/120.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000000000000000000000000000000000000..d75dee65c4ce777d9edaec7e4fd156b3c0b97e41 GIT binary patch literal 6085 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 LNkvXXu0mjfCULG- literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/128.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000000000000000000000000000000000000..21e955a49dffec75949d4aa1cda8b5e9ca585874 GIT binary patch literal 6644 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 KMNUMnLSTXv+=h1m literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/16.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000000000000000000000000000000000000..b6137a26e79ae4b3a845a8c5714c5310b8fd1835 GIT binary patch literal 555 zcmV+`0@VG9P)Px$&lI8T=*0|gfHOIhwx#105^gl zh$|fg#e!nVScBugm~w6>GU;?tFNBbMx#v6QcfTmtb-lykfa~>&N6Ih^L?RI=$8o&# z`3$AhBmM9^58-ea%C>FK@8rwd?FL=f|0(9W?uWIz0GnycG)*{;^T81vMx#+#<0igi z5nyvD6vAe+!E`!BtyTkfdmrm^xuDc7I(22r!vUqTB7F)oQ`EZHd4pipS$| zjK^bCDisU{1C+~U5WJ>oe;D`2xFpqtO*N@+;tg zbU6iPkHun=JD1DBvMd~rN6hDQBoYaM$ZuJ91{~pTx048kLP5qg91aC;KA)G|lgR{) zMnk4UZoCK10W+@GYk|dGGMS9fCd9+*E4~*CuuKc?+hKDl^7l!k2;H!F#F>}Rno{_& z{fX`A3_~e=wM=}n%%A|9XpI+(&Hl6UJUrVkaG;6xbV$dy&$0jenJ7XAd&j}^%P&~E thJ5l1o=gks(ck%exxmmgJej6U=Qk~{DqWfD@iYJc002ovPDHLkV1m*@1{MGS literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/167.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000000000000000000000000000000000000..b611623f2bce3de509f20ef56ba80865e18ef76f GIT binary patch literal 9065 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 literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/20.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000000000000000000000000000000000000..a19d28382964119365a24ac6be2f912e17b914fd GIT binary patch literal 707 zcmV;!0zCbRP)Px%eMv+?R5(waRozNrQ5aq4$IKkZk&H+}K`g4d5TPp(x(Rxg>Iu3G-=Ujsy6{4u zKvD2A=nZm$Zf2Z8v=08BPS4UFE8O9A8&N=(r>-*MP-`+>DZQJhm`{0~RCRk;t zstUK;EmN^9%kK4hP!weqtmz)xwz09X0j1e&+N)MJUjZvs48ySJ7np#He;dnU761vf zwzdY-G?&UidDhq0C1!ri01}MmRI63QVlfPd!v#<$=K1*f z`F@>JcEB2qhPcL-U<_Q~mKQ6Oc#Cah;d`r1k_dS{_~i4w p-Po)C0$7?MMHiVxt3c0cjcxb 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 literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/29.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000000000000000000000000000000000000..54354239a2f6a1e26cbd686c297cd5463d4fe90a GIT binary patch literal 1132 zcmV-y1e5!TP)Px(CP_p=R7gv`SKBMBdl-JcIhZjFW`-6!k=n6~TxlyV6c?1@f`|(#C5bEduH+Ap zqTI=i+>z6@3#1&Q)}B3*SeuXn?s=_mwHDtDyT$s|*G#|pe(&=hp7(iwqa+fE z1OSA?VT3{<#N%;%-QxC7FGW$H)9GL`nUI#2CiWD927^HXlnq7sX`tr@)F23Ku~?vl zLZQU{{r!i4X@O^CWI*xz{fTHa`pZBK1_KnI&!?94-P5XNkmXanUat`8SFV(xOgov! z=dCLii^;2cy*}xYq@YYgc6PS3(*GeE&k#5e2qfi+CMW?J<@oqG>gwvy*w`q*uSLx0 zMn^|cR#t}I-d?a3wa&Z%rI$9F4fFH!=XJ3EWMzCIKc6=7>@ z3+zcc^!&^VP^t%ei8Yy+m_Suk724a|MX{J-0;@}vIJ4P|^z?N2d_IhgjiI)-7R}Ag zxVyW1TY6Q4tOcXv+Su3-gH26MFdB{DKp6$2+}YW|`T03YN=i^uQzO5--EMJSOUE^d z#1~15i;D{^E-p$G4Gj$v{r2{@M9p`Ki;GcTUk|_EkITzToSvS_)7I98($Z34zE(L> zw#>r90?NzFQC(dvX_}dtL4SY$5B)bVFo3D4DLgzph;zrs$2dDX6KA;hCibcp83`u> z7i+^sR?f}MVS0KRtmDYY2r4TpVYl0bFswJvFiUKgWD_f;BB{(%y`@nC?C$Qu>2#vA zvlF+sw?EWPoyW=QDdRJh_R?#5OhW{w6P$@hM@MkET*7MBQznO^$dG1g+1DK%9rBx& z9BQ_Vnx5t5Wps6QiBYnYp9Ec9T@}!wp&{AlV*SYWwY4=|UtgoArw1O7=WU=gpw?!6 zeI18~hoUm9A=!L#av~8hG7=|0KOY4J1t=^m#LdkOmX?+#R%uG#TNSR&&Q`#tJ zo12>g!bmDADx@9QuXKXxyt=xQz9jitT3V1x`3r}oB=JN7iDydoDv>B<1YkXxUiKbS zNvD`kR(L_ literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/32.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea2797157704f5b2423cdca3a2da6ed72efb69a GIT binary patch literal 1245 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 P00000NkvXXu0mjfD9amI literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/50.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000000000000000000000000000000000000..7e8c3a1c062188d6c99941575ec238e3b150d4b2 GIT binary patch literal 2112 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 literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/57.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000000000000000000000000000000000000..c9d62a127ca9101207a1762efcdb69268cfd935d GIT binary patch literal 2521 zcmV;~2`2W5P)Px;lSxEDRA@u(TWLs?Ya71Ca+z@%Evu6yoid^N+>YD%^tfQBpuH$)C+J7@CxVK8 z^fM|df}&{o(H;dsL=-I)ef5E9dd$@PwByO+M}4`bj^i?FZXefO&oIrInt5k5OwJ2~ zvpnzfJlDNm_x(QV1_lNOV0ZDW4_E?y!O+sE&>f^z`(A zgTL2{IJ~{R;qUKn#UUUmiT3t(G0*QobIue%JT%|Hz`&2zNQ5-e&d$ye0Z<8mK=pdP zn69qBzki^$wRKnq7H1lB_B*XIC@2WJuCA^Di^Vdcv}mhM`lhbEy?ub`VkG#Yqf{tq z#cVbYsC3~>{omv7%oMtomKH~R;Csa5aL+;*OeWJ`7|)17_zB40AV-Qu|JLN!G^wf+ zRschle#lz54lhsEaZ8h2JUBR5z>)JZ&ki=Jx96W^Wd!VwK-~gKmVfr_S&{DI#fvdv z!UT{t4Hi%WWXV)tUyozQjv*u@1e-T+hS6vQdA8PdG=Zcfx#0Qp=do$iCNZOF)289x zy?dBEd9u`@S~uXxB}h(KL9AQ14%e?=mmz6sX}Edwrj$|qU5iFFfuw{ z-n~OyT$~ix1j)tc&6|h2ckgOm+>s^}Nc!UT?c1eP;&<#?Jb3Uxv`eiHi6*%rrOi2W z=7>3|>&A^6Vn$jQ|B^q_TwlC+fz;Gg>kzJa>((vtdoBJbodGS}m8MA_dGqECjvqgc zw{PEK#flZ!uwettX0t3#LAVoqY#V2sBOo9EXU?3#{rmSNUG3Sk2NNew9Qx?0v`Mq2 zv>9!=In`NEoiduRmVOaIaXy+LbFO+ZWhfHB6yid!XS{$+5W1Gd0K! zjvYHz%=YBTlgQ4_7Sr0le?Ml~}dkKKrQ7PlVfdeQkER>W)E0{86ibT>y zix$Z^ey=6i*!sI$AeGNBr5rta6c;XBz}&fWv1Q8^Or1Ja{DvDXC@4T%Tbpc>(NO`V z)+vOTm>2;`YRD)`p>TXnO^pcb>eZ`a#$1EeLBDqgsAiBV!ajfg9Q*d|6HRdw+%$pk zHE!HEoI7{U%8DFCQTp%v`SY=3#}35D$6H&X1VNvt3s6v}PMyM$BS*jp&NYUd))62# z!7xrP$xZSvO^MndU=>83J$okklRnFPR5hWVU(j56MMg%Vw6s)&qoOkBP|=p|aQX6O ziR#OiEfY;^joz9ysUj>TO>HzcH_K*i7bYNX?&8IZ*tl_{L-s}ArMZ&TELgBW1m$S+ z)Fz`WLA%>Jbqk~d%7wq=BLqqy4<9}hlgrD?6M-;t?%K6WtRO5bOzx)@d@XV+t!mYv z$I;ala!jBMC@Q#+G?2`*x?sAXmXr&8Z9dK;twjfqxNsrXuV3#HNSZlwDl3n@d-ozeJ^kB6S89)@LsR2pF}`=E z5rSk8Qe=MV(xomXc9oMk{?MUAh>D7Gt7g@tNiIrY6%`edA8AJ1yvmWb8dbSYZIo$X z`SRu1y?Zy3l9EJ|%H&nlw5NVttGc>c%$!kLA$jHbLt@C?o zamI`ph>eYv_6ci6&PhtAE?P@vWu?qPA!rG99ussJ2MfUS4S3Aqs9)nb%)uz>Ftu#yk z*i;|9-?l0tI6q%s@Vu4kCGl@hZy$PkzW$hE&_HSp>#mL$O?!L8*V`MVWo4*qZbpD! zFW~xmdoeR&8h!{1Lx;s;)$E}9q^leCWE{=c)YOFED=X3N={(P(WkL2vljNUJI? zLge^ROplC&&dbYTfEu=%qz0KzUX;8m@d>D@t_Et~;pfCeF!@`7IyseLNB>(RsL@;}LW9_Qs>9IruT@p9u2T zBm41Vq-JHI__qSgij754eme{wIeP$35^X6s3|MOoL_z+>-g*-Bteo{4f|7)OcTanBl2^f z!Q`Vy;?Id_X|o_G(15!7Mm)cN7i*U<7WGmHU& z7!z!in$Px;xJg7oRA@u(TWv^G>lS`S8&k(dEisRF?5J7in&ioDWt}=+E&HG-=nF0s1Vs?k zkBEqZD1s=Wp!!k$h>EBm1$_{mKFF-owY=s>S3Q*;jnbQ!ylT#~hGBGO@BO~dTF+YRS?_+w;BYt`a69->M@NUey1Kf+|2>@Me%T-LzrkPtU%tM+ zFdB{W;{Wc4!5wH@TO0TyKpvhh`a=+T8B{rffJcuWEic!@36v+=Y&P-Ya02Lwjl|;T z=LeI?m{0~XmM)KS zVAiHlqeh{nr9~z(kV*7rDBhRiJRl$d?d|O%Q`w#9IWz<1;Qs#psIIO?U0offPMs>^ znSciqtQx1LrUoS?C73vIqKwtp*r;n9O+ZN_dXb$@YHBJfDk_kioQ#VXFCs23ZqUJU ztQ9L(;PKz|*Hs#j5;_mgfXw zVq(zR+Nz6Mu5m{Yp`oEzvt|u$-MS^M7JFsJ>a%Ch5E~mS<9s!ss&k(=Zyt(^i)A47 z8G$m61_LOK$A)>?vSrB1$`XJIXkK0(A|oR;vqzIhY3bnLU@Td(M5sznl4dh!&cu@^ zPh_o0iVS zMIM$Mz12U}F7jP1gETZWptiOa;Q}U2nuL`rSIXoVOZ<*rARs0xr*#bmiFJCA;)C~M4K6Dy6N9~d`&P0#V>CWK z9((uhMM6RXNaO#jM>%n3#iF7j?A*Ci2B&~XNlB5|B6T@&PB1q&SAeqMyRN1)<`yqr zj13z$h!7yvDFk>grFs~f6b*ds+qVzv*RK~tXjMhJVpNHSW5H!RT9(g-Xir0_T$Ho@*TrP&tjgg80AE%KWSOg z)Kik+WLOT4A3u%*2M$R2aXnc4)6&wUIY>%MlKb>ll2p6KcdZ(Cz*$#i%)aU+&!IO; zOG}ZNnTh-N@8i|0S2%zEJkrzC#nZ}I-s&!m>+PJrLPVEGwWN1?fHjudl{%G4g@Bo8 z)T)8rOsZubUmYkX!-T|)%Fg5P;lsFd=Z+?YqdYl~htax$(o_9S^D0wkXJ_Nkp+mTP z_pW4RZ=O_%kBNwJe(KaIXI9qYO`DKCvFr__OM5e91)=GM*0JkqN2RK z9Gf<6!o!CTovqGmW)flq$Tw$rJ-vESDP<* zAAUy#wQJWdDNmwPF$4%0;57=ePsViy0XMD z-dHzjDArM`C!WWHanF0GeH2tpq$_)0E|3wl-gPO>Ksh0`u1iX;UcFi-x@XTGX};+_ zPKI&CNs{6`f7Yy7Sh{qn0L#zM7q3=UR!TOey5cGYH7|`29v&{BuV258-Me?=^5x57 zP`x@=cQR678Ds21m<>5`0$0l`{EiZl8kHb<9=|6Y`8O#_?@~-q__JGKcS1QC5)vYw zWwJVY^r&3V&-GAtz{-d>P~w{uBSpF5z*wR{py$-;9ZLy4q*fl3R;4jWalR|#ae1by zs!Fmty~}fXeSb??Z`oKEb=_~KK#;;}2~=&$BDmhlh%%IVr8{Ri(^sv*BH3 zB`W`0i zzD0OMgek;3!PnE`0(#4)K-)ubw)BwY|eW&Z4OG-?GN?zoZkLYM`M_zU|(wAhQ^v!QDHCm9AG!1q> z#&lU*9DhlJjiA4VMXGmR`j=~oOQeTsjr=h5z2owB# z5gQlx+2awBl|y+$l{66T?RfMeA2TyDU~6nbaX~(2&Ho8Sd3lIW_zQ{)3NZI?Y49jl2 zo2-~OCj~8T&rNxrYC34yh)4(;?{a}&~koQsa0XF;w?&ABmI z?RXsOTg~a2rp89pfBGb}t!rpNL|7PtOeUCvgGD{LN>Kj&#a43_wCp@oW2;xI>aCmo zxsDBk^XZ(y08*MUOu>MjU$rQ(1nKd|18Sh5{Bsa@k9l&2*Pny9T9h{&X+QA!9sCwj Ud2)9^Q2+n{07*qoM6N<$f{KpO1poj5 literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/60.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000000000000000000000000000000000000..43dfc8c804931aabf6b05d5c300ef65b6a09843e GIT binary patch literal 2619 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 P00000NkvXXu0mjfD#VDp literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/72.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000000000000000000000000000000000000..2ab33e56f5ee9e41de32dbb22f96b8280ac9799f GIT binary patch literal 3271 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 IM6N<$g2`~T+yDRo literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/80.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000000000000000000000000000000000000..c4572eb6f45d3c23fb96a4423bf6f58aca2b1c19 GIT binary patch literal 3697 zcmV-%4vz7OP)Px@D@jB_RCr$PU1@BT)f#@L)6x}aOIb==D9F76RjjlXuuzM*u*;4JxN(0NqtQg; zAODzWVl*Z$#3dxSq9B&CT%bZxuqXvtD#8V!Tp%qVOQ9QWryc0rJTJb@N1f$s=PT)* z&X-IEzB%*FInR05=RN1t=5o1Q@N)6v>({Tt>2%6_b8|Cz&&SuiJ+f1NX0zGAS4c<* z>~=fC!ouX8_q<&;PXH3Arlux&DQJ9_O+ydCx*a(Ni zA;5J7(0pki!HbB9KzMlg-gwu(lIKG-31W0~wAe*4Z*(HOSyNLJaGtfTYHGGU&0CKiJw(_9KstXp8-0Z5Yt(6!r=s3(FNB|ilcJJODHmB3+x_b4hxo-wr-Go3)Obl!d z4GpfK`nFv%RE=%5wY4s$X+iaE8;Asu>7K2+y4s}~N826|Y|RYCR#jEyi%$q5Yw1kg_j2?7`d&`$*X{CZFVg8=%8fS+IQ5)#Oux1$mTO95m+&W^If z{I{8+jGqGF_3H{8`$V#Ym#5lBd;4J-fCP;64zJMAP$3-2FY9_2gWrDptz~@U#*M@J_3Ndxs@}c^Z`J42 zf@#yHVaJXg(l<(C@7}$!b?a8#e*5i~Nuu8XvSab_@%Zes&#+|45>Xq1#Q{+AUU=aJ zy!YOFsHmurdmVkTKXGw!ShZ>u7A{;Un!$T&QXYNuQGD~wH@I@;iU8~H=ZIhFe1wgjB6=ehE>Bf&AkA3_034j_AuxxwZefMF@ zmMxtJK&_S1-W@n_;3fe1+2Fy01;ATn2l<;c6`WA^#|R)L@PP*&z~;@HQC(f#IaHXl z%i;<LkCXbf}_ly!KeGF@?y8O0* z?1;`fz1zx_D{=1JIq`%`moCN7p+hC^vclt;9kiO2H{N(d0Av3+iIl#?#6(Lc(-Hs` zL`ot%Ph(A0=EZ*-!80x&GYg2fcCA;C8NEDB3+*0Nm`;q6XLV3-8Gsz5=9>ge7hzIu zb&;l~cx+4iXvM|!k-wpfKna#HXqUo|&DOG1&uTMbDS++(;eZ&#axDp^vAn!o0A*@r zMKzG=8l@~DAp!mR^%K)Uz&Jr%Frcdlte7l!&*(4!HF#xEd-?KZeE#|8`1s?G#Xd3? zTf26x0BUx*u&x+Yhd?Q(I!>4{0VhwM6wny53>Yv#Qp%Y#XJX8lF>=4Es!F7^BZ7F# z0VE)lI@&|poR2>G2yegrw%EjHpM6#aOheA}k04Vr%{Ul7m)Qfoxzc0=gbs>8a*`;G z2M->^_U+q+*ho)L$7`>>hKUm=iZrr6omz#JLZb0zR`S$SPvPRli+J_bSB04&sJiZ& zgCVHjfB(G%yaa=)$eh_|!+4*mVp>|7fX~#>xDb?J5)`$sDxrrC9g>M-PCRw$R4iMz zOqe07!zs%oP${I!%1XTc`s?`QlTX0J{r&gf7yHD>UnPLjM9C{EDv|))>_s+(vE4Jz zJkt_es#>ZkP!g2^G#v~j^0CJrlSv^nqq(tF>TC&s3LaCo>C>m9Z{NN$`=pap#f(gh z2Hy;_Crz3p1Etz~yVR4jO;S=4cJ10F4ujuw2L}73K{Y3C`0(LEc)a!2Tb43ImH^1X z5riX0j$rrh-J-huKc&%=@m3IDdg&zzaOtTzP>mB6Om!Z{trm>w4j(=&{GP9cQ3RM$ zN7F!CN&D*wbhDaZY-b68e4udwHocyDVNab>Rpa2OV)^;`(o`LU#*{`tIe0n-f}Wk7 zEw<2%Qy}=9APsM-x1{mnPu z6y}BX*pVYgisYCDk&~bVE1d}Ix>P&n)O03BKG19?L66SBd-3AMIC0_xii?Y7V%w`7 z9qL3lcyfHCtymGGx-fI#;AKOKo3jTLG!BT8%Q%q1@~Km&Fl*Kr zjq7Bq!Cx=q0O@rZ4waOYATu))-+lKTX3UsjSv}bDOhgAn&h5!3pOg-B2?g06Q4ePT9SS~i;_e3gZ;tQ0Ol_51XRe)mN zJ8U&f295UDb@Y>nsPbwKpYeLV|z;qQp_%2+F8YqhzHN zRZ^*T0!yGY=hgH}?~!Aq%4?bO%$YL+4tYoxsVJdrJ7UBLELyZkq>r@_1=*}rwWVE$ z0jM@gHzvFDe3UXRd=oTZ42zNud}mud&qZs)t(^#MuR90R7Jz)U?N9*ugrHcU5&Wd*p{_CG%>O9@o3^1VQtu1&?NxW(gAbzhVx+E_x*-Na*D$01sFIskV$SHx(4%T6}M0kGmJA zPM$_WpT0L1g!YI=QQ;4WPw0)|ca1=OT|MYnDl00GnA{&_W#u@qbqf~Gnu!o@&+u@} zV!#5(a|}XlHmu&X8QG7_L`+mTHmzBUf2Q4m)FHPCBeVDCUl17;g*)!PTPXi46%{Dn zu^p3U&6f7tHWeTvH3g}IZxac1Cxrof=mf)+FuNUjn+uRRaWZ0}qj3J*pV(i#3sdLL zWl0<*yLThRZpVnMOtFC*SLY!u{cfZTN`(s{`03y4Fkxi6NU6J}ClaIw>ai!P!ujhr zAUPuo{gaXq)-4nT`D-y`=rE+E4aJ__d*N(qLQYOLN)8^zsnX+^GG`WQYwBPR3qw(U z9_CJ-1eb?nKqci2)Pr}VpcEdCv*l&jwyy-!7A}CJz7f?`RrqCFA?7b$g#G&uNVt-9 z&plYZVg;tmoeR4?6w%R6eJy+#HmiIE7<}58)qaX*hZ8n8bSb-ai4+F)^sEt-+R6t1$ikTqMi> z79I=e{s3Aze5@-Re@AJFij2g@O`FgZ(*vW&kH?8)#}FGAkIKuJ(I>GVV!HRhpJ&Q& z=;xpDVD=ac7?|pDKOLXfa{OIaIDkE8mKgEnC|B@B54#$m+;Id5E=ECxK z-^G-1V=*!_(`)TS)mIK+>!Rw<)}Sb ziiPv$;i{MIunb>rIs%k*O&}jaONn8=%c9ax#l^63qjhsLoTRaAAqc zWXJ*mPqkw{>u-n9D&G~z#~^FiVGjurr$Gj$;?gCly(A?kBg7s88*FeiHhNqF%xAPd zOS(%P#4QCc-M#tOsUEa}1hJ*g>!!x6%(~Bmxb-2%qu{MB1Kw%|+Sl)SqxQ9AK#)xz z1+PCy!yEi#1UCv-ZT@2ft?RtH6O!**z%i~e@HNZ-51(uGPQx0re-7h+TE!ETNQIn_ P00000NkvXXu0mjf&9@M8 literal 0 HcmV?d00001 diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/87.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000000000000000000000000000000000000..0f6478a7e852632f68529d627f1b6f8bceed4859 GIT binary patch literal 4054 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 UIViewController { + return IosAppKt.compose() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + + } +} diff --git a/Diary/Diary/DiaryApp.swift b/Diary/Diary/DiaryApp.swift new file mode 100644 index 00000000..3d677643 --- /dev/null +++ b/Diary/Diary/DiaryApp.swift @@ -0,0 +1,15 @@ +import SwiftUI +import Kotlin + +@main +struct DiaryApp: App { + init() { + IosInitializerKt.doInit() + } + + var body: some Scene { + WindowGroup { + ContentView().ignoresSafeArea() + } + } +} diff --git a/Diary/Diary/Info.plist b/Diary/Diary/Info.plist new file mode 100644 index 00000000..4ffeac78 --- /dev/null +++ b/Diary/Diary/Info.plist @@ -0,0 +1,19 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + Diary Api Url + $(DIARY_API_URL) + Holiday Api Key + $(HOLIDAY_API_KEY) + Holiday Api Url + $(HOLIDAY_API_URL) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/Diary/Diary/Preview Content/Preview Assets.xcassets/Contents.json b/Diary/Diary/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Diary/Diary/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Diary/Diary/appstore.png b/Diary/Diary/appstore.png new file mode 100644 index 0000000000000000000000000000000000000000..d9f4666f921a7ac0dc63804c83ecd4bca3ec2bc6 GIT binary patch literal 37749 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, ) : AccountPreferences { + override suspend fun save(email: String, uid: String, token: String) { + dataStore.edit { + it[stringPreferencesKey(EMAIL)] = email + it[stringPreferencesKey(UID)] = uid + it[stringPreferencesKey(TOKEN)] = token + } + } + + override suspend fun clear() { + dataStore.edit { + it.remove(stringPreferencesKey(EMAIL)) + it.remove(stringPreferencesKey(UID)) + it.remove(stringPreferencesKey(TOKEN)) + } + } + + override fun getEmail(): Flow { + return dataStore.data.mapLatest { it[stringPreferencesKey(EMAIL)] } + } + + override fun getUid(): Flow { + return dataStore.data.mapLatest { it[stringPreferencesKey(UID)] } + } + + override fun getToken(): Flow { + return dataStore.data.mapLatest { it[stringPreferencesKey(TOKEN)] } + } + + companion object { + private const val EMAIL = "EMAIL" + private const val UID = "UID" + private const val TOKEN = "TOKEN" + } +} diff --git a/app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferencesModule.kt b/app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferencesModule.kt new file mode 100644 index 00000000..8711465e --- /dev/null +++ b/app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferencesModule.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.account.preferences.datastore + +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences +import io.github.taetae98coding.diary.library.koin.datastore.getDataStore +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton +import org.koin.core.component.KoinComponent + +@Module +@ComponentScan +public class AccountDataStorePreferencesModule : KoinComponent { + @Singleton + internal fun providesAccountPreferences(): AccountPreferences { + return AccountDataStorePreferences(getDataStore("account.preferences_pb")) + } +} diff --git a/app/core/account-preferences-memory/README.md b/app/core/account-preferences-memory/README.md new file mode 100644 index 00000000..68b1d1a4 --- /dev/null +++ b/app/core/account-preferences-memory/README.md @@ -0,0 +1,3 @@ +# :app:core:account-preferences-memory module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_account_preferences_memory.svg) diff --git a/app/core/account-preferences-memory/build.gradle.kts b/app/core/account-preferences-memory/build.gradle.kts new file mode 100644 index 00000000..9811752b --- /dev/null +++ b/app/core/account-preferences-memory/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.koin.all") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:account-preferences")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.account.preferences.memory" +} diff --git a/app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountMemoryPreferences.kt b/app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountMemoryPreferences.kt new file mode 100644 index 00000000..7d768667 --- /dev/null +++ b/app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountMemoryPreferences.kt @@ -0,0 +1,36 @@ +package io.github.taetae98coding.diary.fore.account.preferences.memory + +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +internal data object AccountMemoryPreferences : AccountPreferences { + private val emailFlow = MutableStateFlow(null) + private val uidFlow = MutableStateFlow(null) + private val tokenFlow = MutableStateFlow(null) + + override suspend fun save(email: String, uid: String, token: String) { + emailFlow.emit(email) + uidFlow.emit(uid) + tokenFlow.emit(token) + } + + override suspend fun clear() { + emailFlow.emit(null) + uidFlow.emit(null) + tokenFlow.emit(null) + } + + override fun getEmail(): Flow { + return emailFlow.asStateFlow() + } + + override fun getUid(): Flow { + return uidFlow.asStateFlow() + } + + override fun getToken(): Flow { + return tokenFlow.asStateFlow() + } +} diff --git a/app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountPreferencesMemoryModule.kt b/app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountPreferencesMemoryModule.kt new file mode 100644 index 00000000..acd85650 --- /dev/null +++ b/app/core/account-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/fore/account/preferences/memory/AccountPreferencesMemoryModule.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.fore.account.preferences.memory + +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton + +@Module +@ComponentScan +public class AccountPreferencesMemoryModule { + @Singleton + internal fun providesAccountPreferences(): AccountPreferences { + return AccountMemoryPreferences + } +} diff --git a/app/core/account-preferences/README.md b/app/core/account-preferences/README.md new file mode 100644 index 00000000..4ed24f72 --- /dev/null +++ b/app/core/account-preferences/README.md @@ -0,0 +1,3 @@ +# :app:core:account-preferences module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_account_preferences.svg) diff --git a/app/core/account-preferences/build.gradle.kts b/app/core/account-preferences/build.gradle.kts new file mode 100644 index 00000000..b0077266 --- /dev/null +++ b/app/core/account-preferences/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.coroutines.core) + } + } + } +} diff --git a/app/core/account-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/AccountPreferences.kt b/app/core/account-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/AccountPreferences.kt new file mode 100644 index 00000000..af0cbea8 --- /dev/null +++ b/app/core/account-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/AccountPreferences.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.account.preferences + +import kotlinx.coroutines.flow.Flow + +public interface AccountPreferences { + public suspend fun save(email: String, uid: String, token: String) + public suspend fun clear() + + public fun getEmail(): Flow + public fun getUid(): Flow + public fun getToken(): Flow +} diff --git a/app/core/calendar-compose/README.md b/app/core/calendar-compose/README.md new file mode 100644 index 00000000..05c28068 --- /dev/null +++ b/app/core/calendar-compose/README.md @@ -0,0 +1,3 @@ +# :app:core:calendar-compose module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_calendar_compose.svg) diff --git a/app/core/calendar-compose/build.gradle.kts b/app/core/calendar-compose/build.gradle.kts new file mode 100644 index 00000000..234b2eff --- /dev/null +++ b/app/core/calendar-compose/build.gradle.kts @@ -0,0 +1,47 @@ +import org.jetbrains.compose.resources.ResourcesExtension +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.compose") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:resources")) + implementation(project(":app:core:design-system")) + implementation(project(":library:color")) + implementation(project(":library:datetime")) + + implementation(compose.material3) + implementation(libs.lifecycle.compose) + } + } + + androidMain { + dependencies { + implementation(compose.preview) + } + } + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + invokeWhenCreated("androidDebug") { + dependencies { + implementation(compose.uiTooling) + } + } + } +} + +compose { + resources { + generateResClass = ResourcesExtension.ResourceClassGeneration.Never + } +} + +android { + namespace = "${Build.NAMESPACE}.core.calendar.compose" +} diff --git a/app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarPreview.kt b/app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarPreview.kt new file mode 100644 index 00000000..63035c3d --- /dev/null +++ b/app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarPreview.kt @@ -0,0 +1,44 @@ +package io.github.taetae98coding.diary.core.calendar.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.core.calendar.compose.state.rememberCalendarState +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import kotlinx.datetime.LocalDate + +@DiaryPreview +@Composable +private fun CalendarPreview() { + DiaryTheme { + val state = rememberCalendarState( + initialLocalDate = LocalDate(2000, 1, 1), + ) + + Calendar( + state = state, + primaryDateListProvider = { + listOf( + LocalDate(2000, 1, 1), + LocalDate(2000, 1, 31), + ) + }, + textItemListProvider = { + listOf( + CalendarItemUiState.Text("2-5", "2-5", (0xFFFFFFFF).toInt(), LocalDate(2000, 1, 2), LocalDate(2000, 1, 5)), + ) + }, + holidayListProvider = { + listOf( + CalendarItemUiState.Holiday("새해", LocalDate(2000, 1, 1), LocalDate(2000, 1, 1)), + ) + }, + onCalendarItemClick = {} + ) + + LaunchedEffect(state) { + state.drag(LocalDate(2000, 1, 7)..LocalDate(2000, 1, 17)) + } + } +} diff --git a/app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarTopBarPreview.kt b/app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarTopBarPreview.kt new file mode 100644 index 00000000..8911bf84 --- /dev/null +++ b/app/core/calendar-compose/src/androidMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarTopBarPreview.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.core.calendar.compose + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.calendar.compose.state.rememberCalendarState +import io.github.taetae98coding.diary.core.calendar.compose.topbar.CalendarTopBar +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@DiaryPreview +@Composable +private fun CalendarTopBarPreview() { + DiaryTheme { + CalendarTopBar(state = rememberCalendarState()) + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/Calendar.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/Calendar.kt new file mode 100644 index 00000000..60bee356 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/Calendar.kt @@ -0,0 +1,102 @@ +package io.github.taetae98coding.diary.core.calendar.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.core.calendar.compose.month.CalendarMonth +import io.github.taetae98coding.diary.core.calendar.compose.month.CalendarMonthState +import io.github.taetae98coding.diary.core.calendar.compose.state.CalendarState +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.plus + +@Composable +public fun Calendar( + state: CalendarState, + primaryDateListProvider: () -> List, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + Surface( + modifier = modifier, + color = DiaryTheme.color.background, + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + DayOfWeekRow() + CalendarPager( + state = state, + primaryDateListProvider = primaryDateListProvider, + textItemListProvider = textItemListProvider, + holidayListProvider = holidayListProvider, + onCalendarItemClick = onCalendarItemClick, + colors = colors, + ) + } + } +} + +@Composable +internal fun CalendarPager( + state: CalendarState, + primaryDateListProvider: () -> List, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + HorizontalPager( + state = state.pagerState, + modifier = modifier, + beyondViewportPageCount = 1, + key = { it }, + ) { page -> + val monthState = remember { + val localDate = LocalDate(1, 1, 1).plus(page, DateTimeUnit.MONTH) + + CalendarMonthState(year = localDate.year, month = localDate.month) + } + + CalendarMonth( + state = monthState, + primaryDateListProvider = primaryDateListProvider, + textItemListProvider = textItemListProvider, + holidayListProvider = holidayListProvider, + onCalendarItemClick = onCalendarItemClick, + colors = colors, + ) + + Drag( + state = state, + monthState = monthState, + ) + } +} + +@Composable +private fun Drag( + state: CalendarState, + monthState: CalendarMonthState, +) { + val currentSelectedDateRange = state.selectedDateRange + + LaunchedEffect(currentSelectedDateRange) { + if (currentSelectedDateRange == null) { + monthState.finishDrag() + } else { + monthState.drag(currentSelectedDateRange) + } + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarDefaults.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarDefaults.kt new file mode 100644 index 00000000..75a09832 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/CalendarDefaults.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.core.calendar.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +public data object CalendarDefaults { + @Composable + public fun colors(): CalendarColors { + return CalendarColors( + sundayColor = Color(0xFFFF5F56), + saturdayColor = Color(0xFF2133FF), + dayColor = Color.Unspecified, + primaryColor = DiaryTheme.color.primary, + onPrimaryColor = DiaryTheme.color.onPrimary, + selectColor = Color.Unspecified, + ) + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/DayOfWeekRow.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/DayOfWeekRow.kt new file mode 100644 index 00000000..0d1c1846 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/DayOfWeekRow.kt @@ -0,0 +1,80 @@ +package io.github.taetae98coding.diary.core.calendar.compose + +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.text.style.TextAlign +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.fri +import io.github.taetae98coding.diary.core.resources.mon +import io.github.taetae98coding.diary.core.resources.sat +import io.github.taetae98coding.diary.core.resources.sun +import io.github.taetae98coding.diary.core.resources.thu +import io.github.taetae98coding.diary.core.resources.tue +import io.github.taetae98coding.diary.core.resources.wed +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun DayOfWeekRow( + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + Row(modifier = modifier) { + val dayOfWeekModifier = Modifier.weight(1F) + + Text( + text = stringResource(Res.string.sun), + modifier = dayOfWeekModifier, + color = colors.sundayColor, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.bodySmall, + ) + Text( + text = stringResource(Res.string.mon), + modifier = dayOfWeekModifier, + color = colors.dayColor.takeOrElse { LocalContentColor.current }, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.bodySmall, + ) + Text( + text = stringResource(Res.string.tue), + modifier = dayOfWeekModifier, + color = colors.dayColor.takeOrElse { LocalContentColor.current }, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.bodySmall, + ) + Text( + text = stringResource(Res.string.wed), + modifier = dayOfWeekModifier, + color = colors.dayColor.takeOrElse { LocalContentColor.current }, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.bodySmall, + ) + Text( + text = stringResource(Res.string.thu), + modifier = dayOfWeekModifier, + color = colors.dayColor.takeOrElse { LocalContentColor.current }, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.bodySmall, + ) + Text( + text = stringResource(Res.string.fri), + modifier = dayOfWeekModifier, + color = colors.dayColor.takeOrElse { LocalContentColor.current }, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.bodySmall, + ) + Text( + text = stringResource(Res.string.sat), + modifier = dayOfWeekModifier, + color = colors.saturdayColor, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.bodySmall, + ) + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/color/CalendarColors.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/color/CalendarColors.kt new file mode 100644 index 00000000..99deff7d --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/color/CalendarColors.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.calendar.compose.color + +import androidx.compose.ui.graphics.Color + +public data class CalendarColors( + val sundayColor: Color, + val saturdayColor: Color, + val dayColor: Color, + val primaryColor: Color, + val onPrimaryColor: Color, + val selectColor: Color, +) diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonth.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonth.kt new file mode 100644 index 00000000..7e9bfd76 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonth.kt @@ -0,0 +1,78 @@ +package io.github.taetae98coding.diary.core.calendar.compose.day + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.calendar.compose.CalendarDefaults +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate + +@Composable +internal fun CalendarDayOfMonth( + state: CalendarDayOfMonthState, + primaryDateListProvider: () -> List, + holidayListProvider: () -> List, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + val isPrimary by remember { derivedStateOf { state.localDate in primaryDateListProvider() } } + val isHoliday by remember { derivedStateOf { holidayListProvider().any { state.localDate in it } } } + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + val color = when { + isPrimary -> colors.onPrimaryColor + state.dayOfWeek == DayOfWeek.SUNDAY || isHoliday -> colors.sundayColor + state.dayOfWeek == DayOfWeek.SATURDAY -> colors.saturdayColor + else -> colors.dayColor.takeOrElse { LocalContentColor.current } + }.run { + copy( + alpha = alpha * if (isPrimary || state.isInMonth) { + 1F + } else { + 0.38F + }, + ) + } + val size = with(LocalDensity.current) { + DiaryTheme.typography.labelMedium.fontSize.toDp() + 16.dp + } + + Box( + modifier = Modifier.size(size) + .run { + if (isPrimary) { + background(color = colors.primaryColor, shape = CircleShape) + } else { + this + } + }, + contentAlignment = Alignment.Center, + ) { + Text( + text = state.localDate.dayOfMonth.toString(), + color = color, + textAlign = TextAlign.Center, + style = DiaryTheme.typography.labelMedium, + ) + } + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonthState.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonthState.kt new file mode 100644 index 00000000..894c0d5e --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/day/CalendarDayOfMonthState.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.core.calendar.compose.day + +import io.github.taetae98coding.diary.library.datetime.invoke +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month + +internal class CalendarDayOfMonthState( + val year: Int, + val month: Month, + val weekOfMonth: Int, + val dayOfWeek: DayOfWeek, +) { + val localDate = LocalDate(year, month, weekOfMonth, dayOfWeek) + val isInMonth = year == localDate.year && month == localDate.month +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/item/CalendarItemUiState.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/item/CalendarItemUiState.kt new file mode 100644 index 00000000..5f1f5963 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/item/CalendarItemUiState.kt @@ -0,0 +1,36 @@ +package io.github.taetae98coding.diary.core.calendar.compose.item + +import kotlinx.datetime.LocalDate + +public sealed class CalendarItemUiState : ClosedRange, Comparable { + public abstract val key: Any + public abstract val text: String + + public data class Holiday( + override val text: String, + override val start: LocalDate, + override val endInclusive: LocalDate, + ) : CalendarItemUiState() { + override val key: String = "$text($start~$endInclusive)" + } + + public data class Text( + override val key: Any, + override val text: String, + val color: Int, + override val start: LocalDate, + override val endInclusive: LocalDate, + ) : CalendarItemUiState() + + override fun compareTo(other: CalendarItemUiState): Int { + if (start != other.start) { + return compareValues(start, other.start) + } + + if (endInclusive != other.endInclusive) { + return -compareValues(endInclusive, other.endInclusive) + } + + return compareValues(text, other.text) + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/modifier/CalendarDateRangeSelectable.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/modifier/CalendarDateRangeSelectable.kt new file mode 100644 index 00000000..c2f3a0f3 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/modifier/CalendarDateRangeSelectable.kt @@ -0,0 +1,84 @@ +package io.github.taetae98coding.diary.core.calendar.compose.modifier + +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback +import io.github.taetae98coding.diary.core.calendar.compose.state.CalendarState +import io.github.taetae98coding.diary.library.datetime.invoke +import kotlinx.coroutines.launch +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.plus + +@Composable +public fun Modifier.calendarDateRangeSelectable( + state: CalendarState, + onSelectDate: (ClosedRange) -> Unit, +): Modifier { + val haptic = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + + fun drag(dateRange: ClosedRange) { + state.drag(dateRange) + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + + return pointerInput(state) { + lateinit var dragStartDate: LocalDate + lateinit var currentDragDate: LocalDate + + fun getSelectDate(): ClosedRange { + return minOf(dragStartDate, currentDragDate)..maxOf(dragStartDate, currentDragDate) + } + + detectDragGesturesAfterLongPress( + onDragStart = { offset -> + val (row, column) = offsetToRowColumn(offset) + val date = state.localDate(row, column) + + dragStartDate = date + currentDragDate = date + drag(getSelectDate()) + }, + onDragEnd = { + state.finishDrag() + onSelectDate(getSelectDate()) + }, + onDragCancel = state::finishDrag, + onDrag = { change, _ -> + val (row, column) = offsetToRowColumn(change.position) + val date = state.localDate(row, column) + + if (!state.pagerState.isScrollInProgress) { + if (column <= 0.33F) { + coroutineScope.launch { state.animateScrollToBackward() } + } else if (column >= 6.66F) { + coroutineScope.launch { state.animateScrollToForward() } + } + } + + if (currentDragDate != date) { + currentDragDate = date + drag(getSelectDate()) + } + }, + ) + } +} + +private fun PointerInputScope.offsetToRowColumn(offset: Offset): Pair { + return (offset.y * 6 / size.height) to (offset.x * 7 / size.width) +} + +private fun CalendarState.localDate(row: Float, column: Float): LocalDate { + return LocalDate(year, month, 0, DayOfWeek.SUNDAY) + .plus(row.toInt(), DateTimeUnit.WEEK) + .plus(column.toInt(), DateTimeUnit.DAY) +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonth.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonth.kt new file mode 100644 index 00000000..5558cbbb --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonth.kt @@ -0,0 +1,69 @@ +package io.github.taetae98coding.diary.core.calendar.compose.month + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.calendar.compose.CalendarDefaults +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.core.calendar.compose.week.CalendarWeek +import io.github.taetae98coding.diary.core.calendar.compose.week.CalendarWeekState +import kotlinx.datetime.LocalDate + +@Composable +internal fun CalendarMonth( + state: CalendarMonthState, + primaryDateListProvider: () -> List, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + Column(modifier = modifier) { + val weekModifier = Modifier.weight(1F) + + repeat(6) { weekOfMonth -> + val weekState = remember { + CalendarWeekState( + year = state.year, + month = state.month, + weekOfMonth = weekOfMonth, + ) + } + + CalendarWeek( + state = weekState, + primaryDateListProvider = primaryDateListProvider, + textItemListProvider = textItemListProvider, + holidayListProvider = holidayListProvider, + onCalendarItemClick = onCalendarItemClick, + modifier = weekModifier, + colors = colors, + ) + + Drag( + state = state, + weekState = weekState, + ) + } + } +} + +@Composable +private fun Drag( + state: CalendarMonthState, + weekState: CalendarWeekState, +) { + val currentSelectedDateRange = state.selectedDateRange + + LaunchedEffect(currentSelectedDateRange) { + if (currentSelectedDateRange == null) { + weekState.finishDrag() + } else { + weekState.drag(currentSelectedDateRange) + } + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonthState.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonthState.kt new file mode 100644 index 00000000..101d9205 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/month/CalendarMonthState.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.core.calendar.compose.month + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month + +internal class CalendarMonthState( + val year: Int, + val month: Month, +) { + var selectedDateRange: ClosedRange? by mutableStateOf(null) + private set + + fun drag(dateRange: ClosedRange) { + selectedDateRange = dateRange + } + + fun finishDrag() { + selectedDateRange = null + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/CalendarState.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/CalendarState.kt new file mode 100644 index 00000000..4384b881 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/CalendarState.kt @@ -0,0 +1,56 @@ +package io.github.taetae98coding.diary.core.calendar.compose.state + +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.plus + +public class CalendarState internal constructor( + internal val pagerState: PagerState, +) { + internal val localDate: LocalDate + get() = LocalDate(1, 1, 1).plus(pagerState.currentPage, DateTimeUnit.MONTH) + + internal var selectedDateRange: ClosedRange? by mutableStateOf(null) + private set + + public val year: Int + get() = localDate.year + + public val month: Month + get() = localDate.month + + internal suspend fun animateScrollTo(localDate: LocalDate) { + pagerState.animateScrollToPage(page(localDate)) + } + + internal suspend fun animateScrollToBackward() { + if (pagerState.canScrollBackward) { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + } + + internal suspend fun animateScrollToForward() { + if (pagerState.canScrollForward) { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + + internal fun drag(dateRange: ClosedRange) { + selectedDateRange = dateRange + } + + internal fun finishDrag() { + selectedDateRange = null + } + + public companion object { + public fun page(localDate: LocalDate): Int { + return (localDate.year - 1) * 12 + (localDate.monthNumber - 1) + } + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/RememberCalendarState.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/RememberCalendarState.kt new file mode 100644 index 00000000..7c54b2dd --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/state/RememberCalendarState.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.core.calendar.compose.state + +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import io.github.taetae98coding.diary.library.datetime.todayIn +import kotlinx.datetime.LocalDate + +@Composable +public fun rememberCalendarState( + initialLocalDate: LocalDate = LocalDate.todayIn(), +): CalendarState { + val pagerState = rememberPagerState(CalendarState.page(initialLocalDate)) { Int.MAX_VALUE } + + return remember { + CalendarState( + pagerState = pagerState, + ) + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt new file mode 100644 index 00000000..cbfe80e6 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/topbar/CalendarTopBar.kt @@ -0,0 +1,130 @@ +package io.github.taetae98coding.diary.core.calendar.compose.topbar + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LifecycleStartEffect +import io.github.taetae98coding.diary.core.calendar.compose.state.CalendarState +import io.github.taetae98coding.diary.core.design.system.date.DiaryDatePickerDialog +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.icon.DropDownIcon +import io.github.taetae98coding.diary.core.resources.icon.DropUpIcon +import io.github.taetae98coding.diary.core.resources.year_and_month +import io.github.taetae98coding.diary.library.datetime.todayIn +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun CalendarTopBar( + state: CalendarState, + modifier: Modifier = Modifier, +) { + CenterAlignedTopAppBar( + title = { + val coroutineScope = rememberCoroutineScope() + var isDialogVisible by rememberSaveable { mutableStateOf(false) } + + Row( + modifier = Modifier.clip(CircleShape) + .clickable { isDialogVisible = true } + .padding(horizontal = 8.dp, vertical = 4.dp) + .padding(start = 8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = stringResource(Res.string.year_and_month, state.localDate.year, state.localDate.monthNumber)) + DropIcon(dropUpProvider = { isDialogVisible }) + } + + if (isDialogVisible) { + DiaryDatePickerDialog( + localDate = state.localDate, + onConfirm = { coroutineScope.launch { state.animateScrollTo(it) } }, + onDismissRequest = { isDialogVisible = false }, + ) + } + }, + modifier = modifier, + actions = { + val coroutineScope = rememberCoroutineScope() + var today by remember { mutableStateOf(LocalDate.todayIn()) } + + IconButton(onClick = { coroutineScope.launch { state.animateScrollTo(LocalDate.todayIn()) } }) { + TodayIcon(text = today.dayOfMonth.toString()) + } + + LifecycleStartEffect(Unit) { + today = LocalDate.todayIn() + onStopOrDispose { } + } + }, + ) +} + +@Composable +private fun DropIcon( + dropUpProvider: () -> Boolean, + modifier: Modifier = Modifier, +) { + Crossfade( + targetState = dropUpProvider(), + modifier = modifier, + ) { isDropUp -> + if (isDropUp) { + DropUpIcon() + } else { + DropDownIcon() + } + } +} + +@Composable +private fun TodayIcon( + text: String, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.size(24.dp) + .border( + width = 1.dp, + color = LocalContentColor.current, + shape = RoundedCornerShape(size = 6.dp), + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + fontSize = with(LocalDensity.current) { 14.dp.toSp() }, + letterSpacing = with(LocalDensity.current) { 0.dp.toSp() }, + textAlign = TextAlign.Center, + lineHeight = with(LocalDensity.current) { 14.dp.toSp() }, + ) + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarDayOfMonthRow.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarDayOfMonthRow.kt new file mode 100644 index 00000000..da23cca3 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarDayOfMonthRow.kt @@ -0,0 +1,45 @@ +package io.github.taetae98coding.diary.core.calendar.compose.week + +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.calendar.compose.CalendarDefaults +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.calendar.compose.day.CalendarDayOfMonth +import io.github.taetae98coding.diary.core.calendar.compose.day.CalendarDayOfMonthState +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.library.datetime.toChristDayOfWeek +import kotlinx.datetime.LocalDate + +@Composable +internal fun CalendarDayOfMonthRow( + state: CalendarWeekState, + primaryDateListProvider: () -> List, + holidayListProvider: () -> List, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + Row(modifier = modifier) { + val dayOfMonthModifier = Modifier.weight(1F) + + repeat(7) { dayOfWeek -> + val dayOfMonthState = remember { + CalendarDayOfMonthState( + year = state.year, + month = state.month, + weekOfMonth = state.weekOfMonth, + dayOfWeek = dayOfWeek.toChristDayOfWeek(), + ) + } + + CalendarDayOfMonth( + state = dayOfMonthState, + primaryDateListProvider = primaryDateListProvider, + holidayListProvider = holidayListProvider, + modifier = dayOfMonthModifier, + colors = colors, + ) + } + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarItemVerticalGrid.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarItemVerticalGrid.kt new file mode 100644 index 00000000..b25987d6 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarItemVerticalGrid.kt @@ -0,0 +1,111 @@ +package io.github.taetae98coding.diary.core.calendar.compose.week + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.calendar.compose.CalendarDefaults +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.library.datetime.christ +import io.github.taetae98coding.diary.library.datetime.isOverlap + +@Composable +internal fun CalendarItemVerticalGrid( + state: CalendarWeekState, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + val items by remember { + derivedStateOf { + val itemList = buildList { + addAll(holidayListProvider()) + addAll(textItemListProvider()) + }.filter { + it.isOverlap(state.dateRange) + }.sorted().toMutableList() + + buildList { + while (itemList.isNotEmpty()) { + var dayOfWeek = 0 + + buildList { + while (true) { + val item = itemList.find { dayOfWeek <= it.startChrist(state) } ?: break + val start = item.startChrist(state) + val endInclusive = item.endInclusiveChrist(state) + + val spaceWeight = start - dayOfWeek + if (spaceWeight > 0) { + add(WeekItem.Space(spaceWeight.toFloat())) + } + + val weight = endInclusive - start + 1F + val weekTextItem = when (item) { + is CalendarItemUiState.Holiday -> { + WeekItem.Holiday( + key = item.key, + name = item.text, + weight = weight, + ) + } + + is CalendarItemUiState.Text -> { + WeekItem.Text( + key = item.key, + name = item.text, + weight = weight, + color = Color(item.color), + ) + } + } + + add(weekTextItem) + itemList.remove(item) + dayOfWeek = endInclusive + 1 + } + + if (dayOfWeek <= kotlinx.datetime.DayOfWeek.SATURDAY.christ) { + val weight = kotlinx.datetime.DayOfWeek.SATURDAY.christ - dayOfWeek + 1F + add(WeekItem.Space(weight)) + } + }.also { + add(it) + } + } + } + } + } + + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(2.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + items(items = items) { + WeekItemRow( + items = it, + onWeekItemClick = onCalendarItemClick, + colors = colors, + ) + } + } +} + +private fun CalendarItemUiState.startChrist(state: CalendarWeekState): Int { + return start.coerceAtLeast(state.dateRange.start).dayOfWeek.christ +} + +private fun CalendarItemUiState.endInclusiveChrist(state: CalendarWeekState): Int { + return endInclusive.coerceAtMost(state.dateRange.endInclusive).dayOfWeek.christ +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeek.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeek.kt new file mode 100644 index 00000000..8adcdbdf --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeek.kt @@ -0,0 +1,65 @@ +package io.github.taetae98coding.diary.core.calendar.compose.week + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.calendar.compose.CalendarDefaults +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.library.color.multiplyAlpha +import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil + +@Composable +internal fun CalendarWeek( + state: CalendarWeekState, + primaryDateListProvider: () -> List, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + val selectColor = colors.selectColor.takeOrElse { LocalContentColor.current.multiplyAlpha(0.2F) } + + Column( + modifier = modifier.drawBehind { + val dateRange = state.selectedDateRange ?: return@drawBehind + val startWeight = state.dateRange.start.daysUntil(dateRange.start) + val endWeight = dateRange.endInclusive.daysUntil(state.dateRange.endInclusive) + + drawRect( + color = selectColor, + topLeft = Offset(size.width / 7 * startWeight, 0F), + size = Size(size.width * (1F - (1F / 7F * (startWeight + endWeight))), size.height), + ) + }, + ) { + HorizontalDivider(thickness = 0.5.dp) + Spacer(modifier = Modifier.height(2.dp)) + CalendarDayOfMonthRow( + state = state, + primaryDateListProvider = primaryDateListProvider, + holidayListProvider = holidayListProvider, + colors = colors, + ) + CalendarItemVerticalGrid( + state = state, + textItemListProvider = textItemListProvider, + holidayListProvider = holidayListProvider, + onCalendarItemClick = onCalendarItemClick, + modifier = Modifier.fillMaxSize(), + colors = colors, + ) + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeekState.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeekState.kt new file mode 100644 index 00000000..e883e410 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/CalendarWeekState.kt @@ -0,0 +1,34 @@ +package io.github.taetae98coding.diary.core.calendar.compose.week + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import io.github.taetae98coding.diary.library.datetime.invoke +import io.github.taetae98coding.diary.library.datetime.isOverlap +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month + +internal class CalendarWeekState( + val year: Int, + val month: Month, + val weekOfMonth: Int, +) { + val dateRange = LocalDate(year, month, weekOfMonth, DayOfWeek.SUNDAY)..LocalDate(year, month, weekOfMonth, DayOfWeek.SATURDAY) + + var selectedDateRange: ClosedRange? by mutableStateOf(null) + private set + + fun drag(range: ClosedRange) { + if (!dateRange.isOverlap(range)) { + selectedDateRange = null + return + } + + selectedDateRange = range.start.coerceIn(dateRange)..range.endInclusive.coerceIn(dateRange) + } + + fun finishDrag() { + selectedDateRange = null + } +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItem.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItem.kt new file mode 100644 index 00000000..95bd0805 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItem.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.core.calendar.compose.week + +import androidx.compose.ui.graphics.Color + +internal sealed class WeekItem { + abstract val weight: Float + + data class Space(override val weight: Float) : WeekItem() + + data class Holiday( + val key: Any, + val name: String, + override val weight: Float, + ) : WeekItem() + + data class Text( + val key: Any, + val name: String, + val color: Color, + override val weight: Float, + ) : WeekItem() +} diff --git a/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItemRow.kt b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItemRow.kt new file mode 100644 index 00000000..03731a72 --- /dev/null +++ b/app/core/calendar-compose/src/commonMain/kotlin/io/github/taetae98coding/diary/core/calendar/compose/week/WeekItemRow.kt @@ -0,0 +1,96 @@ +@file:JvmName("WeekItemRowKt") + +package io.github.taetae98coding.diary.core.calendar.compose.week + +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.calendar.compose.CalendarDefaults +import io.github.taetae98coding.diary.core.calendar.compose.color.CalendarColors +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.library.color.toContrastColor +import kotlin.jvm.JvmName + +@Composable +internal fun WeekItemRow( + items: List, + onWeekItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + colors: CalendarColors = CalendarDefaults.colors(), +) { + Row(modifier = modifier.height(IntrinsicSize.Min)) { + items.forEach { + when (it) { + is WeekItem.Space -> { + Spacer(modifier = Modifier.weight(it.weight)) + } + + is WeekItem.Holiday -> { + key(it.key) { + WeekTextItem( + text = it.name, + color = colors.sundayColor, + modifier = Modifier.weight(it.weight) + .fillMaxHeight() + .padding(horizontal = 1.dp) + .clip(RoundedCornerShape(4.dp)), + ) + } + } + + is WeekItem.Text -> { + key(it.key) { + WeekTextItem( + text = it.name, + color = it.color, + modifier = Modifier.weight(it.weight) + .fillMaxHeight() + .padding(horizontal = 1.dp) + .clip(RoundedCornerShape(4.dp)) + .clickable { onWeekItemClick(it.key) }, + ) + } + } + } + } + } +} + +@Composable +private fun WeekTextItem( + text: String, + color: Color, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.background(color = color), + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + modifier = Modifier.basicMarquee(iterations = Int.MAX_VALUE) + .padding(2.dp), + color = color.toContrastColor(), + textAlign = TextAlign.Center, + maxLines = 1, + style = DiaryTheme.typography.labelMedium, + ) + } +} diff --git a/app/core/coroutines/README.md b/app/core/coroutines/README.md new file mode 100644 index 00000000..f6315217 --- /dev/null +++ b/app/core/coroutines/README.md @@ -0,0 +1,3 @@ +# :app:core:coroutines module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_coroutines.svg) diff --git a/app/core/coroutines/build.gradle.kts b/app/core/coroutines/build.gradle.kts new file mode 100644 index 00000000..41e5a40a --- /dev/null +++ b/app/core/coroutines/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.koin.all") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(libs.kotlinx.coroutines.core) + api(libs.lifecycle.common) + } + } + + androidMain { + dependencies { + implementation(libs.androidx.lifecycle.process) + } + } + + val nonAndroidMain = create("nonAndroidMain") { + dependencies { + implementation(libs.lifecycle.runtime) + } + } + + nonAndroidMain.dependsOn(commonMain.get()) + jvmMain.get().dependsOn(nonAndroidMain) + wasmJsMain.get().dependsOn(nonAndroidMain) + iosMain.get().dependsOn(nonAndroidMain) + } +} + +android { + namespace = "${Build.NAMESPACE}.core.coroutines" +} diff --git a/app/core/coroutines/src/androidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.android.kt b/app/core/coroutines/src/androidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.android.kt new file mode 100644 index 00000000..4079cb06 --- /dev/null +++ b/app/core/coroutines/src/androidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.android.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.core.coroutines + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner + +internal actual fun CoroutinesModule.getAppLifecycleOwner(): LifecycleOwner { + return ProcessLifecycleOwner.get() +} diff --git a/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt b/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt new file mode 100644 index 00000000..f34ab89e --- /dev/null +++ b/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModule.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.core.coroutines + +import androidx.lifecycle.LifecycleOwner +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton + +@Module +@ComponentScan +public class CoroutinesModule { + @Singleton + internal fun providesAppLifecycleOwner(): LifecycleOwner { + return getAppLifecycleOwner() + } +} diff --git a/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.kt b/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.kt new file mode 100644 index 00000000..1159eca1 --- /dev/null +++ b/app/core/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.core.coroutines + +import androidx.lifecycle.LifecycleOwner + +internal expect fun CoroutinesModule.getAppLifecycleOwner(): LifecycleOwner diff --git a/app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/AppLifecycleOwner.kt b/app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/AppLifecycleOwner.kt new file mode 100644 index 00000000..e27becc9 --- /dev/null +++ b/app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/AppLifecycleOwner.kt @@ -0,0 +1,36 @@ +package io.github.taetae98coding.diary.core.coroutines + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry + +public data object AppLifecycleOwner : LifecycleOwner { + private val registry = LifecycleRegistry(this) + + override val lifecycle: Lifecycle + get() = registry + + public fun create() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + } + + public fun start() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_START) + } + + public fun resume() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + public fun pause() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + } + + public fun stop() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } + + public fun destroy() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } +} diff --git a/app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.nonAndroid.kt b/app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.nonAndroid.kt new file mode 100644 index 00000000..6f564d46 --- /dev/null +++ b/app/core/coroutines/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/coroutines/CoroutinesModuleExt.nonAndroid.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.core.coroutines + +import androidx.lifecycle.LifecycleOwner + +internal actual fun CoroutinesModule.getAppLifecycleOwner(): LifecycleOwner { + return AppLifecycleOwner +} diff --git a/app/core/design-system/README.md b/app/core/design-system/README.md new file mode 100644 index 00000000..c4341c4c --- /dev/null +++ b/app/core/design-system/README.md @@ -0,0 +1,3 @@ +# :app:core:design-system module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_design_system.svg) diff --git a/app/core/design-system/build.gradle.kts b/app/core/design-system/build.gradle.kts new file mode 100644 index 00000000..62984101 --- /dev/null +++ b/app/core/design-system/build.gradle.kts @@ -0,0 +1,53 @@ +import org.jetbrains.compose.resources.ResourcesExtension.ResourceClassGeneration +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.compose") +} + +compose { + resources { + generateResClass = ResourceClassGeneration.Never + } +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:resources")) + implementation(project(":library:color")) + implementation(project(":library:datetime")) + + implementation(compose.material3) + implementation(libs.compose.markdown) + } + } + + androidMain { + dependencies { + implementation(compose.preview) + } + } + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + invokeWhenCreated("androidDebug") { + dependencies { + implementation(compose.uiTooling) + } + } + + val nonAndroidMain = create("nonAndroidMain") + + nonAndroidMain.dependsOn(commonMain.get()) + jvmMain.get().dependsOn(nonAndroidMain) + wasmJsMain.get().dependsOn(nonAndroidMain) + iosMain.get().dependsOn(nonAndroidMain) + } +} + +android { + namespace = "${Build.NAMESPACE}.core.design.system" +} diff --git a/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.android.kt b/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.android.kt new file mode 100644 index 00000000..45494ee8 --- /dev/null +++ b/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.android.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Composable +internal actual fun platformDarkColorScheme(): ColorScheme { + return dynamicDarkColorScheme(LocalContext.current) +} + +@Composable +internal actual fun platformLightColorScheme(): ColorScheme { + return dynamicLightColorScheme(LocalContext.current) +} diff --git a/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/DiaryComponentPreview.kt b/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/DiaryComponentPreview.kt new file mode 100644 index 00000000..885b16bb --- /dev/null +++ b/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/DiaryComponentPreview.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.design.system.diary + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.design.system.diary.component.DiaryComponent +import io.github.taetae98coding.diary.core.design.system.diary.component.rememberDiaryComponentState +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@Composable +@DiaryPreview +private fun PreviewDiaryComponent() { + DiaryTheme { + DiaryComponent( + state = rememberDiaryComponentState(), + ) + } +} diff --git a/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/preview/DiaryPreview.kt b/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/preview/DiaryPreview.kt new file mode 100644 index 00000000..8d45bcd5 --- /dev/null +++ b/app/core/design-system/src/androidMain/kotlin/io/github/taetae98coding/diary/core/design/system/preview/DiaryPreview.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.design.system.preview + +import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Preview + +@Preview(name = "LightMode", showSystemUi = false, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL) +@Preview(name = "NightMode", showSystemUi = false, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Preview(name = "Landscape", device = "spec:parent=pixel_5,orientation=landscape") +@Preview(name = "NavigationButton", device = "spec:parent=pixel_5,navigation=buttons") +@Preview(name = "FontScale", fontScale = 2.0F) +@Preview(name = "RTL", locale = "ar") +public annotation class DiaryPreview diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/chip/DiaryAssistChip.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/chip/DiaryAssistChip.kt new file mode 100644 index 00000000..cc39890a --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/chip/DiaryAssistChip.kt @@ -0,0 +1,46 @@ +package io.github.taetae98coding.diary.core.design.system.chip + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.ChipColors +import androidx.compose.material3.ChipElevation +import androidx.compose.material3.LocalMinimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp + +@Composable +public fun DiaryAssistChip( + onClick: () -> Unit, + label: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + shape: Shape = AssistChipDefaults.shape, + colors: ChipColors = AssistChipDefaults.assistChipColors(), + elevation: ChipElevation? = AssistChipDefaults.assistChipElevation(), + border: BorderStroke? = AssistChipDefaults.assistChipBorder(enabled), + interactionSource: MutableInteractionSource? = null, +) { + CompositionLocalProvider( + LocalMinimumInteractiveComponentSize provides Dp.Unspecified, + ) { + AssistChip( + onClick = onClick, + label = label, + modifier = modifier, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + shape = shape, + colors = colors, + elevation = elevation, + border = border, + interactionSource = interactionSource, + ) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColor.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColor.kt new file mode 100644 index 00000000..6442e027 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColor.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.ui.graphics.Color + +public data class DiaryColor( + val primary: Color, + val onPrimary: Color, + val background: Color, + val onSurface: Color, +) diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPicker.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPicker.kt new file mode 100644 index 00000000..1b76ea71 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPicker.kt @@ -0,0 +1,114 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.icon.RefreshIcon +import io.github.taetae98coding.diary.library.color.toContrastColor +import io.github.taetae98coding.diary.library.color.toRgbString + +@Composable +public fun DiaryColorPicker( + state: DiaryColorPickerState, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + val sliderModifier = Modifier.padding(horizontal = DiaryTheme.dimen.diaryHorizontalPadding) + + ColorBox( + state = state, + modifier = Modifier.fillMaxWidth() + .height(200.dp), + ) + + Spacer(modifier = Modifier.height(DiaryTheme.dimen.diaryVerticalPadding)) + + ColorSlider( + color = Color.Red, + valueProvider = { state.red }, + onValueChange = state::onRedChange, + modifier = sliderModifier, + ) + + ColorSlider( + color = Color.Green, + valueProvider = { state.green }, + onValueChange = state::onGreenChange, + modifier = sliderModifier, + ) + + ColorSlider( + color = Color.Blue, + valueProvider = { state.blue }, + onValueChange = state::onBlueChange, + modifier = sliderModifier, + ) + } +} + +@Composable +private fun ColorBox( + state: DiaryColorPickerState, + modifier: Modifier = Modifier, +) { + val animateColor by animateColorAsState(state.color) + + Box( + modifier = modifier.drawBehind { + drawRect(color = animateColor) + }, + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "#${state.color.toArgb().toRgbString()}", + color = state.color.toContrastColor(), + ) + + IconButton( + modifier = Modifier.align(Alignment.TopEnd), + onClick = state::refresh, + colors = IconButtonDefaults.iconButtonColors(contentColor = state.color.toContrastColor()), + ) { + RefreshIcon() + } + } +} + +@Composable +private fun ColorSlider( + color: Color, + valueProvider: () -> Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, +) { + val animateValue by animateFloatAsState(valueProvider()) + + Slider( + modifier = modifier, + value = animateValue, + onValueChange = onValueChange, + colors = SliderDefaults.colors( + thumbColor = color, + activeTrackColor = color, + ), + ) +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerDialog.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerDialog.kt new file mode 100644 index 00000000..a896449a --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerDialog.kt @@ -0,0 +1,64 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.dismiss +import io.github.taetae98coding.diary.core.resources.select +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun DiaryColorPickerDialog( + initialColor: Color, + onDismissRequest: () -> Unit, + onConfirm: (Color) -> Unit, + modifier: Modifier = Modifier, +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier, + ) { + Card( + colors = CardDefaults.cardColors( + containerColor = AlertDialogDefaults.containerColor, + ), + ) { + val state = rememberDiaryColorPickerState(initialColor = initialColor) + + DiaryColorPicker(state = state) + + Row( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 6.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End), + ) { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(Res.string.dismiss)) + } + TextButton( + onClick = { + onConfirm(state.color) + onDismissRequest() + }, + ) { + Text(text = stringResource(Res.string.select)) + } + } + } + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerState.kt new file mode 100644 index 00000000..0c13cbdf --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/DiaryColorPickerState.kt @@ -0,0 +1,49 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import io.github.taetae98coding.diary.library.color.randomArgb + +public class DiaryColorPickerState internal constructor( + initialColor: Color, +) { + public var color: Color by mutableStateOf(initialColor) + private set + + internal val red by derivedStateOf { this.color.red } + internal val green by derivedStateOf { this.color.green } + internal val blue by derivedStateOf { this.color.blue } + + internal fun refresh() { + color = Color(randomArgb()) + } + + internal fun onRedChange(value: Float) { + color = color.copy(red = value) + } + + internal fun onGreenChange(value: Float) { + color = color.copy(green = value) + } + + internal fun onBlueChange(value: Float) { + color = color.copy(blue = value) + } + + public companion object { + internal fun saver(): Saver { + return listSaver( + save = { listOf(it.color.toArgb()) }, + restore = { + DiaryColorPickerState(initialColor = Color(it[0])) + }, + ) + } + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.kt new file mode 100644 index 00000000..62097f2d --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +@Composable +internal expect fun platformDarkColorScheme(): ColorScheme + +@Composable +internal expect fun platformLightColorScheme(): ColorScheme diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/RememberDiaryColorPickerState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/RememberDiaryColorPickerState.kt new file mode 100644 index 00000000..342afcbf --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/RememberDiaryColorPickerState.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.graphics.Color + + +@Composable +public fun rememberDiaryColorPickerState( + initialColor: Color, +): DiaryColorPickerState { + return rememberSaveable(saver = DiaryColorPickerState.saver()) { + DiaryColorPickerState(initialColor = initialColor) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/date/DiaryDatePickerDialog.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/date/DiaryDatePickerDialog.kt new file mode 100644 index 00000000..dd26d2a8 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/date/DiaryDatePickerDialog.kt @@ -0,0 +1,58 @@ +package io.github.taetae98coding.diary.core.design.system.date + +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.dismiss +import io.github.taetae98coding.diary.core.resources.select +import io.github.taetae98coding.diary.library.datetime.toLocalDate +import io.github.taetae98coding.diary.library.datetime.toTimeInMillis +import kotlinx.datetime.LocalDate +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +public fun DiaryDatePickerDialog( + localDate: LocalDate?, + onConfirm: (LocalDate) -> Unit, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + val state = rememberDatePickerState(initialSelectedDateMillis = localDate?.toTimeInMillis()) + + DatePickerDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + val isConfirmButtonEnable by remember { derivedStateOf { state.selectedDateMillis != null } } + + TextButton( + onClick = { + state.selectedDateMillis?.toLocalDate()?.let { date -> + onConfirm(date) + onDismissRequest() + } + }, + enabled = isConfirmButtonEnable, + ) { + Text(text = stringResource(Res.string.select)) + } + }, + modifier = modifier, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(Res.string.dismiss)) + } + }, + ) { + DatePicker(state = state) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColor.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColor.kt new file mode 100644 index 00000000..3367cde8 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColor.kt @@ -0,0 +1,52 @@ +package io.github.taetae98coding.diary.core.design.system.diary.color + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import io.github.taetae98coding.diary.core.design.system.color.DiaryColorPickerDialog +import io.github.taetae98coding.diary.library.color.toContrastColor +import io.github.taetae98coding.diary.library.color.toRgbString + +@Composable +public fun DiaryColor( + state: DiaryColorState, + modifier: Modifier = Modifier, +) { + val animateColor by animateColorAsState(state.color) + var isDialogVisible by rememberSaveable { mutableStateOf(false) } + + Card( + onClick = { isDialogVisible = true }, + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = animateColor, + contentColor = state.color.toContrastColor(), + ), + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text(text = "#${state.color.toArgb().toRgbString()}") + } + } + + if (isDialogVisible) { + DiaryColorPickerDialog( + initialColor = state.color, + onDismissRequest = { isDialogVisible = false }, + onConfirm = { state.onColorChange(it) }, + ) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColorState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColorState.kt new file mode 100644 index 00000000..16e297aa --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/DiaryColorState.kt @@ -0,0 +1,31 @@ +package io.github.taetae98coding.diary.core.design.system.diary.color + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb + +public class DiaryColorState( + initialColor: Color, +) { + public var color: Color by mutableStateOf(initialColor) + private set + + internal fun onColorChange(value: Color) { + color = value + } + + public companion object { + internal fun saver(): Saver { + return listSaver( + save = { listOf(it.color.toArgb()) }, + restore = { + DiaryColorState(initialColor = Color(it[0])) + }, + ) + } + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/RememberDiaryColorState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/RememberDiaryColorState.kt new file mode 100644 index 00000000..ef905b5c --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/color/RememberDiaryColorState.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.core.design.system.diary.color + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse +import io.github.taetae98coding.diary.library.color.randomArgb + +@Composable +public fun rememberDiaryColorState( + vararg inputs: Any?, + initialColor: Color = Color.Unspecified, +): DiaryColorState { + return rememberSaveable( + inputs = inputs, + saver = DiaryColorState.saver(), + ) { + DiaryColorState(initialColor = initialColor.takeOrElse { Color(randomArgb()) }) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponent.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponent.kt new file mode 100644 index 00000000..6684fee6 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponent.kt @@ -0,0 +1,114 @@ +package io.github.taetae98coding.diary.core.design.system.diary.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.unit.dp +import com.mikepenz.markdown.m3.Markdown +import io.github.taetae98coding.diary.core.design.system.text.ClearTextField +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.description +import io.github.taetae98coding.diary.core.resources.icon.MarkdownIcon +import io.github.taetae98coding.diary.core.resources.icon.TextFieldIcon +import io.github.taetae98coding.diary.core.resources.title +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource + +@Composable +public fun DiaryComponent( + state: DiaryComponentState, + modifier: Modifier = Modifier, +) { + Card(modifier = modifier) { + ClearTextField( + valueProvider = { state.title }, + onValueChange = state::onTitleChange, + modifier = Modifier.fillMaxWidth() + .focusRequester(state.titleFocusRequester), + label = { Text(text = stringResource(Res.string.title)) }, + errorProvider = { state.isTitleError }, + singleLine = true + ) + DescriptionTabRow(state = state) + DescriptionPager( + state = state, + modifier = Modifier.height(200.dp), + ) + } +} + +@Composable +private fun DescriptionTabRow( + state: DiaryComponentState, + modifier: Modifier = Modifier, +) { + TabRow( + selectedTabIndex = state.pagerState.currentPage, + modifier = modifier, + ) { + val coroutineScope = rememberCoroutineScope() + + repeat(2) { page -> + Tab( + selected = state.pagerState.currentPage == page, + onClick = { coroutineScope.launch { state.pagerState.animateScrollToPage(page) } }, + icon = { + when (page) { + 0 -> TextFieldIcon() + 1 -> MarkdownIcon() + } + }, + ) + } + } +} + +@Composable +private fun DescriptionPager( + state: DiaryComponentState, + modifier: Modifier = Modifier, +) { + HorizontalPager( + state = state.pagerState, + modifier = modifier, + key = { it }, + ) { page -> + when (page) { + 0 -> { + ClearTextField( + valueProvider = { state.description }, + onValueChange = state::onDescriptionChange, + modifier = Modifier.fillMaxSize(), + label = { Text(text = stringResource(Res.string.description)) }, + ) + } + + 1 -> { + Column( + modifier = Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(DiaryTheme.dimen.diaryPaddingValues), + ) { + Markdown( + content = state.description, + modifier = Modifier.fillMaxSize(), + ) + } + } + } + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponentState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponentState.kt new file mode 100644 index 00000000..ab19a720 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/DiaryComponentState.kt @@ -0,0 +1,71 @@ +package io.github.taetae98coding.diary.core.design.system.diary.component + +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.setValue +import androidx.compose.ui.focus.FocusRequester + +public class DiaryComponentState internal constructor( + initialTitle: String, + initialDescription: String, +) { + internal var isTitleError by mutableStateOf(false) + private set + public var title: String by mutableStateOf(initialTitle) + private set + public var description: String by mutableStateOf(initialDescription) + private set + + internal val pagerState = PagerState( + currentPage = if (initialDescription.isBlank()) { + 0 + } else { + 1 + }, + ) { + 2 + } + internal val titleFocusRequester = FocusRequester() + + internal fun onTitleChange(value: String) { + title = value + if (value.isNotBlank()) { + isTitleError = false + } + } + + internal fun onDescriptionChange(value: String) { + description = value + } + + public fun requestTitleFocus() { + titleFocusRequester.requestFocus() + } + + public fun titleError() { + isTitleError = true + } + + public fun clearInput() { + isTitleError = false + title = "" + description = "" + } + + public companion object { + internal fun saver(): Saver { + return listSaver( + save = { listOf(it.title, it.description) }, + restore = { + DiaryComponentState( + initialTitle = it[0], + initialDescription = it[1], + ) + }, + ) + } + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/RememberDiaryComponentState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/RememberDiaryComponentState.kt new file mode 100644 index 00000000..1a07068e --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/component/RememberDiaryComponentState.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.core.design.system.diary.component + +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable + +@Composable +public fun rememberDiaryComponentState( + vararg inputs: Any?, + initialTitle: String = "", + initialDescription: String = "", +): DiaryComponentState { + return rememberSaveable( + inputs = inputs, + saver = DiaryComponentState.saver(), + ) { + DiaryComponentState( + initialTitle = initialTitle, + initialDescription = initialDescription, + ) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDate.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDate.kt new file mode 100644 index 00000000..588eabe7 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDate.kt @@ -0,0 +1,146 @@ +package io.github.taetae98coding.diary.core.design.system.diary.date + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.Card +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import io.github.taetae98coding.diary.core.design.system.date.DiaryDatePickerDialog +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.date +import io.github.taetae98coding.diary.core.resources.month_and_day +import kotlinx.datetime.LocalDate +import org.jetbrains.compose.resources.stringResource + +@Composable +public fun DiaryDate( + state: DiaryDateState, + modifier: Modifier = Modifier, +) { + Card(modifier = modifier) { + val itemModifier = Modifier.fillMaxWidth() + + Title( + state = state, + modifier = itemModifier.padding(DiaryTheme.dimen.diaryPaddingValues), + ) + AnimatedVisibility( + visible = state.hasDate, + modifier = itemModifier, + ) { + Date(state = state) + } + } +} + +@Composable +private fun Title( + state: DiaryDateState, + modifier: Modifier = Modifier, +) { + Row( + modifier = Modifier.toggleable(value = state.hasDate, onValueChange = state::onHasDateChange) + .then(modifier), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = stringResource(Res.string.date)) + Switch( + checked = state.hasDate, + onCheckedChange = null, + ) + } +} + +@Composable +private fun Date( + state: DiaryDateState, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + verticalAlignment = Alignment.CenterVertically, + ) { + DateButton( + dateProvider = { state.start }, + onSelectDate = state::onStartChange, + modifier = Modifier.weight(1F), + ) + Text(text = " ~ ") + DateButton( + dateProvider = { state.endInclusive }, + onSelectDate = state::onEndInclusiveChange, + modifier = Modifier.weight(1F), + ) + } +} + +@Composable +private fun DateButton( + dateProvider: () -> LocalDate, + onSelectDate: (LocalDate) -> Unit, + modifier: Modifier = Modifier, +) { + var isPickerVisible by rememberSaveable { mutableStateOf(false) } + + TextButton( + onClick = { isPickerVisible = true }, + modifier = modifier, + ) { + AnimatedContent( + targetState = dateProvider(), + transitionSpec = { + if (targetState > initialState) { + (slideInVertically { it } + fadeIn()) togetherWith (slideOutVertically { -it } + fadeOut()) + } else { + (slideInVertically { -it } + fadeIn()) togetherWith (slideOutVertically { it } + fadeOut()) + } + }, + ) { target -> + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = target.year.toString(), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = DiaryTheme.typography.labelSmall, + ) + Text( + text = stringResource(Res.string.month_and_day, target.monthNumber, target.dayOfMonth), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = DiaryTheme.typography.labelLarge, + ) + } + } + } + + if (isPickerVisible) { + DiaryDatePickerDialog( + localDate = dateProvider(), + onConfirm = onSelectDate, + onDismissRequest = { isPickerVisible = false }, + ) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDateState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDateState.kt new file mode 100644 index 00000000..d0b0f64e --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/DiaryDateState.kt @@ -0,0 +1,55 @@ +package io.github.taetae98coding.diary.core.design.system.diary.date + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.setValue +import io.github.taetae98coding.diary.library.datetime.todayIn +import kotlinx.datetime.LocalDate + +public class DiaryDateState internal constructor( + initialStart: LocalDate?, + initialEndInclusive: LocalDate?, +) : ClosedRange { + public var hasDate: Boolean by mutableStateOf(initialStart != null && initialEndInclusive != null) + private set + override var start: LocalDate by mutableStateOf(initialStart ?: LocalDate.todayIn()) + private set + override var endInclusive: LocalDate by mutableStateOf(initialEndInclusive ?: LocalDate.todayIn()) + private set + + internal fun onHasDateChange(value: Boolean) { + hasDate = value + } + + internal fun onStartChange(value: LocalDate) { + start = value + if (endInclusive < value) { + endInclusive = value + } + } + + internal fun onEndInclusiveChange(value: LocalDate) { + endInclusive = value + if (start > value) { + start = value + } + } + + public companion object { + internal fun saver(): Saver { + return listSaver( + save = { listOf(it.hasDate, it.start.toEpochDays(), it.endInclusive.toEpochDays()) }, + restore = { + DiaryDateState( + initialStart = LocalDate.fromEpochDays(it[1] as Int), + initialEndInclusive = LocalDate.fromEpochDays(it[2] as Int), + ).apply { + hasDate = it[0] as Boolean + } + }, + ) + } + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/RememberDiaryDateState.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/RememberDiaryDateState.kt new file mode 100644 index 00000000..a4dedec2 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/diary/date/RememberDiaryDateState.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.core.design.system.diary.date + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable +import kotlinx.datetime.LocalDate + +@Composable +public fun rememberDiaryDateState( + vararg inputs: Any?, + initialStart: LocalDate?, + initialEndInclusive: LocalDate?, +): DiaryDateState { + return rememberSaveable( + inputs = inputs, + saver = DiaryDateState.saver(), + ) { + DiaryDateState( + initialStart = initialStart, + initialEndInclusive = initialEndInclusive, + ) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/dimen/DiaryDimen.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/dimen/DiaryDimen.kt new file mode 100644 index 00000000..abb385ea --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/dimen/DiaryDimen.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.core.design.system.dimen + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +public data class DiaryDimen( + val screenHorizontalPadding: Dp = 12.dp, + val screenVerticalPadding: Dp = 12.dp, + val diaryHorizontalPadding: Dp = 16.dp, + val diaryVerticalPadding: Dp = 8.dp, + val itemSpace: Dp = 4.dp, +) { + val screenPaddingValues: PaddingValues = PaddingValues( + horizontal = screenHorizontalPadding, + vertical = screenVerticalPadding, + ) + + val diaryPaddingValues: PaddingValues = PaddingValues( + horizontal = diaryHorizontalPadding, + vertical = diaryVerticalPadding, + ) +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/emoji/Emoji.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/emoji/Emoji.kt new file mode 100644 index 00000000..48c1009c --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/emoji/Emoji.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.design.system.emoji + +public data object Emoji { + public val congratulate: List = listOf("🥳", "🎉", "🎊") + public val error: List = listOf("😵‍💫", "🫠") +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/text/ClearTextField.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/text/ClearTextField.kt new file mode 100644 index 00000000..8a7acacb --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/text/ClearTextField.kt @@ -0,0 +1,64 @@ +package io.github.taetae98coding.diary.core.design.system.text + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.IconButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.VisualTransformation +import io.github.taetae98coding.diary.core.resources.icon.ClearIcon + +@Composable +@NonRestartableComposable +public fun ClearTextField( + valueProvider: () -> String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + errorProvider: () -> Boolean = { false }, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, +) { + TextField( + value = valueProvider(), + onValueChange = onValueChange, + modifier = modifier, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = { + Row { + if (trailingIcon != null) { + trailingIcon() + } + + if (valueProvider().isNotEmpty()) { + IconButton(onClick = { onValueChange("") }) { + ClearIcon() + } + } + } + }, + isError = errorProvider(), + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + ), + ) +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt new file mode 100644 index 00000000..7f14c739 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/CompositionLocalExt.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.design.system.theme + +import androidx.compose.runtime.staticCompositionLocalOf +import io.github.taetae98coding.diary.core.design.system.color.DiaryColor +import io.github.taetae98coding.diary.core.design.system.dimen.DiaryDimen +import io.github.taetae98coding.diary.core.design.system.typography.DiaryTypography + +private const val Message = "DiaryTheme not found." + +internal val LocalDiaryColor = staticCompositionLocalOf { error(Message) } +internal val LocalDiaryTypography = staticCompositionLocalOf { error(Message) } +internal val LocalDiaryDimen = staticCompositionLocalOf { error(Message) } diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt new file mode 100644 index 00000000..a6d2bfd4 --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/theme/DiaryTheme.kt @@ -0,0 +1,57 @@ +package io.github.taetae98coding.diary.core.design.system.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import io.github.taetae98coding.diary.core.design.system.color.DiaryColor +import io.github.taetae98coding.diary.core.design.system.color.platformDarkColorScheme +import io.github.taetae98coding.diary.core.design.system.color.platformLightColorScheme +import io.github.taetae98coding.diary.core.design.system.dimen.DiaryDimen +import io.github.taetae98coding.diary.core.design.system.typography.DiaryTypography + +public data object DiaryTheme { + val color: DiaryColor + @Composable + get() = LocalDiaryColor.current + + val typography: DiaryTypography + @Composable + get() = LocalDiaryTypography.current + + val dimen: DiaryDimen + @Composable + get() = LocalDiaryDimen.current +} + +@Composable +public fun DiaryTheme( + content: @Composable () -> Unit, +) { + val colorScheme = if (isSystemInDarkTheme()) { + platformDarkColorScheme() + } else { + platformLightColorScheme() + } + + CompositionLocalProvider( + LocalDiaryColor provides DiaryColor( + primary = colorScheme.primary, + onPrimary = colorScheme.onPrimary, + background = colorScheme.background, + onSurface = colorScheme.onSurface, + ), + LocalDiaryTypography provides DiaryTypography( + labelSmall = MaterialTheme.typography.labelSmall, + labelMedium = MaterialTheme.typography.labelMedium, + labelLarge = MaterialTheme.typography.labelLarge, + bodySmall = MaterialTheme.typography.bodySmall, + ), + LocalDiaryDimen provides DiaryDimen(), + ) { + MaterialTheme( + colorScheme = colorScheme, + content = content, + ) + } +} diff --git a/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt new file mode 100644 index 00000000..6b426e8c --- /dev/null +++ b/app/core/design-system/src/commonMain/kotlin/io/github/taetae98coding/diary/core/design/system/typography/DiaryTypography.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.design.system.typography + +import androidx.compose.ui.text.TextStyle + +public data class DiaryTypography( + val labelSmall: TextStyle, + val labelMedium: TextStyle, + val labelLarge: TextStyle, + val bodySmall: TextStyle, +) diff --git a/app/core/design-system/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.nonAndroid.kt b/app/core/design-system/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.nonAndroid.kt new file mode 100644 index 00000000..93f2d5a8 --- /dev/null +++ b/app/core/design-system/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/core/design/system/color/PlatformColorScheme.nonAndroid.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.core.design.system.color + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +@Composable +internal actual fun platformDarkColorScheme(): ColorScheme { + return darkColorScheme() +} + +@Composable +internal actual fun platformLightColorScheme(): ColorScheme { + return lightColorScheme() +} diff --git a/app/core/diary-database-memory/README.md b/app/core/diary-database-memory/README.md new file mode 100644 index 00000000..bc6d136a --- /dev/null +++ b/app/core/diary-database-memory/README.md @@ -0,0 +1,3 @@ +# :app:core:diary-database-memory module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_diary_database_memory.svg) diff --git a/app/core/diary-database-memory/build.gradle.kts b/app/core/diary-database-memory/build.gradle.kts new file mode 100644 index 00000000..7e0160a7 --- /dev/null +++ b/app/core/diary-database-memory/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.koin.all") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:diary-database")) + implementation(project(":library:coroutines")) + implementation(project(":library:datetime")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.diary.database.memory" +} diff --git a/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/DiaryMemoryDatabaseModule.kt b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/DiaryMemoryDatabaseModule.kt new file mode 100644 index 00000000..5e832edf --- /dev/null +++ b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/DiaryMemoryDatabaseModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.core.diary.database.memory + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class DiaryMemoryDatabaseModule diff --git a/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt new file mode 100644 index 00000000..2ec338c9 --- /dev/null +++ b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoBackupMemoryDao.kt @@ -0,0 +1,79 @@ +package io.github.taetae98coding.diary.core.diary.database.memory + +import io.github.taetae98coding.diary.core.diary.database.MemoBackupDao +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.update +import org.koin.core.annotation.Singleton + +@OptIn(ExperimentalCoroutinesApi::class) +@Singleton +internal class MemoBackupMemoryDao( + private val memoMemoryDao: MemoMemoryDao, +) : MemoBackupDao { + private val flow = MutableStateFlow>>(emptyMap()) + private val updateFlow = mutableMapOf>() + + override suspend fun upsert(uid: String, memoId: String) { + val set = buildSet { + flow.value[uid]?.let { addAll(it) } + add(memoId) + } + val map = buildMap { + putAll(flow.value) + put(uid, set) + } + + flow.emit(map) + getInternalUpdateFlow(uid).update { it + 1 } + } + + override suspend fun delete(uid: String, memoId: String) { + val set = buildSet { + flow.value[uid]?.let { addAll(it) } + remove(memoId) + } + val map = buildMap { + putAll(flow.value) + put(uid, set) + } + + flow.emit(map) + } + + override suspend fun deleteByMemoIds(memoIds: List) { + val idSet = memoIds.toSet() + val map = buildMap { + flow.value.forEach { entry -> + val set = entry.value.filterNot { it in idSet } + .toSet() + + put(entry.key, set) + } + } + + flow.emit(map) + } + + override fun getUpdateFlow(uid: String): Flow { + return getInternalUpdateFlow(uid).asStateFlow() + } + + override fun countByUid(uid: String): Flow { + return flow.mapLatest { it[uid]?.size ?: 0 } + } + + override fun findByUid(uid: String): Flow> { + return memoMemoryDao.flow.mapLatest { map -> + map.values.filter { it.owner == uid } + } + } + + private fun getInternalUpdateFlow(uid: String): MutableStateFlow { + return updateFlow.getOrPut(uid) { MutableStateFlow(0) } + } +} diff --git a/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoMemoryDao.kt b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoMemoryDao.kt new file mode 100644 index 00000000..36f1e230 --- /dev/null +++ b/app/core/diary-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/memory/MemoMemoryDao.kt @@ -0,0 +1,103 @@ +package io.github.taetae98coding.diary.core.diary.database.memory + +import io.github.taetae98coding.diary.core.diary.database.MemoDao +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.github.taetae98coding.diary.library.coroutines.filterCollectionLatest +import io.github.taetae98coding.diary.library.datetime.isOverlap +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import org.koin.core.annotation.Singleton + +@OptIn(ExperimentalCoroutinesApi::class) +@Singleton +internal class MemoMemoryDao( + private val clock: Clock, +) : MemoDao { + val flow = MutableStateFlow>(emptyMap()) + + override suspend fun upsert(memo: MemoDto) { + val map = buildMap { + putAll(flow.value) + put(memo.id, memo) + } + + flow.emit(map) + } + + override suspend fun upsert(memoList: List) { + val map = buildMap { + putAll(flow.value) + memoList.forEach { put(it.id, it) } + } + + flow.emit(map) + } + + override suspend fun update(memoId: String, detail: MemoDetail) { + val dto = flow.value[memoId]?.copy( + detail = detail, + updateAt = clock.now(), + ) ?: return + val map = buildMap { + putAll(flow.value) + put(memoId, dto) + } + + flow.emit(map) + } + + override suspend fun updateFinish(memoId: String, isFinish: Boolean) { + val dto = flow.value[memoId]?.copy( + isFinish = isFinish, + updateAt = clock.now(), + ) ?: return + val map = buildMap { + putAll(flow.value) + put(memoId, dto) + } + + flow.emit(map) + } + + override suspend fun updateDelete(memoId: String, isDelete: Boolean) { + val dto = flow.value[memoId]?.copy( + isDelete = isDelete, + updateAt = clock.now(), + ) ?: return + val map = buildMap { + putAll(flow.value) + put(memoId, dto) + } + + flow.emit(map) + } + + override fun find(memoId: String): Flow { + return flow.mapLatest { it[memoId] } + } + + override fun findByDateRange(owner: String?, dateRange: ClosedRange): Flow> { + return flow.mapLatest { it.values } + .filterCollectionLatest { it.owner == owner } + .filterCollectionLatest { !it.isDelete } + .filterCollectionLatest { + val start = it.detail.start ?: return@filterCollectionLatest false + val endInclusive = it.detail.endInclusive ?: return@filterCollectionLatest false + + dateRange.isOverlap(start..endInclusive) + } + } + + override fun getLastServerUpdateAt(owner: String?): Flow { + return flow.mapLatest { it.values } + .filterCollectionLatest { it.owner == owner } + .filterCollectionLatest { it.serverUpdateAt != null } + .mapLatest { list -> list.maxOfOrNull { requireNotNull(it.serverUpdateAt) } } + } +} diff --git a/app/core/diary-database-room/README.md b/app/core/diary-database-room/README.md new file mode 100644 index 00000000..54ad434b --- /dev/null +++ b/app/core/diary-database-room/README.md @@ -0,0 +1,3 @@ +# :app:core:diary-database-room module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_diary_database_room.svg) diff --git a/app/core/diary-database-room/build.gradle.kts b/app/core/diary-database-room/build.gradle.kts new file mode 100644 index 00000000..416144df --- /dev/null +++ b/app/core/diary-database-room/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("diary.room") + id("diary.koin.room") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:diary-database")) + implementation(project(":library:coroutines")) + implementation(project(":library:room")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.diary.database.room" +} diff --git a/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/1.json b/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/1.json new file mode 100644 index 00000000..08cb17b1 --- /dev/null +++ b/app/core/diary-database-room/schemas/io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase/1.json @@ -0,0 +1,136 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "46ec457c31953f6d5914e1c314d0f39e", + "entities": [ + { + "tableName": "MemoEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL DEFAULT '', `title` TEXT NOT NULL DEFAULT '', `description` TEXT NOT NULL DEFAULT '', `start` TEXT DEFAULT null, `endInclusive` TEXT DEFAULT null, `color` INTEGER NOT NULL DEFAULT -16777216, `isFinish` INTEGER NOT NULL DEFAULT 0, `isDelete` INTEGER NOT NULL DEFAULT 0, `owner` TEXT DEFAULT null, `updateAt` INTEGER NOT NULL DEFAULT 0, `serverUpdateAt` INTEGER DEFAULT null, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "endInclusive", + "columnName": "endInclusive", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-16777216" + }, + { + "fieldPath": "isFinish", + "columnName": "isFinish", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isDelete", + "columnName": "isDelete", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "owner", + "columnName": "owner", + "affinity": "TEXT", + "defaultValue": "null" + }, + { + "fieldPath": "updateAt", + "columnName": "updateAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "serverUpdateAt", + "columnName": "serverUpdateAt", + "affinity": "INTEGER", + "defaultValue": "null" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "MemoBackupEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` TEXT NOT NULL, `memoId` TEXT NOT NULL, PRIMARY KEY(`uid`, `memoId`), FOREIGN KEY(`memoId`) REFERENCES `MemoEntity`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "memoId", + "columnName": "memoId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uid", + "memoId" + ] + }, + "foreignKeys": [ + { + "table": "MemoEntity", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "memoId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '46ec457c31953f6d5914e1c314d0f39e')" + ] + } +} \ No newline at end of file diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt new file mode 100644 index 00000000..29441afc --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryDatabase.kt @@ -0,0 +1,30 @@ +package io.github.taetae98coding.diary.core.diary.database.room + +import androidx.room.ConstructedBy +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import io.github.taetae98coding.diary.core.diary.database.room.dao.MemoBackupEntityDao +import io.github.taetae98coding.diary.core.diary.database.room.dao.MemoEntityDao +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoBackupEntity +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import io.github.taetae98coding.diary.core.diary.database.room.internal.DiaryDatabaseConstructor +import io.github.taetae98coding.diary.library.room.InstantConverter +import io.github.taetae98coding.diary.library.room.LocalDataConverter + +@Database( + entities = [ + MemoEntity::class, + MemoBackupEntity::class, + ], + version = 1 +) +@ConstructedBy(DiaryDatabaseConstructor::class) +@TypeConverters( + LocalDataConverter::class, + InstantConverter::class, +) +internal abstract class DiaryDatabase : RoomDatabase() { + abstract fun memo(): MemoEntityDao + abstract fun memoBackup(): MemoBackupEntityDao +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryRoomDatabaseModule.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryRoomDatabaseModule.kt new file mode 100644 index 00000000..e719edb8 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/DiaryRoomDatabaseModule.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.diary.database.room + +import io.github.taetae98coding.diary.library.koin.room.getDatabaseBuilder +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton +import org.koin.core.component.KoinComponent + +@Module +@ComponentScan +public class DiaryRoomDatabaseModule : KoinComponent { + @Singleton + internal fun providesDiaryDatabase(): DiaryDatabase { + return getDatabaseBuilder("diary.db") + .build() + } +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/EntityDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/EntityDao.kt new file mode 100644 index 00000000..97be1391 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/EntityDao.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Transaction +import androidx.room.Upsert + +@Dao +internal abstract class EntityDao { + @Upsert + abstract suspend fun upsert(entity: T) + + @Transaction + @Upsert + abstract suspend fun upsert(entityList: List) + + @Delete + abstract suspend fun delete(entity: T) +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupEntityDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupEntityDao.kt new file mode 100644 index 00000000..cfd1dcf2 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupEntityDao.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoBackupEntity +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import kotlinx.coroutines.flow.Flow + +@Dao +internal abstract class MemoBackupEntityDao : EntityDao() { + @Query("DELETE FROM MemoBackupEntity WHERE memoId IN (:memoIds)") + abstract suspend fun deleteByMemoIds(memoIds: List) + + @Query("SELECT COUNT(memoId) FROM MemoBackupEntity WHERE uid = :uid") + abstract fun countByUid(uid: String): Flow + + @Query(""" + SELECT * + FROM MemoEntity + WHERE id IN (SELECT memoId FROM MemoBackupEntity WHERE uid = :uid) + LIMIT 50 + """) + abstract fun findByUid(uid: String): Flow> +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt new file mode 100644 index 00000000..6d198b24 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoBackupRoomDao.kt @@ -0,0 +1,53 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import io.github.taetae98coding.diary.core.diary.database.MemoBackupDao +import io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoBackupEntity +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import io.github.taetae98coding.diary.core.diary.database.room.mapper.toDto +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.annotation.Factory + +@Factory +internal class MemoBackupRoomDao( + private val database: DiaryDatabase, +) : MemoBackupDao { + override suspend fun upsert(uid: String, memoId: String) { + database.memoBackup().upsert(MemoBackupEntity(uid, memoId)) + getInternalUpdateFlow(uid).update { it + 1 } + } + + override suspend fun delete(uid: String, memoId: String) { + database.memoBackup().delete(MemoBackupEntity(uid, memoId)) + } + + override suspend fun deleteByMemoIds(memoIds: List) { + database.memoBackup().deleteByMemoIds(memoIds) + } + + override fun getUpdateFlow(uid: String): Flow { + return getInternalUpdateFlow(uid).asStateFlow() + } + + override fun countByUid(uid: String): Flow { + return database.memoBackup().countByUid(uid) + } + + override fun findByUid(uid: String): Flow> { + return database.memoBackup().findByUid(uid) + .mapCollectionLatest(MemoEntity::toDto) + } + + companion object { + private val updateFlow = mutableMapOf>() + + private fun getInternalUpdateFlow(uid: String): MutableStateFlow { + return updateFlow.getOrPut(uid) { MutableStateFlow(0) } + } + } +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoEntityDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoEntityDao.kt new file mode 100644 index 00000000..74289394 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoEntityDao.kt @@ -0,0 +1,81 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate + +@Dao +internal abstract class MemoEntityDao : EntityDao() { + @Query( + """ + UPDATE MemoEntity + SET + title = :title, + description = :description, + start = :start, + endInclusive = :endInclusive, + color = :color, + updateAt = :updateAt + WHERE id = :memoId + """, + ) + abstract suspend fun update( + memoId: String, + title: String, + description: String, + start: LocalDate?, + endInclusive: LocalDate?, + color: Int, + updateAt: Instant, + ) + + @Query( + """ + UPDATE MemoEntity + SET + isFinish = :isFinish, + updateAt = :updateAt + WHERE id = :memoId + """, + ) + abstract suspend fun updateFinish(memoId: String, isFinish: Boolean, updateAt: Instant) + + @Query( + """ + UPDATE MemoEntity + SET + isDelete = :isDelete, + updateAt = :updateAt + WHERE id = :memoId + """, + ) + abstract suspend fun updateDelete(memoId: String, isDelete: Boolean, updateAt: Instant) + + @Query( + """ + SELECT * + FROM MemoEntity + WHERE id = :memoId + """, + ) + abstract fun find(memoId: String): Flow + + @Query( + """ + SELECT * + FROM MemoEntity + WHERE isDelete = 0 + AND (owner = :owner OR (owner IS NULL AND :owner IS NULL)) + AND start IS NOT NULL + AND endInclusive IS NOT NULL + AND endInclusive >= :start AND start <= :endInclusive + """, + ) + abstract fun findByDateRange(owner: String?, start: LocalDate, endInclusive: LocalDate): Flow> + + @Query("SELECT MAX(serverUpdateAt) FROM MemoEntity WHERE owner = :owner") + abstract fun getLastUpdateAt(owner: String?): Flow +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt new file mode 100644 index 00000000..cbf97aa5 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/dao/MemoRoomDao.kt @@ -0,0 +1,67 @@ +package io.github.taetae98coding.diary.core.diary.database.room.dao + +import io.github.taetae98coding.diary.core.diary.database.MemoDao +import io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import io.github.taetae98coding.diary.core.diary.database.room.mapper.toDto +import io.github.taetae98coding.diary.core.diary.database.room.mapper.toEntity +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +internal class MemoRoomDao( + private val clock: Clock, + private val database: DiaryDatabase, +) : MemoDao { + override suspend fun upsert(memo: MemoDto) { + database.memo().upsert(memo.toEntity()) + } + + override suspend fun upsert(memoList: List) { + database.memo().upsert(memoList.map(MemoDto::toEntity)) + } + + override suspend fun update(memoId: String, detail: MemoDetail) { + database.memo() + .update( + memoId = memoId, + title = detail.title, + description = detail.description, + start = detail.start, + endInclusive = detail.endInclusive, + color = detail.color, + updateAt = clock.now(), + ) + } + + override suspend fun updateFinish(memoId: String, isFinish: Boolean) { + database.memo().updateFinish(memoId, isFinish, clock.now()) + } + + override suspend fun updateDelete(memoId: String, isDelete: Boolean) { + database.memo().updateDelete(memoId, isDelete, clock.now()) + } + + override fun find(memoId: String): Flow { + return database.memo().find(memoId) + .mapLatest { it?.toDto() } + } + + override fun findByDateRange(owner: String?, dateRange: ClosedRange): Flow> { + return database.memo().findByDateRange(owner, dateRange.start, dateRange.endInclusive) + .mapCollectionLatest(MemoEntity::toDto) + } + + override fun getLastServerUpdateAt(owner: String?): Flow { + return database.memo().getLastUpdateAt(owner) + } +} diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoBackupEntity.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoBackupEntity.kt new file mode 100644 index 00000000..38a9242c --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoBackupEntity.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.core.diary.database.room.entity + +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + primaryKeys = ["uid", "memoId"], + foreignKeys = [ + ForeignKey( + entity = MemoEntity::class, + parentColumns = ["id"], + childColumns = ["memoId"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE, + ), + ], +) +internal data class MemoBackupEntity( + val uid: String, + val memoId: String, +) diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoEntity.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoEntity.kt new file mode 100644 index 00000000..16b9eda4 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/entity/MemoEntity.kt @@ -0,0 +1,34 @@ +package io.github.taetae98coding.diary.core.diary.database.room.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate + +@Entity +internal data class MemoEntity( + @PrimaryKey + @ColumnInfo(defaultValue = "") + val id: String, + @ColumnInfo(defaultValue = "") + val title: String, + @ColumnInfo(defaultValue = "") + val description: String, + @ColumnInfo(defaultValue = "null") + val start: LocalDate?, + @ColumnInfo(defaultValue = "null") + val endInclusive: LocalDate?, + @ColumnInfo(defaultValue = "-16777216") + val color: Int, + @ColumnInfo(defaultValue = "0") + val isFinish: Boolean, + @ColumnInfo(defaultValue = "0") + val isDelete: Boolean, + @ColumnInfo(defaultValue = "null") + val owner: String?, + @ColumnInfo(defaultValue = "0") + val updateAt: Instant, + @ColumnInfo(defaultValue = "null") + val serverUpdateAt: Instant?, +) diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/internal/DiaryDatabaseConstructor.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/internal/DiaryDatabaseConstructor.kt new file mode 100644 index 00000000..f9499307 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/internal/DiaryDatabaseConstructor.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.diary.database.room.internal + +import androidx.room.RoomDatabaseConstructor +import io.github.taetae98coding.diary.core.diary.database.room.DiaryDatabase + +internal expect object DiaryDatabaseConstructor : RoomDatabaseConstructor diff --git a/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt new file mode 100644 index 00000000..19d88281 --- /dev/null +++ b/app/core/diary-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/room/mapper/MemoMapper.kt @@ -0,0 +1,39 @@ +package io.github.taetae98coding.diary.core.diary.database.room.mapper + +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.github.taetae98coding.diary.core.diary.database.room.entity.MemoEntity +import io.github.taetae98coding.diary.core.model.memo.MemoDetail + +internal fun MemoDto.toEntity(): MemoEntity { + return MemoEntity( + id = id, + title = detail.title, + description = detail.description, + start = detail.start, + endInclusive = detail.endInclusive, + color = detail.color, + isFinish = isFinish, + isDelete = isDelete, + owner = owner, + updateAt = updateAt, + serverUpdateAt = serverUpdateAt, + ) +} + +internal fun MemoEntity.toDto(): MemoDto { + return MemoDto( + id = id, + detail = MemoDetail( + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + ), + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + serverUpdateAt = serverUpdateAt, + ) +} diff --git a/app/core/diary-database/README.md b/app/core/diary-database/README.md new file mode 100644 index 00000000..b3632cd6 --- /dev/null +++ b/app/core/diary-database/README.md @@ -0,0 +1,3 @@ +# :app:core:diary-database module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_diary_database.svg) diff --git a/app/core/diary-database/build.gradle.kts b/app/core/diary-database/build.gradle.kts new file mode 100644 index 00000000..0c9d7073 --- /dev/null +++ b/app/core/diary-database/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":app:core:model")) + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.datetime) + } + } + } +} diff --git a/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt new file mode 100644 index 00000000..6d21188b --- /dev/null +++ b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoBackupDao.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.core.diary.database + +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import kotlinx.coroutines.flow.Flow + +public interface MemoBackupDao { + public suspend fun upsert(uid: String, memoId: String) + public suspend fun delete(uid: String, memoId: String) + public suspend fun deleteByMemoIds(memoIds: List) + + public fun getUpdateFlow(uid: String): Flow + public fun countByUid(uid: String): Flow + public fun findByUid(uid: String): Flow> +} diff --git a/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt new file mode 100644 index 00000000..42af30bc --- /dev/null +++ b/app/core/diary-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/database/MemoDao.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.core.diary.database + +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate + +public interface MemoDao { + public suspend fun upsert(memo: MemoDto) + public suspend fun upsert(memoList: List) + + public suspend fun update(memoId: String, detail: MemoDetail) + public suspend fun updateFinish(memoId: String, isFinish: Boolean) + public suspend fun updateDelete(memoId: String, isDelete: Boolean) + + public fun find(memoId: String): Flow + public fun findByDateRange(owner: String?, dateRange: ClosedRange): Flow> + public fun getLastServerUpdateAt(owner: String?): Flow +} diff --git a/app/core/diary-service/README.md b/app/core/diary-service/README.md new file mode 100644 index 00000000..d9f58e79 --- /dev/null +++ b/app/core/diary-service/README.md @@ -0,0 +1,3 @@ +# :app:core:diary-service module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_diary_service.svg) diff --git a/app/core/diary-service/build.gradle.kts b/app/core/diary-service/build.gradle.kts new file mode 100644 index 00000000..2dbb4bee --- /dev/null +++ b/app/core/diary-service/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("diary.kotlin.multiplatform.common") + id("diary.koin.common") + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:account-preferences")) + implementation(project(":common:model")) + implementation(libs.bundles.ktor.client) + + api(project(":app:core:model")) + api(project(":common:exception")) + } + } + } +} diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/DiaryServiceModule.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/DiaryServiceModule.kt new file mode 100644 index 00000000..e86af873 --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/DiaryServiceModule.kt @@ -0,0 +1,57 @@ +package io.github.taetae98coding.diary.core.diary.service + +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences +import io.github.taetae98coding.diary.core.diary.service.plugin.DiaryClientTokenPlugin +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.serialization.kotlinx.json.DefaultJson +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Singleton + +@Module +@ComponentScan +public class DiaryServiceModule { + @Singleton + @Named(DIARY_JSON) + internal fun providesDiaryJson(): Json { + return Json(DefaultJson) { + ignoreUnknownKeys = true + } + } + + @Singleton + @Named(DIARY_CLIENT) + internal fun providesDiaryClient( + accountPreferences: AccountPreferences, + @Named(DIARY_JSON) + json: Json, + @Named(DIARY_API_URL) + apiUrl: String, + ): HttpClient { + return HttpClient { + defaultRequest { + url(apiUrl) + } + + install(ContentNegotiation) { + json(json) + } + + install(DiaryClientTokenPlugin) { + preferences = accountPreferences + } + } + } + + public companion object { + internal const val DIARY_JSON = "DIARY_JSON" + internal const val DIARY_CLIENT = "DIARY_CLIENT" + + public const val DIARY_API_URL: String = "DIARY_API_URL" + } +} diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/account/AccountService.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/account/AccountService.kt new file mode 100644 index 00000000..aa6ac76d --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/account/AccountService.kt @@ -0,0 +1,50 @@ +package io.github.taetae98coding.diary.core.diary.service.account + +import io.github.taetae98coding.diary.common.model.request.account.JoinRequest +import io.github.taetae98coding.diary.common.model.request.account.LoginRequest +import io.github.taetae98coding.diary.common.model.response.account.LoginResponse +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.diary.service.ext.getOrThrow +import io.github.taetae98coding.diary.core.model.account.AccountToken +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Named + +@Factory +public class AccountService internal constructor( + @Named(DiaryServiceModule.DIARY_CLIENT) + private val client: HttpClient, +) { + public suspend fun join(email: String, password: String) { + return client.post("/account/join") { + val body = JoinRequest( + email = email, + password = password, + ) + + contentType(ContentType.Application.Json) + setBody(body) + }.getOrThrow() + } + + public suspend fun login(email: String, password: String): AccountToken { + val response = client.post("/account/login") { + val body = LoginRequest( + email = email, + password = password, + ) + + contentType(ContentType.Application.Json) + setBody(body) + }.getOrThrow() + + return AccountToken( + uid = response.uid, + token = response.token, + ) + } +} diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt new file mode 100644 index 00000000..cec64999 --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/ext/HttpClientExt.kt @@ -0,0 +1,25 @@ +package io.github.taetae98coding.diary.core.diary.service.ext + +import io.github.taetae98coding.diary.common.exception.ApiException +import io.github.taetae98coding.diary.common.exception.account.AccountNotFoundException +import io.github.taetae98coding.diary.common.exception.account.ExistEmailException +import io.github.taetae98coding.diary.common.model.response.DiaryResponse +import io.ktor.client.call.body +import io.ktor.client.statement.HttpResponse +import io.ktor.http.isSuccess + +internal suspend inline fun HttpResponse.getOrThrow(): T { + return if (status.isSuccess()) { + val body = body>() + + requireNotNull(body.body) + } else { + val exception = when (val errorBody = body>()) { + DiaryResponse.AlreadyExistEmail -> ExistEmailException() + DiaryResponse.AccountNotFound -> AccountNotFoundException() + else -> ApiException(errorBody.message) + } + + throw exception + } +} diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt new file mode 100644 index 00000000..503415c1 --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/memo/MemoService.kt @@ -0,0 +1,70 @@ +package io.github.taetae98coding.diary.core.diary.service.memo + +import io.github.taetae98coding.diary.common.model.memo.MemoEntity +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.diary.service.ext.getOrThrow +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.datetime.Instant +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Named + +@Factory +public class MemoService internal constructor( + @Named(DiaryServiceModule.DIARY_CLIENT) + private val client: HttpClient, +) { + public suspend fun upsert(list: List) { + return client.post("/memo/upsert") { + val body = list.map { + MemoEntity( + id = it.id, + title = it.detail.title, + description = it.detail.description, + start = it.detail.start, + endInclusive = it.detail.endInclusive, + color = it.detail.color, + owner = requireNotNull(it.owner), + isFinish = it.isFinish, + isDelete = it.isDelete, + updateAt = it.updateAt, + ) + } + + contentType(ContentType.Application.Json) + setBody(body) + }.getOrThrow() + } + + public suspend fun fetch(updateAt: Instant): List { + val response = client.get("/memo/fetch") { + parameter("updateAt", updateAt) + }.getOrThrow>() + + return response.map { + MemoDto( + id = it.id, + detail = MemoDetail( + title = it.title, + description = it.description, + start = it.start, + endInclusive = it.endInclusive, + color = it.color, + ), + owner = it.owner, + isFinish = it.isFinish, + isDelete = it.isDelete, + updateAt = it.updateAt, + serverUpdateAt = it.updateAt, + ) + } + } +} diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/AccountPreferencesOwner.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/AccountPreferencesOwner.kt new file mode 100644 index 00000000..3682b519 --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/AccountPreferencesOwner.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.core.diary.service.plugin + +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences + +internal class AccountPreferencesOwner { + var preferences: AccountPreferences? = null +} diff --git a/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/DiaryClientTokenPlugin.kt b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/DiaryClientTokenPlugin.kt new file mode 100644 index 00000000..cfded20d --- /dev/null +++ b/app/core/diary-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/diary/service/plugin/DiaryClientTokenPlugin.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.core.diary.service.plugin + +import io.ktor.client.plugins.api.createClientPlugin +import io.ktor.client.request.header +import kotlinx.coroutines.flow.first + +internal val DiaryClientTokenPlugin = createClientPlugin( + name = "DiaryClientTokenPlugin", + createConfiguration = { AccountPreferencesOwner() }, + body = { + val owner = pluginConfig + + onRequest { request, _ -> + owner.preferences?.getToken() + ?.first() + ?.let { request.header("Authorization", "Bearer $it") } + } + }, +) diff --git a/app/core/holiday-database-memory/README.md b/app/core/holiday-database-memory/README.md new file mode 100644 index 00000000..f03cf94f --- /dev/null +++ b/app/core/holiday-database-memory/README.md @@ -0,0 +1,3 @@ +# :app:core:holiday-database-memory module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_holiday_database_memory.svg) diff --git a/app/core/holiday-database-memory/build.gradle.kts b/app/core/holiday-database-memory/build.gradle.kts new file mode 100644 index 00000000..f63d5df3 --- /dev/null +++ b/app/core/holiday-database-memory/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.koin.all") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:holiday-database")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.holiday.database.memory" +} diff --git a/app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDao.kt b/app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDao.kt new file mode 100644 index 00000000..c4a537ab --- /dev/null +++ b/app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDao.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.core.holiday.database.memory + +import io.github.taetae98coding.diary.core.holiday.database.HolidayDao +import io.github.taetae98coding.diary.core.model.holiday.Holiday +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.datetime.Month + +internal data object HolidayMemoryDao : HolidayDao { + private val flow = mutableMapOf, MutableStateFlow>>() + + override fun findHoliday(year: Int, month: Month): Flow> { + return getFlow(year, month).asStateFlow() + } + + override suspend fun upsert(year: Int, month: Month, holidayList: List) { + getFlow(year, month).emit(holidayList) + } + + private fun getFlow(year: Int, month: Month): MutableStateFlow> { + return flow.getOrPut(year to month) { MutableStateFlow(emptyList()) } + } +} diff --git a/app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDatabaseModule.kt b/app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDatabaseModule.kt new file mode 100644 index 00000000..c266c176 --- /dev/null +++ b/app/core/holiday-database-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/memory/HolidayMemoryDatabaseModule.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.core.holiday.database.memory + +import io.github.taetae98coding.diary.core.holiday.database.HolidayDao +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton + +@Module +@ComponentScan +public class HolidayMemoryDatabaseModule { + @Singleton + internal fun providesHolidayDao(): HolidayDao { + return HolidayMemoryDao + } +} diff --git a/app/core/holiday-database-room/README.md b/app/core/holiday-database-room/README.md new file mode 100644 index 00000000..57c04472 --- /dev/null +++ b/app/core/holiday-database-room/README.md @@ -0,0 +1,3 @@ +# :app:core:holiday-database-room module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_holiday_database_room.svg) diff --git a/app/core/holiday-database-room/build.gradle.kts b/app/core/holiday-database-room/build.gradle.kts new file mode 100644 index 00000000..d90dca4c --- /dev/null +++ b/app/core/holiday-database-room/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("diary.room") + id("diary.koin.room") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:holiday-database")) + implementation(project(":library:room")) + implementation(project(":library:coroutines")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.holiday.database.room" +} diff --git a/app/core/holiday-database-room/schemas/io.github.taetae98coding.diary.core.holiday.database.room.HolidayDatabase/1.json b/app/core/holiday-database-room/schemas/io.github.taetae98coding.diary.core.holiday.database.room.HolidayDatabase/1.json new file mode 100644 index 00000000..90b2aa50 --- /dev/null +++ b/app/core/holiday-database-room/schemas/io.github.taetae98coding.diary.core.holiday.database.room.HolidayDatabase/1.json @@ -0,0 +1,48 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "744af569b9a8a02218a9a3d1ebfee7c8", + "entities": [ + { + "tableName": "HolidayEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL DEFAULT '', `start` TEXT NOT NULL DEFAULT '1900-01-01', `endInclusive` TEXT NOT NULL DEFAULT '1900-01-01', PRIMARY KEY(`name`, `start`, `endInclusive`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'1900-01-01'" + }, + { + "fieldPath": "endInclusive", + "columnName": "endInclusive", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'1900-01-01'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name", + "start", + "endInclusive" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '744af569b9a8a02218a9a3d1ebfee7c8')" + ] + } +} \ No newline at end of file diff --git a/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayDatabase.kt b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayDatabase.kt new file mode 100644 index 00000000..b1110c82 --- /dev/null +++ b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayDatabase.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.holiday.database.room + +import androidx.room.ConstructedBy +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import io.github.taetae98coding.diary.core.holiday.database.room.internal.HolidayDatabaseConstructor +import io.github.taetae98coding.diary.library.room.LocalDataConverter + +@Database( + entities = [HolidayEntity::class], + version = 1, +) +@ConstructedBy(HolidayDatabaseConstructor::class) +@TypeConverters(LocalDataConverter::class) +internal abstract class HolidayDatabase : RoomDatabase() { + abstract fun holidayDao(): HolidayEntityDao +} diff --git a/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntity.kt b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntity.kt new file mode 100644 index 00000000..ea2bc864 --- /dev/null +++ b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntity.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.holiday.database.room + +import androidx.room.ColumnInfo +import androidx.room.Entity +import kotlinx.datetime.LocalDate + +@Entity( + primaryKeys = ["name", "start", "endInclusive"], +) +internal data class HolidayEntity( + @ColumnInfo(defaultValue = "") + val name: String, + @ColumnInfo(defaultValue = "1900-01-01") + override val start: LocalDate, + @ColumnInfo(defaultValue = "1900-01-01") + override val endInclusive: LocalDate, +) : ClosedRange diff --git a/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntityDao.kt b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntityDao.kt new file mode 100644 index 00000000..ed9507da --- /dev/null +++ b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayEntityDao.kt @@ -0,0 +1,38 @@ +package io.github.taetae98coding.diary.core.holiday.database.room + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow + +@Dao +internal abstract class HolidayEntityDao { + @Query( + """ + SELECT * + FROM HolidayEntity + WHERE (CAST(STRFTIME('%Y', start) AS INTEGER) = :year OR CAST(STRFTIME('%Y', endInclusive) AS INTEGER) = :year) + AND (CAST(STRFTIME('%m', start) AS INTEGER) = :month OR CAST(STRFTIME('%m', endInclusive) AS INTEGER) = :month) + """, + ) + abstract fun findHoliday(year: Int, month: Int): Flow> + + @Insert(HolidayEntity::class) + protected abstract suspend fun insert(holiday: List) + + @Query( + """ + DELETE FROM HolidayEntity + WHERE (CAST(STRFTIME('%Y', start) AS INTEGER) = :year OR CAST(STRFTIME('%Y', endInclusive) AS INTEGER) = :year) + AND (CAST(STRFTIME('%m', start) AS INTEGER) = :month OR CAST(STRFTIME('%m', endInclusive) AS INTEGER) = :month) + """, + ) + protected abstract suspend fun delete(year: Int, month: Int) + + @Transaction + open suspend fun upsert(year: Int, month: Int, holiday: List) { + delete(year, month) + insert(holiday) + } +} diff --git a/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayMapper.kt b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayMapper.kt new file mode 100644 index 00000000..44108a69 --- /dev/null +++ b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayMapper.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.core.holiday.database.room + +import io.github.taetae98coding.diary.core.model.holiday.Holiday + +internal fun Holiday.toEntity(): HolidayEntity { + return HolidayEntity( + name = name, + start = start, + endInclusive = endInclusive, + ) +} + + +internal fun HolidayEntity.toHoliday(): Holiday { + return Holiday( + name = name, + start = start, + endInclusive = endInclusive, + ) +} diff --git a/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDao.kt b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDao.kt new file mode 100644 index 00000000..cb7abed1 --- /dev/null +++ b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDao.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.core.holiday.database.room + +import io.github.taetae98coding.diary.core.holiday.database.HolidayDao +import io.github.taetae98coding.diary.core.model.holiday.Holiday +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Month +import kotlinx.datetime.number +import org.koin.core.annotation.Factory + +@Factory +internal class HolidayRoomDao( + private val database: HolidayDatabase, +) : HolidayDao { + override fun findHoliday(year: Int, month: Month): Flow> { + return database.holidayDao().findHoliday(year, month.number) + .mapCollectionLatest(HolidayEntity::toHoliday) + } + + override suspend fun upsert(year: Int, month: Month, holidayList: List) { + database.holidayDao().upsert(year, month.number, holidayList.map(Holiday::toEntity)) + } +} diff --git a/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDatabaseModule.kt b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDatabaseModule.kt new file mode 100644 index 00000000..e0f495b5 --- /dev/null +++ b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/HolidayRoomDatabaseModule.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.holiday.database.room + +import io.github.taetae98coding.diary.library.koin.room.getDatabaseBuilder +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton +import org.koin.core.component.KoinComponent + +@Module +@ComponentScan +public class HolidayRoomDatabaseModule : KoinComponent { + @Singleton + internal fun providesHolidayDatabase(): HolidayDatabase { + return getDatabaseBuilder("holiday.db") + .build() + } +} diff --git a/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/internal/HolidayDatabaseConstructor.kt b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/internal/HolidayDatabaseConstructor.kt new file mode 100644 index 00000000..7f057ebc --- /dev/null +++ b/app/core/holiday-database-room/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/room/internal/HolidayDatabaseConstructor.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.core.holiday.database.room.internal + +import androidx.room.RoomDatabaseConstructor +import io.github.taetae98coding.diary.core.holiday.database.room.HolidayDatabase + +internal expect object HolidayDatabaseConstructor : RoomDatabaseConstructor { + override fun initialize(): HolidayDatabase +} diff --git a/app/core/holiday-database/README.md b/app/core/holiday-database/README.md new file mode 100644 index 00000000..50fb2512 --- /dev/null +++ b/app/core/holiday-database/README.md @@ -0,0 +1,3 @@ +# :app:core:holiday-database module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_holiday_database.svg) diff --git a/app/core/holiday-database/build.gradle.kts b/app/core/holiday-database/build.gradle.kts new file mode 100644 index 00000000..0c9d7073 --- /dev/null +++ b/app/core/holiday-database/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":app:core:model")) + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.datetime) + } + } + } +} diff --git a/app/core/holiday-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/HolidayDao.kt b/app/core/holiday-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/HolidayDao.kt new file mode 100644 index 00000000..7559494f --- /dev/null +++ b/app/core/holiday-database/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/database/HolidayDao.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.holiday.database + +import io.github.taetae98coding.diary.core.model.holiday.Holiday +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Month + +public interface HolidayDao { + public fun findHoliday(year: Int, month: Month): Flow> + + public suspend fun upsert(year: Int, month: Month, holidayList: List) +} diff --git a/app/core/holiday-preferences-datastore/README.md b/app/core/holiday-preferences-datastore/README.md new file mode 100644 index 00000000..b46bfa34 --- /dev/null +++ b/app/core/holiday-preferences-datastore/README.md @@ -0,0 +1,3 @@ +# :app:core:holiday-preferences-datastore module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg) diff --git a/app/core/holiday-preferences-datastore/build.gradle.kts b/app/core/holiday-preferences-datastore/build.gradle.kts new file mode 100644 index 00000000..ebe086e9 --- /dev/null +++ b/app/core/holiday-preferences-datastore/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("diary.datastore") + id("diary.koin.datastore") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:holiday-preferences")) + implementation(project(":library:datetime")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.holiday.preferences.datastore" +} diff --git a/app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferences.kt b/app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferences.kt new file mode 100644 index 00000000..d8c3c98c --- /dev/null +++ b/app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferences.kt @@ -0,0 +1,51 @@ +package io.github.taetae98coding.diary.core.holiday.preferences.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import io.github.taetae98coding.diary.core.holiday.preferences.HolidayPreferences +import io.github.taetae98coding.diary.library.datetime.todayIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.monthsUntil +import kotlinx.datetime.number + +internal class HolidayDataStorePreferences( + private val clock: Clock, + private val dataStore: DataStore, +) : HolidayPreferences { + private val memoryDirtyStore = mutableMapOf, MutableStateFlow>() + + override fun isDirty(year: Int, month: Month): Flow { + val dateStoreFlow = dataStore.data.map { it[dirtyKey(year, month)] } + .map { it ?: false } + val memoryFlow = memoryDirtyStore.getOrPut(year to month) { MutableStateFlow(false) } + + return combine(dateStoreFlow, memoryFlow) { dataStore, memory -> dataStore || memory } + } + + override suspend fun setDirty(year: Int, month: Month) { + if (isFuture(year, month)) { + memoryDirtyStore.getOrPut(year to month) { MutableStateFlow(false) }.emit(true) + } else { + dataStore.edit { it[dirtyKey(year, month)] = true } + } + } + + private fun dirtyKey(year: Int, month: Month): Preferences.Key { + return booleanPreferencesKey("$year${month.number.toString().padStart(2, '0')}") + } + + private fun isFuture(year: Int, month: Month): Boolean { + val today = LocalDate.todayIn(clock = clock) + val target = LocalDate(year, month, 1) + + return today.monthsUntil(target) >= 0 && !(today.year == target.year && today.month == target.month) + } +} diff --git a/app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferencesModule.kt b/app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferencesModule.kt new file mode 100644 index 00000000..b935eab2 --- /dev/null +++ b/app/core/holiday-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/datastore/HolidayDataStorePreferencesModule.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.core.holiday.preferences.datastore + +import io.github.taetae98coding.diary.core.holiday.preferences.HolidayPreferences +import io.github.taetae98coding.diary.library.koin.datastore.getDataStore +import kotlinx.datetime.Clock +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton +import org.koin.core.component.KoinComponent + +@Module +@ComponentScan +public class HolidayDataStorePreferencesModule : KoinComponent { + @Singleton + internal fun providesHolidayPreferences( + clock: Clock, + ): HolidayPreferences { + return HolidayDataStorePreferences( + clock = clock, + dataStore = getDataStore("holiday.preferences_pb"), + ) + } +} diff --git a/app/core/holiday-preferences-memory/README.md b/app/core/holiday-preferences-memory/README.md new file mode 100644 index 00000000..cb35719b --- /dev/null +++ b/app/core/holiday-preferences-memory/README.md @@ -0,0 +1,3 @@ +# :app:core:holiday-preferences-memory module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_holiday_preferences_memory.svg) diff --git a/app/core/holiday-preferences-memory/build.gradle.kts b/app/core/holiday-preferences-memory/build.gradle.kts new file mode 100644 index 00000000..d52d2421 --- /dev/null +++ b/app/core/holiday-preferences-memory/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.koin.all") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:holiday-preferences")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.holiday.preferences.memory" +} diff --git a/app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayMemoryPreferences.kt b/app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayMemoryPreferences.kt new file mode 100644 index 00000000..3a433583 --- /dev/null +++ b/app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayMemoryPreferences.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.core.holiday.preferences.memory + +import io.github.taetae98coding.diary.core.holiday.preferences.HolidayPreferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.datetime.Month + +internal data object HolidayMemoryPreferences : HolidayPreferences{ + private val map = mutableMapOf, MutableStateFlow>() + + override fun isDirty(year: Int, month: Month): Flow { + return getFlow(year, month).asStateFlow() + } + + override suspend fun setDirty(year: Int, month: Month) { + getFlow(year, month).emit(true) + } + + private fun getFlow(year: Int, month: Month): MutableStateFlow { + return map.getOrPut(year to month) { MutableStateFlow(false) } + } +} diff --git a/app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayPreferencesMemoryModule.kt b/app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayPreferencesMemoryModule.kt new file mode 100644 index 00000000..7be84278 --- /dev/null +++ b/app/core/holiday-preferences-memory/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/memory/HolidayPreferencesMemoryModule.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.core.holiday.preferences.memory + +import io.github.taetae98coding.diary.core.holiday.preferences.HolidayPreferences +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton + +@Module +@ComponentScan +public class HolidayPreferencesMemoryModule { + @Singleton + internal fun providesHolidayPreferences(): HolidayPreferences { + return HolidayMemoryPreferences + } +} diff --git a/app/core/holiday-preferences/README.md b/app/core/holiday-preferences/README.md new file mode 100644 index 00000000..2b7225ec --- /dev/null +++ b/app/core/holiday-preferences/README.md @@ -0,0 +1,3 @@ +# :app:core:holiday-preferences module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_holiday_preferences.svg) diff --git a/app/core/holiday-preferences/build.gradle.kts b/app/core/holiday-preferences/build.gradle.kts new file mode 100644 index 00000000..316aa78d --- /dev/null +++ b/app/core/holiday-preferences/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.datetime) + } + } + } +} diff --git a/app/core/holiday-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/HolidayPreferences.kt b/app/core/holiday-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/HolidayPreferences.kt new file mode 100644 index 00000000..2fb0d8a7 --- /dev/null +++ b/app/core/holiday-preferences/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/preferences/HolidayPreferences.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.core.holiday.preferences + +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Month + +public interface HolidayPreferences { + public fun isDirty(year: Int, month: Month): Flow + public suspend fun setDirty(year: Int, month: Month) +} diff --git a/app/core/holiday-service/README.md b/app/core/holiday-service/README.md new file mode 100644 index 00000000..b19cec49 --- /dev/null +++ b/app/core/holiday-service/README.md @@ -0,0 +1,3 @@ +# :app:core:holiday-service module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_holiday_service.svg) diff --git a/app/core/holiday-service/build.gradle.kts b/app/core/holiday-service/build.gradle.kts new file mode 100644 index 00000000..e490f4d6 --- /dev/null +++ b/app/core/holiday-service/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("diary.kotlin.multiplatform.common") + id("diary.koin.common") + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:model")) + implementation(libs.bundles.ktor.client) + } + } + } +} diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayService.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayService.kt new file mode 100644 index 00000000..dd239196 --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayService.kt @@ -0,0 +1,45 @@ +package io.github.taetae98coding.diary.core.holiday.service + +import io.github.taetae98coding.diary.core.holiday.service.entity.ApiResultEntity +import io.github.taetae98coding.diary.core.holiday.service.entity.BodyEntity +import io.github.taetae98coding.diary.core.holiday.service.entity.HolidayEntity +import io.github.taetae98coding.diary.core.holiday.service.entity.HolidayItemEntity +import io.github.taetae98coding.diary.core.holiday.service.entity.HolidayItemsEntity +import io.github.taetae98coding.diary.core.holiday.service.mapper.toHoliday +import io.github.taetae98coding.diary.core.model.holiday.Holiday +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import kotlinx.datetime.Month +import kotlinx.datetime.number +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement + +public class HolidayService internal constructor( + private val client: HttpClient, + private val json: Json, +) { + public suspend fun findHoliday(year: Int, month: Month): List { + val response = client.get("getRestDeInfo") { + parameter("solYear", year) + parameter("solMonth", month.number.toString().padStart(2, '0')) + } + + return response.body() + .parseHolidayEntity(json) + .map(HolidayEntity::toHoliday) + } + + private fun ApiResultEntity.parseHolidayEntity(json: Json): List { + return response.body.parseHolidayEntity(json) + } + + private fun BodyEntity.parseHolidayEntity(json: Json): List { + return when (count) { + 0 -> emptyList() + 1 -> listOf(json.decodeFromJsonElement(items).item) + else -> json.decodeFromJsonElement(items).items + } + } +} diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayServiceModule.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayServiceModule.kt new file mode 100644 index 00000000..05f80b23 --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/HolidayServiceModule.kt @@ -0,0 +1,55 @@ +package io.github.taetae98coding.diary.core.holiday.service + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.serialization.kotlinx.json.DefaultJson +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Singleton + +@Module +@ComponentScan +public class HolidayServiceModule { + @Singleton + internal fun providesHolidayService( + @Named(HOLIDAY_API_URL) + apiUrl: String, + @Named(HOLIDAY_API_KEY) + apiKey: String, + ): HolidayService { + val json = Json(DefaultJson) { + ignoreUnknownKeys = true + } + + val client = HttpClient { + expectSuccess = true + + defaultRequest { + url(apiUrl) + url.parameters.append("serviceKey", apiKey) + url.parameters.append("_type", "json") + } + + install(ContentNegotiation) { + json(json) + } + + install(HttpRequestRetry) { + maxRetries = Int.MAX_VALUE + exponentialDelay() + } + } + + return HolidayService(client, json) + } + + public companion object { + public const val HOLIDAY_API_URL: String = "HOLIDAY_API_URL" + public const val HOLIDAY_API_KEY: String = "HOLIDAY_API_KEY" + } +} diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ApiResultEntity.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ApiResultEntity.kt new file mode 100644 index 00000000..f1822cfc --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ApiResultEntity.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.holiday.service.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ApiResultEntity( + @SerialName("response") + val response: ResponseEntity, +) diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/BodyEntity.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/BodyEntity.kt new file mode 100644 index 00000000..433a2e21 --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/BodyEntity.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.holiday.service.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +internal data class BodyEntity( + @SerialName("totalCount") + val count: Int, + @SerialName("items") + val items: JsonElement, +) diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayEntity.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayEntity.kt new file mode 100644 index 00000000..13bb7232 --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayEntity.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.core.holiday.service.entity + +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class HolidayEntity( + @SerialName("dateName") + val name: String, + @SerialName("locdate") + private val date: Int, +) { + val localDate: LocalDate + get() { + val dateString = date.toString() + + return LocalDate( + year = dateString.substring(0 until 4).toInt(), + monthNumber = dateString.substring(4 until 6).toInt(), + dayOfMonth = dateString.substring(6 until 8).toInt(), + ) + } +} diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemEntity.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemEntity.kt new file mode 100644 index 00000000..bda8758b --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemEntity.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.holiday.service.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class HolidayItemEntity( + @SerialName("item") + val item: HolidayEntity +) diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemsEntity.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemsEntity.kt new file mode 100644 index 00000000..e7102828 --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/HolidayItemsEntity.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.holiday.service.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class HolidayItemsEntity( + @SerialName("item") + val items: List +) diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ResponseEntity.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ResponseEntity.kt new file mode 100644 index 00000000..c3e81f46 --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/entity/ResponseEntity.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.holiday.service.entity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ResponseEntity( + @SerialName("body") + val body: BodyEntity, +) diff --git a/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/mapper/HolidayMapper.kt b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/mapper/HolidayMapper.kt new file mode 100644 index 00000000..3c11f8f0 --- /dev/null +++ b/app/core/holiday-service/src/commonMain/kotlin/io/github/taetae98coding/diary/core/holiday/service/mapper/HolidayMapper.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.holiday.service.mapper + +import io.github.taetae98coding.diary.core.holiday.service.entity.HolidayEntity +import io.github.taetae98coding.diary.core.model.holiday.Holiday + +internal fun HolidayEntity.toHoliday(): Holiday { + return Holiday( + name = name, + start = localDate, + endInclusive = localDate, + ) +} diff --git a/app/core/model/README.md b/app/core/model/README.md new file mode 100644 index 00000000..859ad6ab --- /dev/null +++ b/app/core/model/README.md @@ -0,0 +1,3 @@ +# :app:core:model module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_model.svg) diff --git a/app/core/model/build.gradle.kts b/app/core/model/build.gradle.kts new file mode 100644 index 00000000..79aa8585 --- /dev/null +++ b/app/core/model/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.datetime) + } + } + } +} diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt new file mode 100644 index 00000000..98e1af73 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.model.account + +public sealed class Account { + public abstract val uid: String? + + public data object Guest : Account() { + override val uid: String? = null + } + public data class Member( + val email: String, + override val uid: String, + ) : Account() +} diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountToken.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountToken.kt new file mode 100644 index 00000000..1a783b44 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountToken.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.model.account + +public data class AccountToken( + val uid: String, + val token: String +) diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/holiday/Holiday.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/holiday/Holiday.kt new file mode 100644 index 00000000..34609c77 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/holiday/Holiday.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.core.model.holiday + +import kotlinx.datetime.LocalDate + +public data class Holiday( + val name: String, + override val start: LocalDate, + override val endInclusive: LocalDate, +) : ClosedRange diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt new file mode 100644 index 00000000..1dae2193 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/mapper/MemoMapper.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.core.model.mapper + +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.core.model.memo.MemoDto + +public fun MemoDto.toMemo(): Memo { + return Memo( + id = id, + detail = detail, + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + ) +} diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/Memo.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/Memo.kt new file mode 100644 index 00000000..a13794e6 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/Memo.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.core.model.memo + +import kotlinx.datetime.Instant + +public data class Memo( + val id: String, + val detail: MemoDetail, + val owner: String?, + val isFinish: Boolean, + val isDelete: Boolean, + val updateAt: Instant, +) diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt new file mode 100644 index 00000000..2e6f5a6b --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDetail.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.core.model.memo + +import kotlinx.datetime.LocalDate + +public data class MemoDetail( + val title: String, + val description: String, + val start: LocalDate?, + val endInclusive: LocalDate?, + val color: Int, +) diff --git a/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDto.kt b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDto.kt new file mode 100644 index 00000000..1b53f6a4 --- /dev/null +++ b/app/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/memo/MemoDto.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.model.memo + +import kotlinx.datetime.Instant + +public data class MemoDto( + val id: String, + val detail: MemoDetail, + val owner: String?, + val isFinish: Boolean, + val isDelete: Boolean, + val updateAt: Instant, + val serverUpdateAt: Instant?, +) diff --git a/app/core/navigation/README.md b/app/core/navigation/README.md new file mode 100644 index 00000000..5425268c --- /dev/null +++ b/app/core/navigation/README.md @@ -0,0 +1,3 @@ +# :app:core:navigation module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_navigation.svg) diff --git a/app/core/navigation/build.gradle.kts b/app/core/navigation/build.gradle.kts new file mode 100644 index 00000000..0c9e2f48 --- /dev/null +++ b/app/core/navigation/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.kotlin.multiplatform.common") + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:datetime")) + implementation(libs.kotlinx.serialization.core) + } + } + } +} diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/JoinDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/JoinDestination.kt new file mode 100644 index 00000000..64fa4636 --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/JoinDestination.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.navigation.account + +import kotlinx.serialization.Serializable + +@Serializable +public data object JoinDestination diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/LoginDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/LoginDestination.kt new file mode 100644 index 00000000..4c411559 --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/account/LoginDestination.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.navigation.account + +import kotlinx.serialization.Serializable + +@Serializable +public data object LoginDestination diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/calendar/CalendarDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/calendar/CalendarDestination.kt new file mode 100644 index 00000000..94c3fdfa --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/calendar/CalendarDestination.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.navigation.calendar + +import kotlinx.serialization.Serializable + +@Serializable +public data object CalendarDestination diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt new file mode 100644 index 00000000..7fea49b6 --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoAddDestination.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.core.navigation.memo + +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class MemoAddDestination( + @SerialName("start") + val start: LocalDate? = null, + @SerialName("endInclusive") + val endInclusive: LocalDate? = null, +) diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDestination.kt new file mode 100644 index 00000000..55490f63 --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDestination.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.navigation.memo + +import kotlinx.serialization.Serializable + +@Serializable +public data object MemoDestination diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDetailDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDetailDestination.kt new file mode 100644 index 00000000..fc6b6b1d --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/memo/MemoDetailDestination.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.core.navigation.memo + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class MemoDetailDestination( + @SerialName("memoId") + val memoId: String, +) diff --git a/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/more/MoreDestination.kt b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/more/MoreDestination.kt new file mode 100644 index 00000000..eb6e87aa --- /dev/null +++ b/app/core/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/core/navigation/more/MoreDestination.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.core.navigation.more + +import kotlinx.serialization.Serializable + +@Serializable +public data object MoreDestination diff --git a/app/core/resources/README.md b/app/core/resources/README.md new file mode 100644 index 00000000..a1ad0283 --- /dev/null +++ b/app/core/resources/README.md @@ -0,0 +1,3 @@ +# :app:core:resources module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_resources.svg) diff --git a/app/core/resources/build.gradle.kts b/app/core/resources/build.gradle.kts new file mode 100644 index 00000000..91e19841 --- /dev/null +++ b/app/core/resources/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.compose") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(compose.material3) + implementation(compose.materialIconsExtended) + api(compose.components.resources) + } + } + } +} + +compose { + resources { + publicResClass = true + packageOfResClass = "${Build.NAMESPACE}.core.resources" + } +} + +android { + namespace = "${Build.NAMESPACE}.core.resouces" +} diff --git a/app/core/resources/src/commonMain/composeResources/values-ko/strings.xml b/app/core/resources/src/commonMain/composeResources/values-ko/strings.xml new file mode 100644 index 00000000..67d216a0 --- /dev/null +++ b/app/core/resources/src/commonMain/composeResources/values-ko/strings.xml @@ -0,0 +1,47 @@ + + 메모 + 메모 추가 + 캘린더 + 더보기 + 게스트 + 회원가입 + 로그인 + 로그아웃 + 이메일 + 비밀번호 + 선택 + 닫기 + 제목 + 설명 + 날짜 + + %1$d년 %2$d월 + %1$d월 %2$d일 + + 로그인 + 회원가입 + + 이메일을 입력해 주세요 + 비밀번호를 입력해 주세요 + 이메일 형식을 지켜주세요 🫠 + 입력된 패스워드가 달라요 😵‍💫 + + 이미 존재하는 이메일입니다 🥲 + + 비밀번호 확인 + + 메모 추가 + + 제목을 입력해 주세요 🥲 + 계정을 찾을 수 없습니다 🧐 + 네트워크 연결 상태를 확인해 주세요 🤔 + 알 수 없는 에러가 발생했습니다 잠시 후 시도해 주세요 🙀 + + + + + + + + + diff --git a/app/core/resources/src/commonMain/composeResources/values/strings.xml b/app/core/resources/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 00000000..837fae89 --- /dev/null +++ b/app/core/resources/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,47 @@ + + Memo + Memo Add + Calendar + More + Guest + Join + Login + Logout + Email + Password + Select + Dismiss + Title + Description + Date + + %1$d. %2$d. + %1$d. %2$d. + + Login + Join + + The email is blank + The password is blank + Please keep the email format + The password is different + + Exist email + + Check Password + + Memo add + + Title is blank + Account Not Found + Network Error + Unknown Error + + Sun + Mon + Tue + Wed + Thu + Fri + Sat + diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AccountIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AccountIcon.kt new file mode 100644 index 00000000..f5a82dfa --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AccountIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Person +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun AccountIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Person, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AddIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AddIcon.kt new file mode 100644 index 00000000..9e412762 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/AddIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun AddIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/CalendarIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/CalendarIcon.kt new file mode 100644 index 00000000..4dd2e641 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/CalendarIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CalendarToday +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun CalendarIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.CalendarToday, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/ClearIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/ClearIcon.kt new file mode 100644 index 00000000..c89f3c18 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/ClearIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Clear +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun ClearIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Clear, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DeleteIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DeleteIcon.kt new file mode 100644 index 00000000..262936b4 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DeleteIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun DeleteIcon( + modifier: Modifier = Modifier, +) { + Icon( + modifier = modifier, + imageVector = Icons.Rounded.Delete, + contentDescription = null, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropDownIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropDownIcon.kt new file mode 100644 index 00000000..0cb225f2 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropDownIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDropDown +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun DropDownIcon( + modifier: Modifier = Modifier, +) { + Icon( + modifier = modifier, + imageVector = Icons.Rounded.ArrowDropDown, + contentDescription = null, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropUpIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropUpIcon.kt new file mode 100644 index 00000000..002ab7eb --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/DropUpIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDropUp +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun DropUpIcon( + modifier: Modifier = Modifier, +) { + Icon( + modifier = modifier, + imageVector = Icons.Rounded.ArrowDropUp, + contentDescription = null, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/EmailIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/EmailIcon.kt new file mode 100644 index 00000000..ecec91ff --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/EmailIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AlternateEmail +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun EmailIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.AlternateEmail, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/FinishIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/FinishIcon.kt new file mode 100644 index 00000000..55124c7d --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/FinishIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Verified +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun FinishIcon( + modifier: Modifier = Modifier, +) { + Icon( + modifier = modifier, + imageVector = Icons.Rounded.Verified, + contentDescription = null, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/KeyIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/KeyIcon.kt new file mode 100644 index 00000000..0ce22284 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/KeyIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Key +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun KeyIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Key, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LoginIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LoginIcon.kt new file mode 100644 index 00000000..963a553c --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LoginIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Login +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun LoginIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Login, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LogoutIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LogoutIcon.kt new file mode 100644 index 00000000..f3042801 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/LogoutIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Logout +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun LogoutIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Logout, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MarkdownIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MarkdownIcon.kt new file mode 100644 index 00000000..55ebfa9f --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MarkdownIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Code +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun MarkdownIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Code, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MemoIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MemoIcon.kt new file mode 100644 index 00000000..84d4006d --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MemoIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Article +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun MemoIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Article, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MoreIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MoreIcon.kt new file mode 100644 index 00000000..288485ba --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/MoreIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.MoreHoriz +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun MoreIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.MoreHoriz, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/NavigateUpIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/NavigateUpIcon.kt new file mode 100644 index 00000000..a72e745d --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/NavigateUpIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun NavigateUpIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/RefreshIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/RefreshIcon.kt new file mode 100644 index 00000000..b23f4471 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/RefreshIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun RefreshIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/TextFieldIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/TextFieldIcon.kt new file mode 100644 index 00000000..19f6b4b5 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/TextFieldIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.resources.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.TextFields +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun TextFieldIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.TextFields, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOffIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOffIcon.kt new file mode 100644 index 00000000..1b99f61c --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOffIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.design.system.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun VisibilityOffIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.VisibilityOff, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOnIcon.kt b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOnIcon.kt new file mode 100644 index 00000000..2e8a2ad0 --- /dev/null +++ b/app/core/resources/src/commonMain/kotlin/io/github/taetae98coding/diary/core/resources/icon/VisibilityOnIcon.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.core.design.system.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +public fun VisibilityOnIcon( + modifier: Modifier = Modifier, +) { + Icon( + imageVector = Icons.Rounded.Visibility, + contentDescription = null, + modifier = modifier, + ) +} diff --git a/app/data/account/README.md b/app/data/account/README.md new file mode 100644 index 00000000..7d5166f8 --- /dev/null +++ b/app/data/account/README.md @@ -0,0 +1,3 @@ +# :app:data:account module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_data_account.svg) diff --git a/app/data/account/build.gradle.kts b/app/data/account/build.gradle.kts new file mode 100644 index 00000000..884c43b4 --- /dev/null +++ b/app/data/account/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:account-preferences")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:domain:account")) + } + } + } +} diff --git a/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt b/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt new file mode 100644 index 00000000..dc2be468 --- /dev/null +++ b/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.account + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class AccountDataModule diff --git a/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt b/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt new file mode 100644 index 00000000..3f8f7ebe --- /dev/null +++ b/app/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt @@ -0,0 +1,48 @@ +package io.github.taetae98coding.diary.data.account.repository + +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences +import io.github.taetae98coding.diary.core.diary.service.account.AccountService +import io.github.taetae98coding.diary.core.model.account.AccountToken +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +internal class AccountRepositoryImpl( + private val preferencesDataSource: AccountPreferences, + private val remoteDataSource: AccountService, +) : AccountRepository { + override suspend fun join(email: String, password: String) { + remoteDataSource.join(email, password) + } + + override suspend fun save(email: String, token: AccountToken) { + preferencesDataSource.save(email, token.uid, token.token) + } + + override suspend fun clear() { + preferencesDataSource.clear() + } + + override fun getEmail(): Flow { + return preferencesDataSource.getEmail() + } + + override fun getUid(): Flow { + return preferencesDataSource.getUid() + } + + override fun fetchToken(email: String, password: String): Flow { + return flow { emit(remoteDataSource.login(email, password)) } + .mapLatest { + AccountToken( + uid = it.uid, + token = it.token, + ) + } + } +} diff --git a/app/data/backup/README.md b/app/data/backup/README.md new file mode 100644 index 00000000..7666ec36 --- /dev/null +++ b/app/data/backup/README.md @@ -0,0 +1,3 @@ +# :app:data:backup module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_data_backup.svg) diff --git a/app/data/backup/build.gradle.kts b/app/data/backup/build.gradle.kts new file mode 100644 index 00000000..f096bfbd --- /dev/null +++ b/app/data/backup/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:diary-database")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:domain:backup")) + } + } + } +} diff --git a/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/BackupDataModule.kt b/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/BackupDataModule.kt new file mode 100644 index 00000000..3a18399d --- /dev/null +++ b/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/BackupDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.backup + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class BackupDataModule diff --git a/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt b/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt new file mode 100644 index 00000000..1482317a --- /dev/null +++ b/app/data/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/data/backup/repository/BackupRepositoryImpl.kt @@ -0,0 +1,38 @@ +package io.github.taetae98coding.diary.data.backup.repository + +import io.github.taetae98coding.diary.core.diary.database.MemoBackupDao +import io.github.taetae98coding.diary.core.diary.service.memo.MemoService +import io.github.taetae98coding.diary.core.model.mapper.toMemo +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +internal class BackupRepositoryImpl( + private val memoBackupDao: MemoBackupDao, + private val memoService: MemoService, +) : BackupRepository { + override suspend fun backupMemo(uid: String) { + while (memoBackupDao.countByUid(uid).first() > 0) { + val memoList = memoBackupDao.findByUid(uid).first() + .map(MemoDto::toMemo) + + memoService.upsert(memoList) + memoBackupDao.deleteByMemoIds(memoList.map { it.id }) + } + } + + override fun getUpdateFlow(uid: String): Flow { + return memoBackupDao.getUpdateFlow(uid) + .mapLatest { } + } + + override fun countBackupMemo(uid: String): Flow { + return memoBackupDao.countByUid(uid) + } +} diff --git a/app/data/fetch/README.md b/app/data/fetch/README.md new file mode 100644 index 00000000..0e351595 --- /dev/null +++ b/app/data/fetch/README.md @@ -0,0 +1,3 @@ +# :app:data:fetch module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_data_fetch.svg) diff --git a/app/data/fetch/build.gradle.kts b/app/data/fetch/build.gradle.kts new file mode 100644 index 00000000..e8273fd9 --- /dev/null +++ b/app/data/fetch/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:diary-database")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:domain:fetch")) + } + } + } +} diff --git a/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/FetchDataModule.kt b/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/FetchDataModule.kt new file mode 100644 index 00000000..538463c0 --- /dev/null +++ b/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/FetchDataModule.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.data.fetch + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class FetchDataModule { +} diff --git a/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/FetchRepositoryImpl.kt b/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/FetchRepositoryImpl.kt new file mode 100644 index 00000000..33228ece --- /dev/null +++ b/app/data/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/data/fetch/repository/FetchRepositoryImpl.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary.data.fetch.repository + +import io.github.taetae98coding.diary.core.diary.database.MemoDao +import io.github.taetae98coding.diary.core.diary.service.memo.MemoService +import io.github.taetae98coding.diary.domain.fetch.repository.FetchRepository +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.datetime.Instant +import org.koin.core.annotation.Factory + +@Factory +internal class FetchRepositoryImpl( + private val memoDao: MemoDao, + private val memoService: MemoService, +) : FetchRepository { + override suspend fun fetchMemo(uid: String) { + while (true) { + val updateAt = memoDao.getLastServerUpdateAt(uid).first() ?: Instant.fromEpochMilliseconds(0L) + val memoList = memoService.fetch(updateAt) + + if (memoList.isEmpty()) { + break + } + + memoDao.upsert(memoList) + delay(2000L) + } + } +} diff --git a/app/data/holiday/README.md b/app/data/holiday/README.md new file mode 100644 index 00000000..776f3897 --- /dev/null +++ b/app/data/holiday/README.md @@ -0,0 +1,3 @@ +# :app:data:holiday module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_data_holiday.svg) diff --git a/app/data/holiday/build.gradle.kts b/app/data/holiday/build.gradle.kts new file mode 100644 index 00000000..06f18bc9 --- /dev/null +++ b/app/data/holiday/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:holiday-preferences")) + implementation(project(":app:core:holiday-database")) + implementation(project(":app:core:holiday-service")) + implementation(project(":app:domain:holiday")) + } + } + } +} diff --git a/app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/HolidayDataModule.kt b/app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/HolidayDataModule.kt new file mode 100644 index 00000000..6f8d992d --- /dev/null +++ b/app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/HolidayDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.holiday + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class HolidayDataModule diff --git a/app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/repository/HolidayRepositoryImpl.kt b/app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/repository/HolidayRepositoryImpl.kt new file mode 100644 index 00000000..d894f2a4 --- /dev/null +++ b/app/data/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/data/holiday/repository/HolidayRepositoryImpl.kt @@ -0,0 +1,131 @@ +package io.github.taetae98coding.diary.data.holiday.repository + +import io.github.taetae98coding.diary.core.holiday.database.HolidayDao +import io.github.taetae98coding.diary.core.holiday.preferences.HolidayPreferences +import io.github.taetae98coding.diary.core.holiday.service.HolidayService +import io.github.taetae98coding.diary.core.model.holiday.Holiday +import io.github.taetae98coding.diary.domain.holiday.repository.HolidayRepository +import io.github.taetae98coding.diary.library.coroutines.combine +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import io.github.taetae98coding.diary.library.datetime.isOverlap +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.daysUntil +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +internal class HolidayRepositoryImpl( + private val preferencesDataSource: HolidayPreferences, + private val localDataSource: HolidayDao, + private val remoteDataSource: HolidayService, +) : HolidayRepository { + override fun findHoliday(year: Int, month: Month): Flow> { + val localDate = LocalDate(year, month, 1) + val list = IntRange(-1, 1).map { localDate.plus(it, DateTimeUnit.MONTH) } + + return channelFlow { + launch { fetch(list) } + + list.map { localDataSource.findHoliday(it.year, it.month) } + .combine { array -> array.flatMap { it } } + .mapLatest { it.zipHoliday() } + .mapLatest { it.filter(year, month) } + .mapCollectionLatest { it.prettyName() } + .collectLatest { send(it) } + } + } + + private fun List.zipHoliday(): List { + val comparator = Comparator { a, b -> + when { + a.start != b.start -> compareValues(a.start, b.start) + a.endInclusive != b.endInclusive -> compareValues(a.endInclusive, b.endInclusive) + else -> compareValues(a.name, b.name) + } + } + val sorted = distinct().sortedWith(comparator).toMutableList() + + return buildList { + while (sorted.isNotEmpty()) { + if (isEmpty()) { + add(sorted.removeFirst()) + continue + } + + val isNameSame = last().name == sorted.first().name + val isUntilOneDay = last().endInclusive.daysUntil(sorted.first().start) <= 1 + + if (isNameSame && isUntilOneDay) { + val origin = removeLast() + val target = sorted.removeFirst() + val result = origin.copy( + start = minOf(origin.start, target.start), + endInclusive = maxOf(origin.endInclusive, target.endInclusive), + ) + + add(result) + } else { + add(sorted.removeFirst()) + } + } + } + } + + private fun List.filter(year: Int, month: Month): List { + return filter { + val start = LocalDate(year, month, 1) + val endInclusive = start.plus(1, DateTimeUnit.MONTH).minus(1, DateTimeUnit.DAY) + + it.isOverlap(start..endInclusive) + } + } + + private fun Holiday.prettyName(): Holiday { + return copy(name = name.toPrettyName()) + } + + private fun String.toPrettyName(): String { + return when (this) { + "1월1일" -> "새해" + "기독탄신일" -> "크리스마스" + else -> this + } + } + + private suspend fun fetch(list: List) { + mutex.withLock { + coroutineScope { + list.map { + async { + runCatching { + if (!preferencesDataSource.isDirty(it.year, it.month).first()) { + localDataSource.upsert(it.year, it.month, remoteDataSource.findHoliday(it.year, it.month)) + preferencesDataSource.setDirty(it.year, it.month) + } + } + } + }.awaitAll() + } + } + } + + companion object { + private val mutex = Mutex() + } +} diff --git a/app/data/memo/README.md b/app/data/memo/README.md new file mode 100644 index 00000000..9c5178d3 --- /dev/null +++ b/app/data/memo/README.md @@ -0,0 +1,3 @@ +# :app:data:memo module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_data_memo.svg) diff --git a/app/data/memo/build.gradle.kts b/app/data/memo/build.gradle.kts new file mode 100644 index 00000000..31002b1c --- /dev/null +++ b/app/data/memo/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("diary.app.data") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:diary-database")) + implementation(project(":app:domain:memo")) + } + } + } +} diff --git a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt new file mode 100644 index 00000000..2b9a7775 --- /dev/null +++ b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.memo + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class MemoDataModule diff --git a/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt new file mode 100644 index 00000000..e72bf0fa --- /dev/null +++ b/app/data/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt @@ -0,0 +1,70 @@ +package io.github.taetae98coding.diary.data.memo.repository + +import io.github.taetae98coding.diary.core.diary.database.MemoBackupDao +import io.github.taetae98coding.diary.core.diary.database.MemoDao +import io.github.taetae98coding.diary.core.model.mapper.toMemo +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.model.memo.MemoDto +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.LocalDate +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +internal class MemoRepositoryImpl( + private val localDataSource: MemoDao, + private val backupDataSource: MemoBackupDao, +) : MemoRepository { + override suspend fun upsert(uid: String?, memo: Memo) { + val dto = MemoDto( + id = memo.id, + detail = memo.detail, + owner = memo.owner, + isFinish = memo.isFinish, + isDelete = memo.isDelete, + updateAt = memo.updateAt, + serverUpdateAt = null, + ) + + localDataSource.upsert(dto) + if (!uid.isNullOrBlank()) { + backupDataSource.upsert(uid, memo.id) + } + } + + override suspend fun update(uid: String?, memoId: String, detail: MemoDetail) { + localDataSource.update(memoId, detail) + if (!uid.isNullOrBlank()) { + backupDataSource.upsert(uid, memoId) + } + } + + override suspend fun updateFinish(uid: String?, memoId: String, isFinish: Boolean) { + localDataSource.updateFinish(memoId, isFinish) + if (!uid.isNullOrBlank()) { + backupDataSource.upsert(uid, memoId) + } + } + + override suspend fun updateDelete(uid: String?, memoId: String, isDelete: Boolean) { + localDataSource.updateDelete(memoId, isDelete) + if (!uid.isNullOrBlank()) { + backupDataSource.upsert(uid, memoId) + } + } + + override fun find(memoId: String): Flow { + return localDataSource.find(memoId) + .mapLatest { it?.toMemo() } + } + + override fun findByDateRange(owner: String?, dateRange: ClosedRange): Flow> { + return localDataSource.findByDateRange(owner, dateRange) + .mapCollectionLatest(MemoDto::toMemo) + } +} diff --git a/app/domain/account/README.md b/app/domain/account/README.md new file mode 100644 index 00000000..cffeb1b6 --- /dev/null +++ b/app/domain/account/README.md @@ -0,0 +1,3 @@ +# :app:domain:account module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_domain_account.svg) diff --git a/app/domain/account/build.gradle.kts b/app/domain/account/build.gradle.kts new file mode 100644 index 00000000..9df8cde7 --- /dev/null +++ b/app/domain/account/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.app.domain") +} diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt new file mode 100644 index 00000000..1c24f4b0 --- /dev/null +++ b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.account + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class AccountDomainModule diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt new file mode 100644 index 00000000..63bb8ea0 --- /dev/null +++ b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.domain.account.repository + +import io.github.taetae98coding.diary.core.model.account.AccountToken +import kotlinx.coroutines.flow.Flow + +public interface AccountRepository { + public suspend fun join(email: String, password: String) + public suspend fun save(email: String, token: AccountToken) + public suspend fun clear() + + public fun fetchToken(email: String, password: String): Flow + public fun getEmail(): Flow + public fun getUid(): Flow +} diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt new file mode 100644 index 00000000..3efe78c8 --- /dev/null +++ b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt @@ -0,0 +1,39 @@ +package io.github.taetae98coding.diary.domain.account.usecase + +import io.github.taetae98coding.diary.core.model.account.Account +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class GetAccountUseCase( + private val repository: AccountRepository +) { + public operator fun invoke(): Flow> { + return flow { + combine( + repository.getEmail(), + repository.getUid() + ) { email, uid -> + if (email == null || uid == null) { + Account.Guest + } else { + Account.Member(email, uid) + } + }.also { + emitAll(it) + } + }.mapLatest { + Result.success(it) + }.catch { + emit(Result.failure(it)) + } + } +} diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt new file mode 100644 index 00000000..d8e2acc2 --- /dev/null +++ b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.domain.account.usecase + +import io.github.taetae98coding.diary.common.exception.account.InvalidEmailException +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import io.github.taetae98coding.diary.library.kotlin.regex.email +import org.koin.core.annotation.Factory + +@Factory +public class JoinUseCase internal constructor( + private val repository: AccountRepository, +) { + public suspend operator fun invoke(email: String, password: String): Result { + return runCatching { + if (!email.contains(Regex.email())) throw InvalidEmailException() + + repository.join(email, password) + } + } +} diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt new file mode 100644 index 00000000..c7fd3ca7 --- /dev/null +++ b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LoginUseCase.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.domain.account.usecase + +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class LoginUseCase internal constructor( + private val repository: AccountRepository, +) { + public suspend operator fun invoke(email: String, password: String): Result { + return runCatching { + val token = repository.fetchToken(email, password).first() + + repository.save(email = email, token = token) + } + } +} diff --git a/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt new file mode 100644 index 00000000..75fe3e98 --- /dev/null +++ b/app/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/LogoutUseCase.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.domain.account.usecase + +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import org.koin.core.annotation.Factory + +@Factory +public class LogoutUseCase internal constructor( + private val repository: AccountRepository, +) { + public suspend operator fun invoke(): Result { + return runCatching { repository.clear() } + } +} diff --git a/app/domain/backup/README.md b/app/domain/backup/README.md new file mode 100644 index 00000000..dd8a61b3 --- /dev/null +++ b/app/domain/backup/README.md @@ -0,0 +1,3 @@ +# :app:domain:backup module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_domain_backup.svg) diff --git a/app/domain/backup/build.gradle.kts b/app/domain/backup/build.gradle.kts new file mode 100644 index 00000000..84e3b1c2 --- /dev/null +++ b/app/domain/backup/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.app.domain") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:account")) + } + } + } +} diff --git a/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/BackupDomainModule.kt b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/BackupDomainModule.kt new file mode 100644 index 00000000..0ece9e01 --- /dev/null +++ b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/BackupDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.backup + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class BackupDomainModule diff --git a/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt new file mode 100644 index 00000000..38f5474a --- /dev/null +++ b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/repository/BackupRepository.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.domain.backup.repository + +import kotlinx.coroutines.flow.Flow + +public interface BackupRepository { + public suspend fun backupMemo(uid: String) + + public fun getUpdateFlow(uid: String): Flow + public fun countBackupMemo(uid: String): Flow +} diff --git a/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt new file mode 100644 index 00000000..792c6244 --- /dev/null +++ b/app/domain/backup/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/backup/usecase/BackupUseCase.kt @@ -0,0 +1,39 @@ +package io.github.taetae98coding.diary.domain.backup.usecase + +import io.github.taetae98coding.diary.core.model.account.Account +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.backup.repository.BackupRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class BackupUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: BackupRepository, +) { + public suspend operator fun invoke(): Result { + return runCatching { + getAccountUseCase().mapLatest { it.getOrNull() } + .flatMapLatest { account -> + if (account is Account.Member) { + repository.getUpdateFlow(account.uid) + .mapLatest { account.uid } + } else { + emptyFlow() + } + } + .collectLatest { uid -> + runCatching { backup(uid) } + } + } + } + + private suspend fun backup(uid: String) { + repository.backupMemo(uid) + } +} diff --git a/app/domain/fetch/README.md b/app/domain/fetch/README.md new file mode 100644 index 00000000..e4e4a67a --- /dev/null +++ b/app/domain/fetch/README.md @@ -0,0 +1,3 @@ +# :app:domain:fetch module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_domain_fetch.svg) diff --git a/app/domain/fetch/build.gradle.kts b/app/domain/fetch/build.gradle.kts new file mode 100644 index 00000000..84e3b1c2 --- /dev/null +++ b/app/domain/fetch/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.app.domain") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:account")) + } + } + } +} diff --git a/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/FetchDomainModule.kt b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/FetchDomainModule.kt new file mode 100644 index 00000000..152eb94e --- /dev/null +++ b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/FetchDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.fetch + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class FetchDomainModule diff --git a/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/repository/FetchRepository.kt b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/repository/FetchRepository.kt new file mode 100644 index 00000000..8d8b243c --- /dev/null +++ b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/repository/FetchRepository.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.domain.fetch.repository + +public interface FetchRepository { + public suspend fun fetchMemo(uid: String) +} diff --git a/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt new file mode 100644 index 00000000..47e67611 --- /dev/null +++ b/app/domain/fetch/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/fetch/usecase/FetchUseCase.kt @@ -0,0 +1,31 @@ +package io.github.taetae98coding.diary.domain.fetch.usecase + +import io.github.taetae98coding.diary.core.model.account.Account +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.fetch.repository.FetchRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FetchUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: FetchRepository, +) { + public suspend operator fun invoke(): Result { + return runCatching { + getAccountUseCase().mapLatest { it.getOrNull() } + .collectLatest { account -> + if (account is Account.Member) { + runCatching { fetch(account.uid) } + } + } + } + } + + private suspend fun fetch(uid: String) { + repository.fetchMemo(uid) + } +} diff --git a/app/domain/holiday/README.md b/app/domain/holiday/README.md new file mode 100644 index 00000000..84d334f1 --- /dev/null +++ b/app/domain/holiday/README.md @@ -0,0 +1,3 @@ +# :app:domain:holiday module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_domain_holiday.svg) diff --git a/app/domain/holiday/build.gradle.kts b/app/domain/holiday/build.gradle.kts new file mode 100644 index 00000000..9df8cde7 --- /dev/null +++ b/app/domain/holiday/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.app.domain") +} diff --git a/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/HolidayDomainModule.kt b/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/HolidayDomainModule.kt new file mode 100644 index 00000000..7b3d93a0 --- /dev/null +++ b/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/HolidayDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.holiday + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class HolidayDomainModule diff --git a/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/repository/HolidayRepository.kt b/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/repository/HolidayRepository.kt new file mode 100644 index 00000000..6ae827d1 --- /dev/null +++ b/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/repository/HolidayRepository.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.domain.holiday.repository + +import io.github.taetae98coding.diary.core.model.holiday.Holiday +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Month + +public interface HolidayRepository { + public fun findHoliday(year: Int, month: Month): Flow> +} diff --git a/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/usecase/FindHolidayUseCase.kt b/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/usecase/FindHolidayUseCase.kt new file mode 100644 index 00000000..1a135f50 --- /dev/null +++ b/app/domain/holiday/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/holiday/usecase/FindHolidayUseCase.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary.domain.holiday.usecase + +import io.github.taetae98coding.diary.core.model.holiday.Holiday +import io.github.taetae98coding.diary.domain.holiday.repository.HolidayRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.Month +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FindHolidayUseCase internal constructor( + private val repository: HolidayRepository, +) { + public operator fun invoke(year: Int, month: Month): Flow>> { + return flow { emitAll(repository.findHoliday(year, month)) } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } + } +} diff --git a/app/domain/memo/README.md b/app/domain/memo/README.md new file mode 100644 index 00000000..bc65080d --- /dev/null +++ b/app/domain/memo/README.md @@ -0,0 +1,3 @@ +# :app:domain:memo module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_domain_memo.svg) diff --git a/app/domain/memo/build.gradle.kts b/app/domain/memo/build.gradle.kts new file mode 100644 index 00000000..84e3b1c2 --- /dev/null +++ b/app/domain/memo/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.app.domain") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:account")) + } + } + } +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt new file mode 100644 index 00000000..94600e87 --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.memo + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class MemoDomainModule diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt new file mode 100644 index 00000000..3a33d8bc --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.domain.memo.repository + +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.LocalDate + +public interface MemoRepository { + public suspend fun upsert(uid: String?, memo: Memo) + public suspend fun update(uid: String?, memoId: String, detail: MemoDetail) + public suspend fun updateFinish(uid: String?, memoId: String, isFinish: Boolean) + public suspend fun updateDelete(uid: String?, memoId: String, isDelete: Boolean) + + public fun find(memoId: String): Flow + public fun findByDateRange(owner: String?, dateRange: ClosedRange): Flow> +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt new file mode 100644 index 00000000..7ae5cee3 --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/AddMemoUseCase.kt @@ -0,0 +1,40 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.common.exception.memo.MemoTitleBlankException +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +import kotlinx.coroutines.flow.first +import kotlinx.datetime.Clock +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalUuidApi::class) +@Factory +public class AddMemoUseCase internal constructor( + private val clock: Clock, + private val getAccountUseCase: GetAccountUseCase, + private val repository: MemoRepository, +) { + public suspend operator fun invoke(detail: MemoDetail): Result { + return runCatching { + if (detail.title.isBlank()) throw MemoTitleBlankException() + + val account = getAccountUseCase().first().getOrThrow() + val id = Uuid.random().toString() + + val memo = Memo( + id = id, + detail = detail, + owner = account.uid, + isFinish = false, + isDelete = false, + updateAt = clock.now(), + ) + + repository.upsert(account.uid, memo) + } + } +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt new file mode 100644 index 00000000..f223acc8 --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/DeleteMemoUseCase.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class DeleteMemoUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: MemoRepository, +) { + public suspend operator fun invoke(memoId: String?): Result { + return runCatching { + if (memoId.isNullOrBlank()) return@runCatching + + val account = getAccountUseCase().first().getOrThrow() + repository.updateDelete(account.uid, memoId, true) + } + } +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindCalendarMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindCalendarMemoUseCase.kt new file mode 100644 index 00000000..0cf3d8b4 --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindCalendarMemoUseCase.kt @@ -0,0 +1,33 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.LocalDate +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FindCalendarMemoUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: MemoRepository, +) { + public operator fun invoke(dateRange: ClosedRange): Flow>> { + return flow { + getAccountUseCase().mapLatest { it.getOrThrow() } + .flatMapLatest { repository.findByDateRange(it.uid, dateRange) } + .also { emitAll(it) } + }.mapLatest { + Result.success(it) + }.catch { + emit(Result.failure(it)) + } + } +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt new file mode 100644 index 00000000..16c1216b --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FindMemoUseCase.kt @@ -0,0 +1,26 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.core.model.memo.Memo +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapLatest +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FindMemoUseCase internal constructor( + private val repository: MemoRepository, +) { + public operator fun invoke(memoId: String?): Flow> { + if (memoId.isNullOrBlank()) return flowOf(Result.success(null)) + + return flow { emitAll(repository.find(memoId)) } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } + } +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt new file mode 100644 index 00000000..f50e2376 --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FinishMemoUseCase.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class FinishMemoUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: MemoRepository, +) { + public suspend operator fun invoke(memoId: String?): Result { + return runCatching { + if (memoId.isNullOrBlank()) return@runCatching + + val account = getAccountUseCase().first().getOrThrow() + repository.updateFinish(account.uid, memoId, true) + } + } +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt new file mode 100644 index 00000000..66e7abdf --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/RestartMemoUseCase.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class RestartMemoUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: MemoRepository, +) { + public suspend operator fun invoke(memoId: String?): Result { + return runCatching { + if (memoId.isNullOrBlank()) return@runCatching + + val account = getAccountUseCase().first().getOrThrow() + repository.updateFinish(account.uid, memoId, false) + } + } +} diff --git a/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt new file mode 100644 index 00000000..81fa18bd --- /dev/null +++ b/app/domain/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpdateMemoUseCase.kt @@ -0,0 +1,27 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class UpdateMemoUseCase internal constructor( + private val getAccountUseCase: GetAccountUseCase, + private val repository: MemoRepository, +) { + public suspend operator fun invoke(memoId: String?, detail: MemoDetail): Result { + return runCatching { + if (memoId.isNullOrBlank()) return@runCatching + + val memo = repository.find(memoId).first() ?: return@runCatching + val account = getAccountUseCase().first().getOrThrow() + val validDetail = detail.copy(title = detail.title.ifBlank { memo.detail.title }) + + if (memo.detail != validDetail) { + repository.update(account.uid, memoId, validDetail) + } + } + } +} diff --git a/app/feature/account/README.md b/app/feature/account/README.md new file mode 100644 index 00000000..160545df --- /dev/null +++ b/app/feature/account/README.md @@ -0,0 +1,3 @@ +# :app:feature:account module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_feature_account.svg) diff --git a/app/feature/account/build.gradle.kts b/app/feature/account/build.gradle.kts new file mode 100644 index 00000000..57f5f7c9 --- /dev/null +++ b/app/feature/account/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("diary.app.feature") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:account")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.feature.account" +} diff --git a/app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/JoinScreenPreview.kt b/app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/JoinScreenPreview.kt new file mode 100644 index 00000000..b9eaaa14 --- /dev/null +++ b/app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/JoinScreenPreview.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.feature.account + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.feature.account.join.JoinScreen +import io.github.taetae98coding.diary.feature.account.join.state.JoinUiState +import io.github.taetae98coding.diary.feature.account.join.state.rememberJoinScreenState + +@Composable +@DiaryPreview +private fun JoinScreenPreview() { + DiaryTheme { + JoinScreen( + state = rememberJoinScreenState(), + onNavigateUp = {}, + onJoin = {}, + uiStateProvider = { JoinUiState() }, + onLoginFinish = {} + ) + } +} diff --git a/app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/LoginScreenPreview.kt b/app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/LoginScreenPreview.kt new file mode 100644 index 00000000..bccbed4d --- /dev/null +++ b/app/feature/account/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/account/LoginScreenPreview.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.feature.account + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.feature.account.login.LoginScreen +import io.github.taetae98coding.diary.feature.account.login.state.LoginUiState +import io.github.taetae98coding.diary.feature.account.login.state.rememberLoginScreenState + +@Composable +@DiaryPreview +private fun LoginScreenPreview() { + DiaryTheme { + LoginScreen( + state = rememberLoginScreenState(), + onNavigateUp = {}, + onLogin = {}, + uiStateProvider = { LoginUiState() }, + onLoginFinish = {}, + ) + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountFeatureModule.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountFeatureModule.kt new file mode 100644 index 00000000..7967210d --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountFeatureModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.feature.account + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class AccountFeatureModule diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountNavigation.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountNavigation.kt new file mode 100644 index 00000000..e0d5d84e --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/AccountNavigation.kt @@ -0,0 +1,25 @@ +package io.github.taetae98coding.diary.feature.account + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import io.github.taetae98coding.diary.core.navigation.account.JoinDestination +import io.github.taetae98coding.diary.core.navigation.account.LoginDestination +import io.github.taetae98coding.diary.feature.account.join.JoinRoute +import io.github.taetae98coding.diary.feature.account.login.LoginRoute + +public fun NavGraphBuilder.accountNavigation( + navController: NavController, +) { + composable { + JoinRoute( + navigateUp = navController::popBackStack, + ) + } + + composable { + LoginRoute( + navigateUp = navController::popBackStack, + ) + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BasePasswordTextField.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BasePasswordTextField.kt new file mode 100644 index 00000000..8197641d --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BasePasswordTextField.kt @@ -0,0 +1,53 @@ +package io.github.taetae98coding.diary.feature.account.common + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import io.github.taetae98coding.diary.core.design.system.icon.VisibilityOffIcon +import io.github.taetae98coding.diary.core.design.system.icon.VisibilityOnIcon +import io.github.taetae98coding.diary.core.design.system.text.ClearTextField +import io.github.taetae98coding.diary.core.resources.icon.KeyIcon + +@Composable +internal fun BasePasswordTextField( + valueProvider: () -> String, + onValueChange: (String) -> Unit, + placeholder: @Composable () -> Unit, + passwordVisibleProvider: () -> Boolean, + onPasswordVisibleChange: (Boolean) -> Unit, + keyboardOptions: KeyboardOptions, + modifier: Modifier = Modifier, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + ClearTextField( + valueProvider = valueProvider, + onValueChange = onValueChange, + modifier = modifier, + placeholder = placeholder, + leadingIcon = { KeyIcon() }, + trailingIcon = { + IconButton(onClick = { onPasswordVisibleChange(!passwordVisibleProvider()) }) { + Crossfade(passwordVisibleProvider()) { isVisible -> + if (isVisible) { + VisibilityOnIcon() + } else { + VisibilityOffIcon() + } + } + } + }, + visualTransformation = if (passwordVisibleProvider()) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = true, + ) +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BottomBarButton.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BottomBarButton.kt new file mode 100644 index 00000000..cf61d236 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/BottomBarButton.kt @@ -0,0 +1,61 @@ +package io.github.taetae98coding.diary.feature.account.common + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme + +@Composable +internal fun BottomBarButton( + onClick: () -> Unit, + enableProvider: () -> Boolean, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit, +) { + val animatedButtonColor by animateColorAsState( + targetValue = if (enableProvider()) { + DiaryTheme.color.primary + } else { + DiaryTheme.color.onSurface.copy(alpha = 0.12F) + }, + ) + + Button( + onClick = onClick, + modifier = modifier, + enabled = enableProvider(), + shape = RectangleShape, + colors = ButtonDefaults.buttonColors( + containerColor = animatedButtonColor, + disabledContainerColor = animatedButtonColor, + ), + content = content, + ) +} + +@Composable +internal fun BottomBarButtonContent( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Box( + modifier = modifier.fillMaxWidth() + .height(50.dp) + .windowInsetsPadding(NavigationBarDefaults.windowInsets), + contentAlignment = Alignment.Center, + content = content, + ) +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/EmailTextField.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/EmailTextField.kt new file mode 100644 index 00000000..8db3d2a1 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/common/EmailTextField.kt @@ -0,0 +1,45 @@ +package io.github.taetae98coding.diary.feature.account.common + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import io.github.taetae98coding.diary.core.design.system.text.ClearTextField +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.email +import io.github.taetae98coding.diary.core.resources.icon.EmailIcon +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun EmailTextField( + valueProvider: () -> String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + + ClearTextField( + valueProvider = valueProvider, + onValueChange = onValueChange, + modifier = modifier.focusRequester(focusRequester), + placeholder = { Text(text = stringResource(Res.string.email)) }, + leadingIcon = { EmailIcon() }, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next, + ), + singleLine = true, + ) + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinRoute.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinRoute.kt new file mode 100644 index 00000000..f5ec6324 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinRoute.kt @@ -0,0 +1,27 @@ +package io.github.taetae98coding.diary.feature.account.join + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.feature.account.join.state.rememberJoinScreenState +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun JoinRoute( + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + joinViewModel: JoinViewModel = koinViewModel(), +) { + val state = rememberJoinScreenState() + val uiState by joinViewModel.uiState.collectAsStateWithLifecycle() + + JoinScreen( + state = state, + onNavigateUp = navigateUp, + onJoin = { joinViewModel.join(state.email, state.password) }, + uiStateProvider = { uiState }, + onLoginFinish = navigateUp, + modifier = modifier, + ) +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt new file mode 100644 index 00000000..4ac44ce7 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinScreen.kt @@ -0,0 +1,248 @@ +package io.github.taetae98coding.diary.feature.account.join + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.bottom_button_email_blank +import io.github.taetae98coding.diary.core.resources.bottom_button_password_blank +import io.github.taetae98coding.diary.core.resources.check_password +import io.github.taetae98coding.diary.core.resources.icon.NavigateUpIcon +import io.github.taetae98coding.diary.core.resources.join +import io.github.taetae98coding.diary.core.resources.join_button_invalid_email_message +import io.github.taetae98coding.diary.core.resources.join_button_message +import io.github.taetae98coding.diary.core.resources.join_button_password_different_message +import io.github.taetae98coding.diary.core.resources.join_exist_email_message +import io.github.taetae98coding.diary.core.resources.network_error +import io.github.taetae98coding.diary.core.resources.password +import io.github.taetae98coding.diary.core.resources.unknown_error +import io.github.taetae98coding.diary.feature.account.common.BasePasswordTextField +import io.github.taetae98coding.diary.feature.account.common.BottomBarButton +import io.github.taetae98coding.diary.feature.account.common.BottomBarButtonContent +import io.github.taetae98coding.diary.feature.account.common.EmailTextField +import io.github.taetae98coding.diary.feature.account.join.state.JoinScreenButtonUiState +import io.github.taetae98coding.diary.feature.account.join.state.JoinScreenState +import io.github.taetae98coding.diary.feature.account.join.state.JoinUiState +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun JoinScreen( + state: JoinScreenState, + onNavigateUp: () -> Unit, + onJoin: () -> Unit, + uiStateProvider: () -> JoinUiState, + onLoginFinish: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(text = stringResource(Res.string.join)) }, + navigationIcon = { + IconButton(onClick = onNavigateUp) { + NavigateUpIcon() + } + }, + ) + }, + bottomBar = { + val isEnable by remember { + derivedStateOf { state.buttonState == JoinScreenButtonUiState.JoinEnable } + } + val isProgress by remember { + derivedStateOf { uiStateProvider().isProgress } + } + + BottomBarButton( + onClick = onJoin, + enableProvider = { isEnable }, + modifier = Modifier.fillMaxWidth(), + ) { + JoinButtonContent( + uiState = if (isProgress) { + JoinScreenButtonUiState.Progress + } else { + state.buttonState + }, + ) + } + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + ) { + Content( + state = state, + onJoin = onJoin, + modifier = Modifier.padding(it) + .padding(DiaryTheme.dimen.screenPaddingValues), + ) + } + + Message( + state = state, + uiStateProvider = uiStateProvider, + onLoginFinish = onLoginFinish, + ) +} + +@Composable +private fun JoinButtonContent( + uiState: JoinScreenButtonUiState, + modifier: Modifier = Modifier, +) { + BottomBarButtonContent(modifier = modifier) { + when (uiState) { + JoinScreenButtonUiState.JoinEnable -> { + Text(text = stringResource(Res.string.join_button_message)) + } + + JoinScreenButtonUiState.EmailBlank -> { + Text(text = stringResource(Res.string.bottom_button_email_blank)) + } + + JoinScreenButtonUiState.PasswordBlank -> { + Text(text = stringResource(Res.string.bottom_button_password_blank)) + } + + JoinScreenButtonUiState.InvalidEmail -> { + Text(text = stringResource(Res.string.join_button_invalid_email_message)) + } + + JoinScreenButtonUiState.PasswordDifferent -> { + Text(text = stringResource(Res.string.join_button_password_different_message)) + } + + JoinScreenButtonUiState.Progress -> { + CircularProgressIndicator(color = LocalContentColor.current) + } + } + } +} + +@Composable +private fun Content( + state: JoinScreenState, + onJoin: () -> Unit, + modifier: Modifier = Modifier, +) { + Card(modifier = modifier) { + val textFieldModifier = Modifier.fillMaxWidth() + + EmailTextField( + valueProvider = { state.email }, + onValueChange = state::onEmailChange, + modifier = textFieldModifier, + ) + + PasswordTextField( + state = state, + modifier = textFieldModifier, + ) + CheckPasswordTextField( + state = state, + onJoin = onJoin, + modifier = textFieldModifier, + ) + } +} + + +@Composable +private fun PasswordTextField( + state: JoinScreenState, + modifier: Modifier = Modifier, +) { + BasePasswordTextField( + valueProvider = { state.password }, + onValueChange = state::onPasswordChange, + modifier = modifier, + placeholder = { Text(text = stringResource(Res.string.password)) }, + passwordVisibleProvider = { state.isPasswordVisible }, + onPasswordVisibleChange = state::onPasswordVisibleChange, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Next, + ), + ) +} + +@Composable +private fun CheckPasswordTextField( + state: JoinScreenState, + onJoin: () -> Unit, + modifier: Modifier = Modifier, +) { + BasePasswordTextField( + valueProvider = { state.checkPassword }, + onValueChange = state::onCheckPasswordChange, + modifier = modifier, + placeholder = { Text(text = stringResource(Res.string.check_password)) }, + passwordVisibleProvider = { state.isCheckPasswordVisible }, + onPasswordVisibleChange = state::onCheckPasswordVisibleChange, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onAny = { + if (state.buttonState == JoinScreenButtonUiState.JoinEnable) { + onJoin() + } + }, + ), + ) +} + + +@Composable +private fun Message( + state: JoinScreenState, + uiStateProvider: () -> JoinUiState, + onLoginFinish: () -> Unit, +) { + val uiState = uiStateProvider() + val existEmailMessage = stringResource(Res.string.join_exist_email_message) + val networkErrorMessage = stringResource(Res.string.network_error) + val unknownErrorMessage = stringResource(Res.string.unknown_error) + + LaunchedEffect( + uiState.isLoginFinish, + uiState.isExistEmail, + uiState.isNetworkError, + uiState.isUnknownError, + ) { + if (!uiState.hasMessage) return@LaunchedEffect + + when { + uiState.isLoginFinish -> onLoginFinish() + uiState.isExistEmail -> state.showMessage(existEmailMessage) + uiState.isNetworkError -> state.showMessage(networkErrorMessage) + uiState.isUnknownError -> state.showMessage(unknownErrorMessage) + } + + uiState.onMessageShow() + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt new file mode 100644 index 00000000..3664738e --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/JoinViewModel.kt @@ -0,0 +1,60 @@ +package io.github.taetae98coding.diary.feature.account.join + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.common.exception.NetworkException +import io.github.taetae98coding.diary.common.exception.account.ExistEmailException +import io.github.taetae98coding.diary.domain.account.usecase.JoinUseCase +import io.github.taetae98coding.diary.domain.account.usecase.LoginUseCase +import io.github.taetae98coding.diary.feature.account.join.state.JoinUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel + +@KoinViewModel +internal class JoinViewModel( + private val joinUseCase: JoinUseCase, + private val loginUseCase: LoginUseCase, +) : ViewModel() { + private val _uiState = MutableStateFlow(JoinUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() + + fun join(email: String, password: String) { + if (uiState.value.isProgress) return + + viewModelScope.launch { + _uiState.update { it.copy(isProgress = true) } + joinUseCase(email, password) + .onSuccess { login(email, password) } + .onFailure { handleThrowable(it) } + } + } + + private fun login(email: String, password: String) { + viewModelScope.launch { + loginUseCase(email, password) + _uiState.update { it.copy(isProgress = false, isLoginFinish = true) } + } + } + + private fun handleThrowable(throwable: Throwable) { + when (throwable) { + is ExistEmailException -> _uiState.update { it.copy(isProgress = false, isExistEmail = true) } + is NetworkException -> _uiState.update { it.copy(isProgress = false, isNetworkError = true) } + else -> _uiState.update { it.copy(isProgress = false, isUnknownError = true) } + } + } + + private fun clearMessage() { + _uiState.update { + it.copy( + isLoginFinish = false, + isExistEmail = false, + isNetworkError = false, + isUnknownError = false, + ) + } + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenButtonUiState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenButtonUiState.kt new file mode 100644 index 00000000..4220d701 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenButtonUiState.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.feature.account.join.state + +internal enum class JoinScreenButtonUiState { + JoinEnable, EmailBlank, PasswordBlank, InvalidEmail, PasswordDifferent, Progress +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenState.kt new file mode 100644 index 00000000..93c9cf2a --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinScreenState.kt @@ -0,0 +1,88 @@ +package io.github.taetae98coding.diary.feature.account.join.state + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.setValue +import io.github.taetae98coding.diary.library.kotlin.regex.email +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +internal class JoinScreenState( + val coroutineScope: CoroutineScope, +) { + private var messageJob: Job? = null + val hostState: SnackbarHostState = SnackbarHostState() + + var email by mutableStateOf("") + private set + + var isPasswordVisible by mutableStateOf(false) + private set + + var password by mutableStateOf("") + private set + + var isCheckPasswordVisible by mutableStateOf(false) + + var checkPassword by mutableStateOf("") + private set + + val buttonState by derivedStateOf { + if (email.isBlank()) { + JoinScreenButtonUiState.EmailBlank + } else if (password.isBlank()) { + JoinScreenButtonUiState.PasswordBlank + } else if (!email.contains(Regex.email())) { + JoinScreenButtonUiState.InvalidEmail + } else if (password != checkPassword) { + JoinScreenButtonUiState.PasswordDifferent + } else { + JoinScreenButtonUiState.JoinEnable + } + } + + fun onEmailChange(value: String) { + email = value + } + + fun onPasswordVisibleChange(value: Boolean) { + isPasswordVisible = value + } + + fun onPasswordChange(value: String) { + password = value + } + + fun onCheckPasswordVisibleChange(value: Boolean) { + isCheckPasswordVisible = value + } + + fun onCheckPasswordChange(value: String) { + checkPassword = value + } + + fun showMessage(message: String) { + messageJob?.cancel() + messageJob = coroutineScope.launch { hostState.showSnackbar(message) } + } + + companion object { + fun saver(coroutineScope: CoroutineScope): Saver { + return listSaver( + save = { listOf(it.email, it.password, it.checkPassword) }, + restore = { + JoinScreenState(coroutineScope).apply { + email = it[0] + password = it[1] + checkPassword = it[2] + } + }, + ) + } + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinUiState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinUiState.kt new file mode 100644 index 00000000..9c316718 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/JoinUiState.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.feature.account.join.state + +internal data class JoinUiState( + val isProgress: Boolean = false, + val isLoginFinish: Boolean = false, + val isExistEmail: Boolean = false, + val isNetworkError: Boolean = false, + val isUnknownError: Boolean = false, + val onMessageShow: () -> Unit = {}, +) { + val hasMessage = isLoginFinish || isExistEmail || isNetworkError || isUnknownError +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/RememberJoinScreenState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/RememberJoinScreenState.kt new file mode 100644 index 00000000..da1c46ec --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/join/state/RememberJoinScreenState.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.feature.account.join.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable + +@Composable +internal fun rememberJoinScreenState(): JoinScreenState { + val coroutineScope = rememberCoroutineScope() + + return rememberSaveable( + saver = JoinScreenState.saver(coroutineScope), + ) { + JoinScreenState(coroutineScope) + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginRoute.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginRoute.kt new file mode 100644 index 00000000..b31099de --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginRoute.kt @@ -0,0 +1,27 @@ +package io.github.taetae98coding.diary.feature.account.login + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.feature.account.login.state.rememberLoginScreenState +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun LoginRoute( + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + viewModel: LoginViewModel = koinViewModel(), +) { + val state = rememberLoginScreenState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LoginScreen( + state = state, + onNavigateUp = navigateUp, + onLogin = { viewModel.login(state.email, state.password) }, + uiStateProvider = { uiState }, + onLoginFinish = navigateUp, + modifier = modifier, + ) +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt new file mode 100644 index 00000000..1143d792 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginScreen.kt @@ -0,0 +1,198 @@ +package io.github.taetae98coding.diary.feature.account.login + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.account_not_found_error +import io.github.taetae98coding.diary.core.resources.bottom_button_email_blank +import io.github.taetae98coding.diary.core.resources.bottom_button_password_blank +import io.github.taetae98coding.diary.core.resources.check_password +import io.github.taetae98coding.diary.core.resources.icon.NavigateUpIcon +import io.github.taetae98coding.diary.core.resources.login +import io.github.taetae98coding.diary.core.resources.login_button_message +import io.github.taetae98coding.diary.core.resources.network_error +import io.github.taetae98coding.diary.core.resources.unknown_error +import io.github.taetae98coding.diary.feature.account.common.BasePasswordTextField +import io.github.taetae98coding.diary.feature.account.common.BottomBarButton +import io.github.taetae98coding.diary.feature.account.common.BottomBarButtonContent +import io.github.taetae98coding.diary.feature.account.common.EmailTextField +import io.github.taetae98coding.diary.feature.account.login.state.LoginScreenButtonUiState +import io.github.taetae98coding.diary.feature.account.login.state.LoginScreenState +import io.github.taetae98coding.diary.feature.account.login.state.LoginUiState +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun LoginScreen( + state: LoginScreenState, + onNavigateUp: () -> Unit, + onLogin: () -> Unit, + uiStateProvider: () -> LoginUiState, + onLoginFinish: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(text = stringResource(Res.string.login)) }, + navigationIcon = { + IconButton(onClick = onNavigateUp) { + NavigateUpIcon() + } + }, + ) + }, + bottomBar = { + val isEnable by remember { + derivedStateOf { state.buttonState == LoginScreenButtonUiState.LoginEnable } + } + val isProgress by remember { + derivedStateOf { uiStateProvider().isProgress } + } + + BottomBarButton( + onClick = onLogin, + enableProvider = { isEnable }, + modifier = Modifier.fillMaxWidth(), + ) { + LoginButtonContent( + uiState = if (isProgress) { + LoginScreenButtonUiState.Progress + } else { + state.buttonState + }, + ) + } + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + ) { + Content( + state = state, + onLogin = onLogin, + modifier = Modifier.padding(it) + .padding(DiaryTheme.dimen.screenPaddingValues), + ) + } + + Message( + state = state, + uiStateProvider = uiStateProvider, + onLoginFinish = onLoginFinish, + ) +} + +@Composable +private fun Message( + state: LoginScreenState, + uiStateProvider: () -> LoginUiState, + onLoginFinish: () -> Unit, +) { + val uiState = uiStateProvider() + val accountNotFoundErrorMessage = stringResource(Res.string.account_not_found_error) + val networkErrorMessage = stringResource(Res.string.network_error) + val unknownErrorMessage = stringResource(Res.string.unknown_error) + + LaunchedEffect( + uiState.isLoginFinish, + uiState.isAccountNotFound, + uiState.isNetworkError, + uiState.isUnknownError, + ) { + if (!uiState.hasMessage) return@LaunchedEffect + + when { + uiState.isLoginFinish -> onLoginFinish() + uiState.isAccountNotFound -> state.showMessage(accountNotFoundErrorMessage) + uiState.isNetworkError -> state.showMessage(networkErrorMessage) + uiState.isUnknownError -> state.showMessage(unknownErrorMessage) + } + + uiState.onMessageShow() + } +} + +@Composable +private fun LoginButtonContent( + uiState: LoginScreenButtonUiState, + modifier: Modifier = Modifier, +) { + BottomBarButtonContent(modifier = modifier) { + when (uiState) { + LoginScreenButtonUiState.LoginEnable -> { + Text(text = stringResource(Res.string.login_button_message)) + } + + LoginScreenButtonUiState.EmailBlank -> { + Text(text = stringResource(Res.string.bottom_button_email_blank)) + } + + LoginScreenButtonUiState.PasswordBlank -> { + Text(text = stringResource(Res.string.bottom_button_password_blank)) + } + + LoginScreenButtonUiState.Progress -> { + CircularProgressIndicator(color = LocalContentColor.current) + } + } + } +} + +@Composable +private fun Content( + state: LoginScreenState, + onLogin: () -> Unit, + modifier: Modifier = Modifier, +) { + Card(modifier = modifier) { + val textFieldModifier = Modifier.fillMaxWidth() + + EmailTextField( + valueProvider = { state.email }, + onValueChange = state::onEmailChange, + modifier = textFieldModifier, + ) + + BasePasswordTextField( + valueProvider = { state.password }, + onValueChange = state::onPasswordChange, + modifier = textFieldModifier, + placeholder = { Text(text = stringResource(Res.string.check_password)) }, + passwordVisibleProvider = { state.isPasswordVisible }, + onPasswordVisibleChange = state::onPasswordVisibleChange, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onAny = { + if (state.buttonState == LoginScreenButtonUiState.LoginEnable) { + onLogin() + } + }, + ), + ) + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt new file mode 100644 index 00000000..6bbbc335 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/LoginViewModel.kt @@ -0,0 +1,51 @@ +package io.github.taetae98coding.diary.feature.account.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.common.exception.NetworkException +import io.github.taetae98coding.diary.common.exception.account.AccountNotFoundException +import io.github.taetae98coding.diary.domain.account.usecase.LoginUseCase +import io.github.taetae98coding.diary.feature.account.login.state.LoginUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel + +@KoinViewModel +internal class LoginViewModel( + private val loginUseCase: LoginUseCase, +) : ViewModel() { + private val _uiState = MutableStateFlow(LoginUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() + + fun login(email: String, password: String) { + if (uiState.value.isProgress) return + + viewModelScope.launch { + _uiState.update { it.copy(isProgress = true) } + loginUseCase(email, password) + .onSuccess { _uiState.update { it.copy(isProgress = false, isLoginFinish = true) } } + .onFailure { handleThrowable(it) } + } + } + + private fun handleThrowable(throwable: Throwable) { + when (throwable) { + is AccountNotFoundException -> _uiState.update { it.copy(isProgress = false, isAccountNotFound = true) } + is NetworkException -> _uiState.update { it.copy(isProgress = false, isNetworkError = true) } + else -> _uiState.update { it.copy(isProgress = false, isUnknownError = true) } + } + } + + private fun clearMessage() { + _uiState.update { + it.copy( + isLoginFinish = false, + isAccountNotFound = false, + isNetworkError = false, + isUnknownError = false, + ) + } + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenButtonUiState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenButtonUiState.kt new file mode 100644 index 00000000..31b8983c --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenButtonUiState.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.feature.account.login.state + +internal enum class LoginScreenButtonUiState { + LoginEnable, EmailBlank, PasswordBlank, Progress +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenState.kt new file mode 100644 index 00000000..f078a906 --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginScreenState.kt @@ -0,0 +1,69 @@ +package io.github.taetae98coding.diary.feature.account.login.state + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.setValue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +internal class LoginScreenState( + private val coroutineScope: CoroutineScope, +) { + private var messageJob: Job? = null + val hostState: SnackbarHostState = SnackbarHostState() + + var email by mutableStateOf("") + private set + var isPasswordVisible by mutableStateOf(false) + private set + var password by mutableStateOf("") + private set + + val buttonState by derivedStateOf { + if (email.isBlank()) { + LoginScreenButtonUiState.EmailBlank + } else if (password.isBlank()) { + LoginScreenButtonUiState.PasswordBlank + } else { + LoginScreenButtonUiState.LoginEnable + } + } + + fun onEmailChange(value: String) { + email = value + } + + fun onPasswordVisibleChange(value: Boolean) { + isPasswordVisible = value + } + + fun onPasswordChange(value: String) { + password = value + } + + fun showMessage(message: String) { + messageJob?.cancel() + messageJob = coroutineScope.launch { hostState.showSnackbar(message) } + } + + companion object { + fun saver( + coroutineScope: CoroutineScope, + ): Saver { + return listSaver( + save = { listOf(it.email, it.password) }, + restore = { + LoginScreenState(coroutineScope = coroutineScope).apply { + email = it[0] + password = it[1] + } + }, + ) + } + } +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginUiState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginUiState.kt new file mode 100644 index 00000000..a9a0980c --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/LoginUiState.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.feature.account.login.state + +internal data class LoginUiState( + val isProgress: Boolean = false, + val isLoginFinish: Boolean = false, + val isAccountNotFound: Boolean = false, + val isNetworkError: Boolean = false, + val isUnknownError: Boolean = false, + val onMessageShow: () -> Unit = {}, +) { + val hasMessage = isLoginFinish || isAccountNotFound || isNetworkError || isUnknownError +} diff --git a/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/RememberLoginScreenState.kt b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/RememberLoginScreenState.kt new file mode 100644 index 00000000..5739e81d --- /dev/null +++ b/app/feature/account/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/account/login/state/RememberLoginScreenState.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.feature.account.login.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable + +@Composable +internal fun rememberLoginScreenState(): LoginScreenState { + val coroutineScope = rememberCoroutineScope() + + return rememberSaveable(saver = LoginScreenState.saver(coroutineScope = coroutineScope)) { + LoginScreenState(coroutineScope = coroutineScope) + } +} diff --git a/app/feature/calendar/README.md b/app/feature/calendar/README.md new file mode 100644 index 00000000..2f77442b --- /dev/null +++ b/app/feature/calendar/README.md @@ -0,0 +1,3 @@ +# :app:feature:calendar module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_feature_calendar.svg) diff --git a/app/feature/calendar/build.gradle.kts b/app/feature/calendar/build.gradle.kts new file mode 100644 index 00000000..2aa424e7 --- /dev/null +++ b/app/feature/calendar/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("diary.app.feature") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:calendar-compose")) + + implementation(project(":app:domain:memo")) + implementation(project(":app:domain:holiday")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.feature.calendar" +} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarFeatureModule.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarFeatureModule.kt new file mode 100644 index 00000000..5644cc9c --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarFeatureModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.feature.calendar + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class CalendarFeatureModule diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarMemoViewModel.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarMemoViewModel.kt new file mode 100644 index 00000000..78954d74 --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarMemoViewModel.kt @@ -0,0 +1,55 @@ +package io.github.taetae98coding.diary.feature.calendar + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.domain.memo.usecase.FindCalendarMemoUseCase +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class CalendarMemoViewModel( + findCalendarMemoUseCase: FindCalendarMemoUseCase, +) : ViewModel() { + private val yearAndMonth = MutableStateFlow?>(null) + + val textItemList = yearAndMonth.filterNotNull() + .mapLatest { (year, month) -> LocalDate(year, month, 1) } + .mapLatest { it.minus(3, DateTimeUnit.MONTH)..it.plus(3, DateTimeUnit.MONTH) } + .flatMapLatest { findCalendarMemoUseCase(it) } + .mapLatest { it.getOrNull().orEmpty() } + .mapCollectionLatest { + CalendarItemUiState.Text( + key = MemoKey(it.id), + text = it.detail.title, + color = it.detail.color, + start = requireNotNull(it.detail.start), + endInclusive = requireNotNull(it.detail.endInclusive), + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + fun fetchMemo(year: Int, month: Month) { + viewModelScope.launch { + yearAndMonth.emit(year to month) + } + } +} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarNavigation.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarNavigation.kt new file mode 100644 index 00000000..8133c1a4 --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarNavigation.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.feature.calendar + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import io.github.taetae98coding.diary.core.navigation.calendar.CalendarDestination +import io.github.taetae98coding.diary.core.navigation.memo.MemoAddDestination +import io.github.taetae98coding.diary.core.navigation.memo.MemoDetailDestination + +public fun NavGraphBuilder.calendarNavigation( + navController: NavController, +) { + composable { + CalendarRoute( + navigateToMemoAdd = { navController.navigate(MemoAddDestination(it.start, it.endInclusive)) }, + navigateToMemoDetail = { navController.navigate(MemoDetailDestination(it)) }, + ) + } +} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarRoute.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarRoute.kt new file mode 100644 index 00000000..63666d92 --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarRoute.kt @@ -0,0 +1,53 @@ +package io.github.taetae98coding.diary.feature.calendar + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.datetime.LocalDate +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun CalendarRoute( + navigateToMemoAdd: (ClosedRange) -> Unit, + navigateToMemoDetail: (String) -> Unit, + modifier: Modifier = Modifier, + memoViewModel: CalendarMemoViewModel = koinViewModel(), + holidayViewModel: HolidayViewModel = koinViewModel(), +) { + val state = rememberCalendarScreenState() + val textItemList by memoViewModel.textItemList.collectAsStateWithLifecycle() + val holidayList by holidayViewModel.holidayList.collectAsStateWithLifecycle() + + CalendarScreen( + state = state, + onSelectDate = navigateToMemoAdd, + textItemListProvider = { textItemList }, + holidayListProvider = { holidayList }, + onCalendarItemClick = { key -> + when (key) { + is MemoKey -> navigateToMemoDetail(key.id) + } + }, + modifier = modifier, + ) + + Fetch( + state = state, + memoViewModel = memoViewModel, + holidayViewModel = holidayViewModel, + ) +} + +@Composable +private fun Fetch( + state: CalendarScreenState, + memoViewModel: CalendarMemoViewModel, + holidayViewModel: HolidayViewModel, +) { + LaunchedEffect(state.calendarState.year, state.calendarState.month) { + memoViewModel.fetchMemo(state.calendarState.year, state.calendarState.month) + holidayViewModel.fetchHoliday(state.calendarState.year, state.calendarState.month) + } +} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreen.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreen.kt new file mode 100644 index 00000000..f21cede0 --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreen.kt @@ -0,0 +1,54 @@ +package io.github.taetae98coding.diary.feature.calendar + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.LifecycleStartEffect +import io.github.taetae98coding.diary.core.calendar.compose.Calendar +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.core.calendar.compose.modifier.calendarDateRangeSelectable +import io.github.taetae98coding.diary.core.calendar.compose.topbar.CalendarTopBar +import io.github.taetae98coding.diary.library.datetime.todayIn +import kotlinx.datetime.LocalDate + +@Composable +internal fun CalendarScreen( + state: CalendarScreenState, + onSelectDate: (ClosedRange) -> Unit, + textItemListProvider: () -> List, + holidayListProvider: () -> List, + onCalendarItemClick: (Any) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { CalendarTopBar(state = state.calendarState) }, + ) { + var today by remember { mutableStateOf(LocalDate.todayIn()) } + + Calendar( + state = state.calendarState, + primaryDateListProvider = { listOf(today) }, + textItemListProvider = textItemListProvider, + holidayListProvider = holidayListProvider, + onCalendarItemClick = onCalendarItemClick, + modifier = Modifier.fillMaxSize() + .padding(it) + .calendarDateRangeSelectable( + state = state.calendarState, + onSelectDate = onSelectDate, + ), + ) + + LifecycleStartEffect(Unit) { + today = LocalDate.todayIn() + onStopOrDispose { } + } + } +} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreenState.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreenState.kt new file mode 100644 index 00000000..60915036 --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/CalendarScreenState.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.feature.calendar + +import io.github.taetae98coding.diary.core.calendar.compose.state.CalendarState + +internal class CalendarScreenState( + val calendarState: CalendarState, +) diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/HolidayViewModel.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/HolidayViewModel.kt new file mode 100644 index 00000000..a2149e65 --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/HolidayViewModel.kt @@ -0,0 +1,53 @@ +package io.github.taetae98coding.diary.feature.calendar + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.core.calendar.compose.item.CalendarItemUiState +import io.github.taetae98coding.diary.domain.holiday.usecase.FindHolidayUseCase +import io.github.taetae98coding.diary.library.coroutines.combine +import io.github.taetae98coding.diary.library.coroutines.mapCollectionLatest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.plus +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class HolidayViewModel( + findHolidayUseCase: FindHolidayUseCase, +) : ViewModel() { + private val yearAndMonth = MutableStateFlow?>(null) + + val holidayList = yearAndMonth.filterNotNull() + .mapLatest { (year, month) -> LocalDate(year, month, 1) } + .mapLatest { localDate -> IntRange(-2, 2).map { localDate.plus(it, DateTimeUnit.MONTH) } } + .mapLatest { list -> list.map { findHolidayUseCase(it.year, it.month) } } + .flatMapLatest { list -> list.combine { array -> array.flatMap { it.getOrNull().orEmpty() } } } + .mapCollectionLatest { + CalendarItemUiState.Holiday( + text = it.name, + start = it.start, + endInclusive = it.endInclusive, + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + fun fetchHoliday(year: Int, month: Month) { + viewModelScope.launch { + yearAndMonth.emit(year to month) + } + } +} diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/MemoKey.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/MemoKey.kt new file mode 100644 index 00000000..6d22695a --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/MemoKey.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.feature.calendar + +import kotlin.jvm.JvmInline + +@JvmInline +internal value class MemoKey(val id: String) diff --git a/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/RememberCalendarScreenState.kt b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/RememberCalendarScreenState.kt new file mode 100644 index 00000000..d1cb8010 --- /dev/null +++ b/app/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/RememberCalendarScreenState.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.feature.calendar + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import io.github.taetae98coding.diary.core.calendar.compose.state.rememberCalendarState + +@Composable +internal fun rememberCalendarScreenState(): CalendarScreenState { + val calendarState = rememberCalendarState() + + return remember { + CalendarScreenState( + calendarState = calendarState, + ) + } +} diff --git a/app/feature/memo/README.md b/app/feature/memo/README.md new file mode 100644 index 00000000..65c9de45 --- /dev/null +++ b/app/feature/memo/README.md @@ -0,0 +1,3 @@ +# :app:feature:memo module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_feature_memo.svg) diff --git a/app/feature/memo/build.gradle.kts b/app/feature/memo/build.gradle.kts new file mode 100644 index 00000000..041461a8 --- /dev/null +++ b/app/feature/memo/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("diary.app.feature") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:memo")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.feature.memo" +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoFeatureModule.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoFeatureModule.kt new file mode 100644 index 00000000..29c72de1 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoFeatureModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.feature.memo + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class MemoFeatureModule diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoNavigation.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoNavigation.kt new file mode 100644 index 00000000..0a4148b4 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/MemoNavigation.kt @@ -0,0 +1,36 @@ +package io.github.taetae98coding.diary.feature.memo + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import io.github.taetae98coding.diary.core.navigation.memo.MemoAddDestination +import io.github.taetae98coding.diary.core.navigation.memo.MemoDestination +import io.github.taetae98coding.diary.core.navigation.memo.MemoDetailDestination +import io.github.taetae98coding.diary.feature.memo.add.MemoAddRoute +import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailRoute +import io.github.taetae98coding.diary.library.navigation.LocalDateNavType +import kotlin.reflect.typeOf +import kotlinx.datetime.LocalDate + +public fun NavGraphBuilder.memoNavigation( + navController: NavController, +) { + navigation( + startDestination = MemoAddDestination(), + ) { + composable( + typeMap = mapOf(typeOf() to LocalDateNavType), + ) { + MemoAddRoute( + navigateUp = navController::popBackStack, + ) + } + + composable { + MemoDetailRoute( + navigateUp = navController::popBackStack, + ) + } + } +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt new file mode 100644 index 00000000..49dc19c3 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddRoute.kt @@ -0,0 +1,56 @@ +package io.github.taetae98coding.diary.feature.memo.add + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.memo_add +import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailActionButton +import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailFloatingButton +import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailNavigationButton +import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailScreen +import io.github.taetae98coding.diary.feature.memo.detail.rememberMemoDetailScreenAddState +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +internal fun MemoAddRoute( + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + addViewModel: MemoAddViewModel = koinViewModel(), +) { + val navigator = rememberListDetailPaneScaffoldNavigator() + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val state = rememberMemoDetailScreenAddState( + initialStart = addViewModel.route.start, + initialEndInclusive = addViewModel.route.endInclusive, + ) + val title = stringResource(Res.string.memo_add) + val uiState by addViewModel.uiState.collectAsStateWithLifecycle() + + MemoDetailScreen( + state = state, + titleProvider = { title }, + navigateButtonProvider = { MemoDetailNavigationButton.NavigateUp(onNavigateUp = navigateUp) }, + actionButtonProvider = { MemoDetailActionButton.None }, + floatingButtonProvider = { MemoDetailFloatingButton.Add { addViewModel.add(state.memoDetail) } }, + uiStateProvider = { uiState }, + ) + } + }, + detailPane = { + }, + modifier = modifier, + ) +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt new file mode 100644 index 00000000..f6a5ca53 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/add/MemoAddViewModel.kt @@ -0,0 +1,60 @@ +package io.github.taetae98coding.diary.feature.memo.add + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import io.github.taetae98coding.diary.common.exception.memo.MemoTitleBlankException +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.navigation.memo.MemoAddDestination +import io.github.taetae98coding.diary.domain.memo.usecase.AddMemoUseCase +import io.github.taetae98coding.diary.feature.memo.detail.MemoDetailScreenUiState +import io.github.taetae98coding.diary.library.navigation.LocalDateNavType +import kotlin.reflect.typeOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate +import org.koin.android.annotation.KoinViewModel + +@KoinViewModel +internal class MemoAddViewModel( + savedStateHandle: SavedStateHandle, + private val addMemoUseCase: AddMemoUseCase, +) : ViewModel() { + val route = savedStateHandle.toRoute( + typeMap = mapOf(typeOf() to LocalDateNavType), + ) + + private val _uiState = MutableStateFlow(MemoDetailScreenUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() + + private fun clearMessage() { + _uiState.update { + it.copy( + isAdd = false, + isTitleBlankError = false, + isUnknownError = false, + ) + } + } + + fun add(detail: MemoDetail) { + if (uiState.value.isProgress) return + + viewModelScope.launch { + _uiState.update { it.copy(isProgress = true) } + addMemoUseCase(detail) + .onSuccess { _uiState.update { it.copy(isProgress = false, isAdd = true) } } + .onFailure { handleThrowable(it) } + } + } + + private fun handleThrowable(throwable: Throwable) { + when (throwable) { + is MemoTitleBlankException -> _uiState.update { it.copy(isProgress = false, isTitleBlankError = true) } + else -> _uiState.update { it.copy(isProgress = false, isUnknownError = true) } + } + } +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt new file mode 100644 index 00000000..c30f07e4 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailActionButton.kt @@ -0,0 +1,10 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +internal sealed class MemoDetailActionButton { + data object None : MemoDetailActionButton() + data class FinishAndDetail( + val isFinish: Boolean, + val onFinishChange: (Boolean) -> Unit, + val delete: () -> Unit, + ) : MemoDetailActionButton() +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt new file mode 100644 index 00000000..5737ddd2 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailFloatingButton.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +internal sealed class MemoDetailFloatingButton { + data object None : MemoDetailFloatingButton() + data class Add(val onAdd: () -> Unit) : MemoDetailFloatingButton() +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt new file mode 100644 index 00000000..79ddc869 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailNavigationButton.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +internal sealed class MemoDetailNavigationButton { + data object None : MemoDetailNavigationButton() + data class NavigateUp(val onNavigateUp: () -> Unit) : MemoDetailNavigationButton() +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt new file mode 100644 index 00000000..6ff7daae --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailRoute.kt @@ -0,0 +1,50 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +internal fun MemoDetailRoute( + navigateUp: () -> Unit, + modifier: Modifier = Modifier, + detailViewModel: MemoDetailViewModel = koinViewModel(), +) { + val navigator = rememberListDetailPaneScaffoldNavigator() + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + val detail by detailViewModel.detail.collectAsStateWithLifecycle() + val state = rememberMemoDetailScreenDetailState( + onDelete = navigateUp, + onUpdate = navigateUp, + detailProvider = { detail }, + ) + val actionButton by detailViewModel.actionButton.collectAsStateWithLifecycle() + val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() + + MemoDetailScreen( + state = state, + titleProvider = { detail?.title }, + navigateButtonProvider = { MemoDetailNavigationButton.NavigateUp(onNavigateUp = { detailViewModel.update(state.memoDetail) }) }, + actionButtonProvider = { actionButton }, + floatingButtonProvider = { MemoDetailFloatingButton.None }, + uiStateProvider = { uiState }, + ) + } + }, + detailPane = { + }, + modifier = modifier, + ) +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt new file mode 100644 index 00000000..9ff3134a --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreen.kt @@ -0,0 +1,217 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.design.system.diary.color.DiaryColor +import io.github.taetae98coding.diary.core.design.system.diary.component.DiaryComponent +import io.github.taetae98coding.diary.core.design.system.diary.date.DiaryDate +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.icon.AddIcon +import io.github.taetae98coding.diary.core.resources.icon.DeleteIcon +import io.github.taetae98coding.diary.core.resources.icon.FinishIcon +import io.github.taetae98coding.diary.core.resources.icon.NavigateUpIcon +import io.github.taetae98coding.diary.core.resources.memo_add_message +import io.github.taetae98coding.diary.core.resources.title_blank_error +import io.github.taetae98coding.diary.core.resources.unknown_error +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun MemoDetailScreen( + state: MemoDetailScreenState, + titleProvider: () -> String?, + navigateButtonProvider: () -> MemoDetailNavigationButton, + actionButtonProvider: () -> MemoDetailActionButton, + floatingButtonProvider: () -> MemoDetailFloatingButton, + uiStateProvider: () -> MemoDetailScreenUiState, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + titleProvider()?.let { + Text( + text = it, + modifier = Modifier.basicMarquee(iterations = Int.MAX_VALUE), + maxLines = 1, + ) + } + }, + navigationIcon = { + when (val button = navigateButtonProvider()) { + is MemoDetailNavigationButton.NavigateUp -> { + IconButton(onClick = button.onNavigateUp) { + NavigateUpIcon() + } + } + + else -> Unit + } + }, + actions = { + when (val button = actionButtonProvider()) { + is MemoDetailActionButton.FinishAndDetail -> { + IconToggleButton( + checked = button.isFinish, + onCheckedChange = button.onFinishChange, + ) { + FinishIcon() + } + + IconButton(onClick = button.delete) { + DeleteIcon() + } + } + + else -> Unit + } + }, + ) + }, + snackbarHost = { SnackbarHost(hostState = state.hostState) }, + floatingActionButton = { + when (val button = floatingButtonProvider()) { + is MemoDetailFloatingButton.Add -> { + val isProgress by remember { derivedStateOf { uiStateProvider().isProgress } } + + FloatingActionButton(onClick = button.onAdd) { + if (isProgress) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } else { + AddIcon() + } + } + } + + else -> Unit + } + }, + ) { + Content( + state = state, + modifier = Modifier.fillMaxSize() + .padding(it) + .padding(DiaryTheme.dimen.screenPaddingValues), + ) + } + + Message( + state = state, + uiStateProvider = uiStateProvider, + ) + + LaunchedFocus(state = state) +} + +@Composable +private fun Message( + state: MemoDetailScreenState, + uiStateProvider: () -> MemoDetailScreenUiState, +) { + val uiState = uiStateProvider() + val addMessage = stringResource(Res.string.memo_add_message) + val titleBlankMessage = stringResource(Res.string.title_blank_error) + val unknownErrorMessage = stringResource(Res.string.unknown_error) + + LaunchedEffect( + uiState.isAdd, + uiState.isDelete, + uiState.isUpdate, + uiState.isTitleBlankError, + uiState.isUnknownError, + ) { + if (!uiState.hasMessage) return@LaunchedEffect + + when { + uiState.isAdd -> { + state.showMessage(addMessage) + state.clearInput() + state.requestTitleFocus() + } + + uiState.isDelete -> { + if (state is MemoDetailScreenState.Detail) { + state.onDelete() + } + } + + uiState.isUpdate -> { + if (state is MemoDetailScreenState.Detail) { + state.onUpdate() + } + } + + uiState.isTitleBlankError -> { + state.showMessage(titleBlankMessage) + state.titleError() + } + + uiState.isUnknownError -> state.showMessage(unknownErrorMessage) + } + + uiState.onMessageShow() + } +} + +@Composable +private fun LaunchedFocus( + state: MemoDetailScreenState, +) { + LaunchedEffect(state) { + if (state is MemoDetailScreenState.Add) { + state.requestTitleFocus() + } + } +} + +@Composable +private fun Content( + state: MemoDetailScreenState, + modifier: Modifier = Modifier, +) { + Column( + modifier = Modifier.verticalScroll(state = rememberScrollState()) + .then(modifier), + verticalArrangement = Arrangement.spacedBy(DiaryTheme.dimen.itemSpace), + ) { + DiaryComponent(state = state.componentState) + DiaryDate(state = state.dateState) + Row { + DiaryColor( + state = state.colorState, + modifier = Modifier.weight(1F) + .height(100.dp), + ) + + Spacer(modifier = Modifier.weight(1F)) + } + } +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt new file mode 100644 index 00000000..feda2bda --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenState.kt @@ -0,0 +1,67 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.ui.graphics.toArgb +import io.github.taetae98coding.diary.core.design.system.diary.color.DiaryColorState +import io.github.taetae98coding.diary.core.design.system.diary.component.DiaryComponentState +import io.github.taetae98coding.diary.core.design.system.diary.date.DiaryDateState +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +internal sealed class MemoDetailScreenState { + abstract val coroutineScope: CoroutineScope + abstract val componentState: DiaryComponentState + abstract val dateState: DiaryDateState + abstract val colorState: DiaryColorState + + private var messageJob: Job? = null + + val hostState: SnackbarHostState = SnackbarHostState() + + data class Add( + override val coroutineScope: CoroutineScope, + override val componentState: DiaryComponentState, + override val dateState: DiaryDateState, + override val colorState: DiaryColorState, + ) : MemoDetailScreenState() + + data class Detail( + val onDelete: () -> Unit, + val onUpdate: () -> Unit, + override val coroutineScope: CoroutineScope, + override val componentState: DiaryComponentState, + override val dateState: DiaryDateState, + override val colorState: DiaryColorState, + ) : MemoDetailScreenState() + + val memoDetail: MemoDetail + get() { + return MemoDetail( + title = componentState.title, + description = componentState.description, + start = dateState.start.takeIf { dateState.hasDate }, + endInclusive = dateState.endInclusive.takeIf { dateState.hasDate }, + color = colorState.color.toArgb(), + ) + } + + fun requestTitleFocus() { + componentState.requestTitleFocus() + } + + fun titleError() { + requestTitleFocus() + componentState.titleError() + } + + fun showMessage(message: String) { + messageJob?.cancel() + messageJob = coroutineScope.launch { hostState.showSnackbar(message) } + } + + fun clearInput() { + componentState.clearInput() + } +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt new file mode 100644 index 00000000..f46b1103 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailScreenUiState.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +internal data class MemoDetailScreenUiState( + val isProgress: Boolean = false, + val isAdd: Boolean = false, + val isDelete: Boolean = false, + val isUpdate: Boolean = false, + val isTitleBlankError: Boolean = false, + val isUnknownError: Boolean = false, + val onMessageShow: () -> Unit = {}, +) { + val hasMessage = isAdd || isDelete || isUpdate || isTitleBlankError || isUnknownError +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt new file mode 100644 index 00000000..9d2fc475 --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/MemoDetailViewModel.kt @@ -0,0 +1,109 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import io.github.taetae98coding.diary.core.navigation.memo.MemoDetailDestination +import io.github.taetae98coding.diary.domain.memo.usecase.DeleteMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.FindMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.FinishMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.RestartMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.UpdateMemoUseCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class MemoDetailViewModel( + savedStateHandle: SavedStateHandle, + findMemoUseCase: FindMemoUseCase, + private val updateMemoUseCase: UpdateMemoUseCase, + private val finishMemoUseCase: FinishMemoUseCase, + private val restartMemoUseCase: RestartMemoUseCase, + private val deleteMemoUseCase: DeleteMemoUseCase, +) : ViewModel() { + private val route = savedStateHandle.toRoute() + + private val _uiState = MutableStateFlow(MemoDetailScreenUiState(onMessageShow = ::clearMessage)) + val uiState = _uiState.asStateFlow() + + private val memo = findMemoUseCase(route.memoId) + .mapLatest { it.getOrNull() } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + val detail = memo.mapLatest { it?.detail } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + val actionButton = memo.mapLatest { + MemoDetailActionButton.FinishAndDetail( + isFinish = it?.isFinish ?: false, + onFinishChange = ::onFinishChange, + delete = ::delete, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = MemoDetailActionButton.FinishAndDetail( + isFinish = false, + onFinishChange = ::onFinishChange, + delete = ::delete, + ), + ) + + private fun onFinishChange(isFinish: Boolean) { + viewModelScope.launch { + if (isFinish) { + finishMemoUseCase(route.memoId).onFailure { handleThrowable() } + } else { + restartMemoUseCase(route.memoId).onFailure { handleThrowable() } + } + } + } + + private fun delete() { + viewModelScope.launch { + deleteMemoUseCase(route.memoId) + .onSuccess { _uiState.update { it.copy(isDelete = true) } } + .onFailure { handleThrowable() } + } + } + + private fun handleThrowable() { + _uiState.update { it.copy(isUnknownError = true) } + } + + private fun clearMessage() { + _uiState.update { + it.copy( + isDelete = false, + isUpdate = false, + isUnknownError = false, + ) + } + } + + fun update(detail: MemoDetail) { + viewModelScope.launch { + updateMemoUseCase(route.memoId, detail) + .onSuccess { _uiState.update { it.copy(isUpdate = true) } } + .onFailure { handleThrowable() } + } + } +} diff --git a/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenState.kt b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenState.kt new file mode 100644 index 00000000..6566a48c --- /dev/null +++ b/app/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/detail/RememberMemoDetailScreenState.kt @@ -0,0 +1,69 @@ +package io.github.taetae98coding.diary.feature.memo.detail + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.Color +import io.github.taetae98coding.diary.core.design.system.diary.color.rememberDiaryColorState +import io.github.taetae98coding.diary.core.design.system.diary.component.rememberDiaryComponentState +import io.github.taetae98coding.diary.core.design.system.diary.date.rememberDiaryDateState +import io.github.taetae98coding.diary.core.model.memo.MemoDetail +import kotlinx.datetime.LocalDate + +@Composable +internal fun rememberMemoDetailScreenAddState( + initialStart: LocalDate?, + initialEndInclusive: LocalDate?, +): MemoDetailScreenState.Add { + val coroutineScope = rememberCoroutineScope() + val componentState = rememberDiaryComponentState() + val dateState = rememberDiaryDateState( + initialStart = initialStart, + initialEndInclusive = initialEndInclusive, + ) + val colorState = rememberDiaryColorState() + + return remember { + MemoDetailScreenState.Add( + coroutineScope = coroutineScope, + componentState = componentState, + dateState = dateState, + colorState = colorState, + ) + } +} + +@Composable +internal fun rememberMemoDetailScreenDetailState( + onDelete: () -> Unit, + onUpdate: () -> Unit, + detailProvider: () -> MemoDetail?, +): MemoDetailScreenState.Detail { + val detail = detailProvider() + val coroutineScope = rememberCoroutineScope() + val componentState = rememberDiaryComponentState( + inputs = arrayOf(detail?.title, detail?.description), + initialTitle = detail?.title.orEmpty(), + initialDescription = detail?.description.orEmpty(), + ) + val dateState = rememberDiaryDateState( + inputs = arrayOf(detail?.start, detail?.endInclusive), + initialStart = detail?.start, + initialEndInclusive = detail?.endInclusive, + ) + val colorState = rememberDiaryColorState( + inputs = arrayOf(detail?.color), + initialColor = detail?.color?.let { Color(it) } ?: Color.Unspecified, + ) + + return remember(componentState, dateState, colorState) { + MemoDetailScreenState.Detail( + onDelete = onDelete, + onUpdate = onUpdate, + coroutineScope = coroutineScope, + componentState = componentState, + dateState = dateState, + colorState = colorState, + ) + } +} diff --git a/app/feature/more/README.md b/app/feature/more/README.md new file mode 100644 index 00000000..2b751c99 --- /dev/null +++ b/app/feature/more/README.md @@ -0,0 +1,3 @@ +# :app:feature:more module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_feature_more.svg) diff --git a/app/feature/more/build.gradle.kts b/app/feature/more/build.gradle.kts new file mode 100644 index 00000000..278a55e8 --- /dev/null +++ b/app/feature/more/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("diary.app.feature") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:domain:account")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.feature.more" +} diff --git a/app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreAccountPreview.kt b/app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreAccountPreview.kt new file mode 100644 index 00000000..ff963c2f --- /dev/null +++ b/app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreAccountPreview.kt @@ -0,0 +1,48 @@ +package io.github.taetae98coding.diary.feature.more + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.feature.more.account.MoreAccount +import io.github.taetae98coding.diary.feature.more.account.state.MoreAccountUiState + +@Composable +@DiaryPreview +private fun LoadingPreview() { + DiaryTheme { + MoreAccount( + uiStateProvider = { MoreAccountUiState.Loading }, + onLogin = {}, + onJoin = {}, + ) + } +} + +@Composable +@DiaryPreview +private fun GuestPreview() { + DiaryTheme { + MoreAccount( + uiStateProvider = { MoreAccountUiState.Guest }, + onLogin = {}, + onJoin = {}, + ) + } +} + +@Composable +@DiaryPreview +private fun MemberPreview() { + DiaryTheme { + MoreAccount( + uiStateProvider = { + MoreAccountUiState.Member( + email = "taetae98coding@gmail.com", + logout = {}, + ) + }, + onLogin = {}, + onJoin = {}, + ) + } +} diff --git a/app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreenPreview.kt b/app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreenPreview.kt new file mode 100644 index 00000000..38e51907 --- /dev/null +++ b/app/feature/more/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreenPreview.kt @@ -0,0 +1,18 @@ +package io.github.taetae98coding.diary.feature.more + +import androidx.compose.runtime.Composable +import io.github.taetae98coding.diary.core.design.system.preview.DiaryPreview +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.feature.more.account.state.MoreAccountUiState + +@Composable +@DiaryPreview +private fun MoreScreenPreview() { + DiaryTheme { + MoreScreen( + accountUiStateProvider = { MoreAccountUiState.Loading }, + onLogin = {}, + onJoin = {}, + ) + } +} diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreFeatureModule.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreFeatureModule.kt new file mode 100644 index 00000000..12de7770 --- /dev/null +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreFeatureModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.feature.more + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class MoreFeatureModule diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreNavigation.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreNavigation.kt new file mode 100644 index 00000000..87464cc0 --- /dev/null +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreNavigation.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.feature.more + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import io.github.taetae98coding.diary.core.navigation.account.JoinDestination +import io.github.taetae98coding.diary.core.navigation.account.LoginDestination +import io.github.taetae98coding.diary.core.navigation.more.MoreDestination + +public fun NavGraphBuilder.moreNavigation( + navController: NavController, +) { + composable { + MoreRoute( + navigateToLogin = { navController.navigate(LoginDestination) }, + navigateToJoin = { navController.navigate(JoinDestination) }, + ) + } +} diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreRoute.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreRoute.kt new file mode 100644 index 00000000..27471cd4 --- /dev/null +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreRoute.kt @@ -0,0 +1,25 @@ +package io.github.taetae98coding.diary.feature.more + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.taetae98coding.diary.feature.more.viewmodel.MoreAccountViewModel +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun MoreRoute( + navigateToLogin: () -> Unit, + navigateToJoin: () -> Unit, + modifier: Modifier = Modifier, + accountViewModel: MoreAccountViewModel = koinViewModel(), +) { + val accountUiState by accountViewModel.uiState.collectAsStateWithLifecycle() + + MoreScreen( + accountUiStateProvider = { accountUiState }, + onLogin = navigateToLogin, + onJoin = navigateToJoin, + modifier = modifier, + ) +} diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreen.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreen.kt new file mode 100644 index 00000000..a3ab5ac9 --- /dev/null +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/MoreScreen.kt @@ -0,0 +1,65 @@ +package io.github.taetae98coding.diary.feature.more + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.more +import io.github.taetae98coding.diary.feature.more.account.MoreAccount +import io.github.taetae98coding.diary.feature.more.account.state.MoreAccountUiState +import org.jetbrains.compose.resources.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun MoreScreen( + accountUiStateProvider: () -> MoreAccountUiState, + onLogin: () -> Unit, + onJoin: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar(title = { Text(text = stringResource(Res.string.more)) }) + }, + ) { + Content( + accountUiStateProvider = accountUiStateProvider, + onLogin = onLogin, + onJoin = onJoin, + modifier = Modifier.fillMaxSize() + .padding(it) + .padding(DiaryTheme.dimen.screenPaddingValues), + ) + } +} + +@Composable +private fun Content( + accountUiStateProvider: () -> MoreAccountUiState, + onLogin: () -> Unit, + onJoin: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + .then(modifier), + ) { + MoreAccount( + uiStateProvider = accountUiStateProvider, + onLogin = onLogin, + onJoin = onJoin, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/MoreAccount.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/MoreAccount.kt new file mode 100644 index 00000000..9214c753 --- /dev/null +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/MoreAccount.kt @@ -0,0 +1,205 @@ +package io.github.taetae98coding.diary.feature.more.account + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.taetae98coding.diary.core.design.system.chip.DiaryAssistChip +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.guest +import io.github.taetae98coding.diary.core.resources.icon.AccountIcon +import io.github.taetae98coding.diary.core.resources.icon.LoginIcon +import io.github.taetae98coding.diary.core.resources.icon.LogoutIcon +import io.github.taetae98coding.diary.core.resources.join +import io.github.taetae98coding.diary.core.resources.login +import io.github.taetae98coding.diary.core.resources.logout +import io.github.taetae98coding.diary.feature.more.account.state.MoreAccountUiState +import io.github.taetae98coding.diary.library.color.multiplyAlpha +import io.github.taetae98coding.diary.library.shimmer.m3.shimmer +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun MoreAccount( + uiStateProvider: () -> MoreAccountUiState, + onLogin: () -> Unit, + onJoin: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier, + ) { + Column( + modifier = Modifier.padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + ProfileRow( + uiStateProvider = uiStateProvider, + modifier = Modifier.padding(horizontal = 8.dp), + ) + + ButtonRow( + uiStateProvider = uiStateProvider, + onLogin = onLogin, + onJoin = onJoin, + ) + } + } +} + +@Composable +private fun ProfileRow( + uiStateProvider: () -> MoreAccountUiState, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ProfileImage() + Email(uiStateProvider = uiStateProvider) + } +} + +@Composable +private fun ProfileImage( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.size(54.dp) + .background( + color = LocalContentColor.current.multiplyAlpha(value = 0.38F), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + AccountIcon(modifier = Modifier.size(40.dp)) + } +} + +@Composable +private fun Email( + uiStateProvider: () -> MoreAccountUiState, + modifier: Modifier = Modifier, +) { + Crossfade(targetState = uiStateProvider()) { uiState -> + when (uiState) { + is MoreAccountUiState.Loading -> { + Text( + text = "", + modifier = modifier.width(100.dp) + .shimmer(), + ) + } + + is MoreAccountUiState.Guest -> { + Text( + text = stringResource(Res.string.guest), + modifier = modifier, + ) + } + + is MoreAccountUiState.Member -> { + Text( + text = uiState.email, + modifier = modifier, + ) + } + } + } +} + +@Composable +private fun ButtonRow( + uiStateProvider: () -> MoreAccountUiState, + onLogin: () -> Unit, + onJoin: () -> Unit, + modifier: Modifier = Modifier, +) { + LazyRow( + modifier = modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + when (val uiState = uiStateProvider()) { + is MoreAccountUiState.Loading -> { + items( + count = 1, + key = { it }, + contentType = { "placeHolder" }, + ) { + DiaryAssistChip( + onClick = {}, + label = { Text(text = "") }, + modifier = Modifier.width(80.dp).animateItem(), + shape = CircleShape, + colors = AssistChipDefaults.assistChipColors( + containerColor = LocalContentColor.current.multiplyAlpha(0.38F), + ), + border = null, + ) + } + } + + is MoreAccountUiState.Guest -> { + item( + key = "login", + contentType = "Chip", + ) { + DiaryAssistChip( + onClick = onLogin, + label = { Text(text = stringResource(Res.string.login)) }, + modifier = Modifier.animateItem(), + leadingIcon = { LoginIcon() }, + shape = CircleShape, + ) + } + + item( + key = "join", + contentType = "Chip", + ) { + DiaryAssistChip( + onClick = onJoin, + label = { Text(text = stringResource(Res.string.join)) }, + modifier = Modifier.animateItem(), + leadingIcon = { AccountIcon() }, + shape = CircleShape, + ) + } + } + + is MoreAccountUiState.Member -> { + item( + key = "logout", + contentType = "Chip", + ) { + DiaryAssistChip( + onClick = uiState.logout, + label = { Text(text = stringResource(Res.string.logout)) }, + modifier = Modifier.animateItem(), + leadingIcon = { LogoutIcon() }, + shape = CircleShape, + ) + } + } + } + } +} diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/state/MoreAccountUiState.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/state/MoreAccountUiState.kt new file mode 100644 index 00000000..fb5caf3a --- /dev/null +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/account/state/MoreAccountUiState.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.feature.more.account.state + +internal sealed class MoreAccountUiState { + + data object Loading : MoreAccountUiState() + + data object Guest : MoreAccountUiState() + + data class Member( + val email: String, + val logout: () -> Unit, + ) : MoreAccountUiState() +} diff --git a/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt new file mode 100644 index 00000000..a77ccade --- /dev/null +++ b/app/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/viewmodel/MoreAccountViewModel.kt @@ -0,0 +1,59 @@ +package io.github.taetae98coding.diary.feature.more.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.taetae98coding.diary.core.model.account.Account +import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase +import io.github.taetae98coding.diary.domain.account.usecase.LogoutUseCase +import io.github.taetae98coding.diary.feature.more.account.state.MoreAccountUiState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.koin.android.annotation.KoinViewModel + +@OptIn(ExperimentalCoroutinesApi::class) +@KoinViewModel +internal class MoreAccountViewModel( + getAccountUseCase: GetAccountUseCase, + private val logoutUseCase: LogoutUseCase, +) : ViewModel() { + private val isProgress = MutableStateFlow(false) + private val account = getAccountUseCase().mapLatest { it.getOrNull() } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null, + ) + + val uiState = combine(isProgress, account) { isProgress, account -> + if (isProgress) { + MoreAccountUiState.Loading + } else if (account == null) { + MoreAccountUiState.Loading + } else { + when (account) { + is Account.Guest -> MoreAccountUiState.Guest + is Account.Member -> MoreAccountUiState.Member( + email = account.email, + logout = ::logout, + ) + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = MoreAccountUiState.Loading, + ) + + private fun logout() { + viewModelScope.launch { + isProgress.emit(true) + logoutUseCase() + isProgress.emit(false) + } + } +} diff --git a/app/platform/android/README.md b/app/platform/android/README.md new file mode 100644 index 00000000..89281410 --- /dev/null +++ b/app/platform/android/README.md @@ -0,0 +1,3 @@ +# :app:platform:android module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_platform_android.svg) diff --git a/app/platform/android/build.gradle.kts b/app/platform/android/build.gradle.kts new file mode 100644 index 00000000..5a412c6a --- /dev/null +++ b/app/platform/android/build.gradle.kts @@ -0,0 +1,111 @@ +import ext.getLocalProperty + +private val localProperties = requireNotNull(project.getLocalProperty()) + +plugins { + id("diary.android.application") + id("diary.kotlin.android") + id("diary.compose") + alias(libs.plugins.dependency.guard) +} + +android { + namespace = Build.NAMESPACE + + signingConfigs { + create("dev") { + storeFile = file("keystore/dev.jks") + storePassword = localProperties.getProperty("android.dev.store.password") + keyAlias = localProperties.getProperty("android.dev.key.alias") + keyPassword = localProperties.getProperty("android.dev.key.password") + } + + create("real") { + storeFile = file("keystore/real.jks") + storePassword = localProperties.getProperty("android.real.store.password") + keyAlias = localProperties.getProperty("android.real.key.alias") + keyPassword = localProperties.getProperty("android.real.key.password") + } + } + + defaultConfig { + applicationId = "io.github.taetae98coding.diary" + + versionCode = 1 + versionName = "1.0.0-beta01" + } + + buildTypes { + debug { + isDefault = true + + applicationIdSuffix = ".debug" + signingConfig = null + } + + release { + signingConfig = null + + isMinifyEnabled = true + isShrinkResources = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + flavorDimensions.add("development") + productFlavors { + create("dev") { + isDefault = true + + dimension = "development" + applicationIdSuffix = ".dev" + signingConfig = signingConfigs.getByName("dev") + + buildConfigField("String", "DIARY_API_URL", "\"${localProperties.getProperty("diary.dev.api.base.url")}\"") + buildConfigField("String", "HOLIDAY_API_URL", "\"${localProperties.getProperty("holiday.dev.api.url")}\"") + buildConfigField("String", "HOLIDAY_API_KEY", "\"${localProperties.getProperty("holiday.dev.api.key")}\"") + } + + create("real") { + dimension = "development" + signingConfig = signingConfigs.getByName("real") + + buildConfigField("String", "DIARY_API_URL", "\"${localProperties.getProperty("diary.real.api.base.url")}\"") + buildConfigField("String", "HOLIDAY_API_URL", "\"${localProperties.getProperty("holiday.real.api.url")}\"") + buildConfigField("String", "HOLIDAY_API_KEY", "\"${localProperties.getProperty("holiday.real.api.key")}\"") + } + } + + buildFeatures { + buildConfig = true + } +} + +dependencies { + implementation(project(":app:platform:common")) + implementation(project(":app:core:diary-database-room")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:core:account-preferences-datastore")) + implementation(project(":app:core:holiday-preferences-datastore")) + implementation(project(":app:core:holiday-database-room")) + implementation(project(":app:core:holiday-service")) + + implementation(libs.android.material) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.startup) + + implementation(platform(libs.koin.bom)) + implementation(libs.koin.android) + + runtimeOnly(libs.ktor.client.okhttp) + + debugImplementation(libs.leakcanary) +} + +dependencyGuard { + configuration("realReleaseRuntimeClasspath") { + allowedFilter = { + !it.contains("junit") && !it.contains("leakcanary") + } + } +} diff --git a/app/platform/android/dependencies/realReleaseRuntimeClasspath.txt b/app/platform/android/dependencies/realReleaseRuntimeClasspath.txt new file mode 100644 index 00000000..2005f48a --- /dev/null +++ b/app/platform/android/dependencies/realReleaseRuntimeClasspath.txt @@ -0,0 +1,269 @@ +androidx.activity:activity-compose:1.9.3 +androidx.activity:activity-ktx:1.9.3 +androidx.activity:activity:1.9.3 +androidx.annotation:annotation-experimental:1.4.1 +androidx.annotation:annotation-jvm:1.8.1 +androidx.annotation:annotation:1.8.1 +androidx.appcompat:appcompat-resources:1.7.0 +androidx.appcompat:appcompat:1.7.0 +androidx.arch.core:core-common:2.2.0 +androidx.arch.core:core-runtime:2.2.0 +androidx.autofill:autofill:1.0.0 +androidx.cardview:cardview:1.0.0 +androidx.collection:collection-jvm:1.4.2 +androidx.collection:collection-ktx:1.4.2 +androidx.collection:collection:1.4.2 +androidx.compose.animation:animation-android:1.7.1 +androidx.compose.animation:animation-core-android:1.7.1 +androidx.compose.animation:animation-core:1.7.1 +androidx.compose.animation:animation:1.7.1 +androidx.compose.foundation:foundation-android:1.7.1 +androidx.compose.foundation:foundation-layout-android:1.7.1 +androidx.compose.foundation:foundation-layout:1.7.1 +androidx.compose.foundation:foundation:1.7.1 +androidx.compose.material3.adaptive:adaptive-android:1.0.0 +androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0 +androidx.compose.material3.adaptive:adaptive-layout:1.0.0 +androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0 +androidx.compose.material3.adaptive:adaptive-navigation:1.0.0 +androidx.compose.material3.adaptive:adaptive:1.0.0 +androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.0 +androidx.compose.material3:material3-adaptive-navigation-suite:1.3.0 +androidx.compose.material3:material3-android:1.3.0 +androidx.compose.material3:material3:1.3.0 +androidx.compose.material:material-icons-core-android:1.7.1 +androidx.compose.material:material-icons-core:1.7.1 +androidx.compose.material:material-icons-extended-android:1.7.1 +androidx.compose.material:material-icons-extended:1.7.1 +androidx.compose.material:material-ripple-android:1.7.1 +androidx.compose.material:material-ripple:1.7.1 +androidx.compose.runtime:runtime-android:1.7.1 +androidx.compose.runtime:runtime-saveable-android:1.7.1 +androidx.compose.runtime:runtime-saveable:1.7.1 +androidx.compose.runtime:runtime:1.7.1 +androidx.compose.ui:ui-android:1.7.1 +androidx.compose.ui:ui-geometry-android:1.7.1 +androidx.compose.ui:ui-geometry:1.7.1 +androidx.compose.ui:ui-graphics-android:1.7.1 +androidx.compose.ui:ui-graphics:1.7.1 +androidx.compose.ui:ui-text-android:1.7.1 +androidx.compose.ui:ui-text:1.7.1 +androidx.compose.ui:ui-tooling-preview-android:1.7.1 +androidx.compose.ui:ui-tooling-preview:1.7.1 +androidx.compose.ui:ui-unit-android:1.7.1 +androidx.compose.ui:ui-unit:1.7.1 +androidx.compose.ui:ui-util-android:1.7.1 +androidx.compose.ui:ui-util:1.7.1 +androidx.compose.ui:ui:1.7.1 +androidx.concurrent:concurrent-futures:1.1.0 +androidx.constraintlayout:constraintlayout-solver:2.0.1 +androidx.constraintlayout:constraintlayout:2.0.1 +androidx.coordinatorlayout:coordinatorlayout:1.1.0 +androidx.core:core-ktx:1.13.1 +androidx.core:core:1.13.1 +androidx.cursoradapter:cursoradapter:1.0.0 +androidx.customview:customview-poolingcontainer:1.0.0 +androidx.customview:customview:1.1.0 +androidx.datastore:datastore-android:1.1.1 +androidx.datastore:datastore-core-android:1.1.1 +androidx.datastore:datastore-core-okio-jvm:1.1.1 +androidx.datastore:datastore-core-okio:1.1.1 +androidx.datastore:datastore-core:1.1.1 +androidx.datastore:datastore-preferences-android:1.1.1 +androidx.datastore:datastore-preferences-core-jvm:1.1.1 +androidx.datastore:datastore-preferences-core:1.1.1 +androidx.datastore:datastore-preferences:1.1.1 +androidx.datastore:datastore:1.1.1 +androidx.documentfile:documentfile:1.0.0 +androidx.drawerlayout:drawerlayout:1.1.1 +androidx.dynamicanimation:dynamicanimation:1.0.0 +androidx.emoji2:emoji2-views-helper:1.3.0 +androidx.emoji2:emoji2:1.3.0 +androidx.fragment:fragment-ktx:1.8.3 +androidx.fragment:fragment:1.8.3 +androidx.graphics:graphics-path:1.0.1 +androidx.interpolator:interpolator:1.0.0 +androidx.legacy:legacy-support-core-utils:1.0.0 +androidx.lifecycle:lifecycle-common-java8:2.8.5 +androidx.lifecycle:lifecycle-common-jvm:2.8.5 +androidx.lifecycle:lifecycle-common:2.8.5 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.5 +androidx.lifecycle:lifecycle-livedata-core:2.8.5 +androidx.lifecycle:lifecycle-livedata:2.8.5 +androidx.lifecycle:lifecycle-process:2.8.5 +androidx.lifecycle:lifecycle-runtime-android:2.8.5 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.5 +androidx.lifecycle:lifecycle-runtime-compose:2.8.5 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.5 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.5 +androidx.lifecycle:lifecycle-runtime:2.8.5 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.5 +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.5 +androidx.lifecycle:lifecycle-viewmodel-compose:2.8.5 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.5 +androidx.lifecycle:lifecycle-viewmodel:2.8.5 +androidx.loader:loader:1.0.0 +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 +androidx.navigation:navigation-common-ktx:2.8.0-rc01 +androidx.navigation:navigation-common:2.8.0-rc01 +androidx.navigation:navigation-compose:2.8.0-rc01 +androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 +androidx.navigation:navigation-runtime:2.8.0-rc01 +androidx.print:print:1.0.0 +androidx.profileinstaller:profileinstaller:1.3.1 +androidx.recyclerview:recyclerview:1.1.0 +androidx.resourceinspection:resourceinspection-annotation:1.0.1 +androidx.room:room-common-jvm:2.7.0-alpha11 +androidx.room:room-common:2.7.0-alpha11 +androidx.room:room-runtime-android:2.7.0-alpha11 +androidx.room:room-runtime:2.7.0-alpha11 +androidx.savedstate:savedstate-ktx:1.2.1 +androidx.savedstate:savedstate:1.2.1 +androidx.sqlite:sqlite-android:2.5.0-alpha11 +androidx.sqlite:sqlite-bundled-android:2.5.0-alpha11 +androidx.sqlite:sqlite-bundled:2.5.0-alpha11 +androidx.sqlite:sqlite-framework-android:2.5.0-alpha11 +androidx.sqlite:sqlite-framework:2.5.0-alpha11 +androidx.sqlite:sqlite:2.5.0-alpha11 +androidx.startup:startup-runtime:1.2.0 +androidx.tracing:tracing:1.0.0 +androidx.transition:transition:1.5.0 +androidx.vectordrawable:vectordrawable-animated:1.1.0 +androidx.vectordrawable:vectordrawable:1.1.0 +androidx.versionedparcelable:versionedparcelable:1.1.1 +androidx.viewpager2:viewpager2:1.0.0 +androidx.viewpager:viewpager:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window-core-android:1.3.0 +androidx.window:window-core:1.3.0 +androidx.window:window:1.3.0 +co.touchlab:stately-concurrency-jvm:2.1.0 +co.touchlab:stately-concurrency:2.1.0 +co.touchlab:stately-concurrent-collections-jvm:2.1.0 +co.touchlab:stately-concurrent-collections:2.1.0 +co.touchlab:stately-strict-jvm:2.1.0 +co.touchlab:stately-strict:2.1.0 +com.google.android.material:material:1.12.0 +com.google.errorprone:error_prone_annotations:2.15.0 +com.google.guava:listenablefuture:1.0 +com.mikepenz:multiplatform-markdown-renderer-android:0.27.0 +com.mikepenz:multiplatform-markdown-renderer-m3-android:0.27.0 +com.mikepenz:multiplatform-markdown-renderer-m3:0.27.0 +com.mikepenz:multiplatform-markdown-renderer:0.27.0 +com.squareup.okhttp3:okhttp-sse:4.12.0 +com.squareup.okhttp3:okhttp:4.12.0 +com.squareup.okio:okio-jvm:3.9.1 +com.squareup.okio:okio:3.9.1 +io.insert-koin:koin-android:4.0.0 +io.insert-koin:koin-annotations-bom:2.0.0-Beta1 +io.insert-koin:koin-annotations-jvm:2.0.0-Beta1 +io.insert-koin:koin-annotations:2.0.0-Beta1 +io.insert-koin:koin-bom:4.0.0 +io.insert-koin:koin-compose-jvm:4.0.0 +io.insert-koin:koin-compose-viewmodel-jvm:4.0.0 +io.insert-koin:koin-compose-viewmodel-navigation-jvm:4.0.0 +io.insert-koin:koin-compose-viewmodel-navigation:4.0.0 +io.insert-koin:koin-compose-viewmodel:4.0.0 +io.insert-koin:koin-compose:4.0.0 +io.insert-koin:koin-core-jvm:4.0.0 +io.insert-koin:koin-core-viewmodel-jvm:4.0.0 +io.insert-koin:koin-core-viewmodel-navigation-jvm:4.0.0 +io.insert-koin:koin-core-viewmodel-navigation:4.0.0 +io.insert-koin:koin-core-viewmodel:4.0.0 +io.insert-koin:koin-core:4.0.0 +io.ktor:ktor-client-content-negotiation-jvm:3.0.1 +io.ktor:ktor-client-content-negotiation:3.0.1 +io.ktor:ktor-client-core-jvm:3.0.1 +io.ktor:ktor-client-core:3.0.1 +io.ktor:ktor-client-okhttp-jvm:3.0.1 +io.ktor:ktor-client-okhttp:3.0.1 +io.ktor:ktor-events-jvm:3.0.1 +io.ktor:ktor-events:3.0.1 +io.ktor:ktor-http-jvm:3.0.1 +io.ktor:ktor-http:3.0.1 +io.ktor:ktor-io-jvm:3.0.1 +io.ktor:ktor-io:3.0.1 +io.ktor:ktor-serialization-jvm:3.0.1 +io.ktor:ktor-serialization-kotlinx-json-jvm:3.0.1 +io.ktor:ktor-serialization-kotlinx-json:3.0.1 +io.ktor:ktor-serialization-kotlinx-jvm:3.0.1 +io.ktor:ktor-serialization-kotlinx:3.0.1 +io.ktor:ktor-serialization:3.0.1 +io.ktor:ktor-sse-jvm:3.0.1 +io.ktor:ktor-sse:3.0.1 +io.ktor:ktor-utils-jvm:3.0.1 +io.ktor:ktor-utils:3.0.1 +io.ktor:ktor-websocket-serialization-jvm:3.0.1 +io.ktor:ktor-websocket-serialization:3.0.1 +io.ktor:ktor-websockets-jvm:3.0.1 +io.ktor:ktor-websockets:3.0.1 +org.jetbrains.androidx.core:core-bundle-android:1.0.1 +org.jetbrains.androidx.core:core-bundle:1.0.1 +org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.3 +org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.3 +org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.3 +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2 +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.2 +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.3 +org.jetbrains.androidx.navigation:navigation-common:2.8.0-alpha10 +org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10 +org.jetbrains.androidx.navigation:navigation-runtime:2.8.0-alpha10 +org.jetbrains.androidx.savedstate:savedstate:1.2.2 +org.jetbrains.androidx.window:window-core:1.3.0 +org.jetbrains.compose.animation:animation-core:1.7.0 +org.jetbrains.compose.animation:animation:1.7.0 +org.jetbrains.compose.annotation-internal:annotation:1.7.0 +org.jetbrains.compose.collection-internal:collection:1.7.0 +org.jetbrains.compose.components:components-resources-android:1.7.0 +org.jetbrains.compose.components:components-resources:1.7.0 +org.jetbrains.compose.foundation:foundation-layout:1.7.0 +org.jetbrains.compose.foundation:foundation:1.7.0 +org.jetbrains.compose.material3.adaptive:adaptive-layout:1.0.0 +org.jetbrains.compose.material3.adaptive:adaptive-navigation:1.0.0 +org.jetbrains.compose.material3.adaptive:adaptive:1.0.0 +org.jetbrains.compose.material3:material3-adaptive-navigation-suite:1.7.0 +org.jetbrains.compose.material3:material3:1.7.0 +org.jetbrains.compose.material:material-icons-core:1.7.0 +org.jetbrains.compose.material:material-icons-extended:1.7.0 +org.jetbrains.compose.material:material-ripple:1.7.0 +org.jetbrains.compose.runtime:runtime-saveable:1.7.0 +org.jetbrains.compose.runtime:runtime:1.7.0 +org.jetbrains.compose.ui:ui-geometry:1.7.0 +org.jetbrains.compose.ui:ui-graphics:1.7.0 +org.jetbrains.compose.ui:ui-text:1.7.0 +org.jetbrains.compose.ui:ui-tooling-preview:1.7.0 +org.jetbrains.compose.ui:ui-unit:1.7.0 +org.jetbrains.compose.ui:ui-util:1.7.0 +org.jetbrains.compose.ui:ui:1.7.0 +org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.22 +org.jetbrains.kotlin:kotlin-bom:1.8.22 +org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 +org.jetbrains.kotlinx:atomicfu-jvm:0.23.2 +org.jetbrains.kotlinx:atomicfu:0.23.2 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.9.0 +org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1 +org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 +org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.5.4 +org.jetbrains.kotlinx:kotlinx-io-bytestring:0.5.4 +org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.5.4 +org.jetbrains.kotlinx:kotlinx-io-core:0.5.4 +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json-io-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json-io:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 +org.jetbrains:annotations:23.0.0 +org.jetbrains:markdown-jvm:0.7.3 +org.jetbrains:markdown:0.7.3 +org.slf4j:slf4j-api:2.0.16 diff --git a/app/platform/android/keystore/dev.jks b/app/platform/android/keystore/dev.jks new file mode 100644 index 0000000000000000000000000000000000000000..2cd6779e457373f7e56066c2df95dc79c892bc96 GIT binary patch literal 2552 zcma)8S5y-U5={@m@aPf{2)#ZbGzmqeNC_CFi4+kMN>J&7GzCm3Dn~;R5Co)1Z;BK_ zq=b%0uhK-CND~Ai5MbT2@95dD-H(|$bMM^wn|lUBMr?zCUG`8KTTj=nV+%MApO)p~FM~O5AjEnvS^{7N z9KZx{?4p8{N10bM$AaK@-rD9Kv=xnUIN( zK>q!t9Z|ImDb_=w^=Gx#Inqzs(0Yf95s!l(RGEy_`h8yjJ;WRgz3LwM>a-_JNsG8& z3X?QTLR?;2rQE=eB6p=vc~HhbKP7PXG+BLviKXnAbO}+mP>)ScW4ik)Q1^K0YCv2> zaQ!Uev*Qvp44E4%u_s z5I0ik%1ku3obAMXY`17mX}UhgSg2tA30bJi8>DgN Qxr;d{Bhty5}_SR3VcIy&e zw{laJMH<3g^AEP%JsHyQxpWFJF2(;@hXK9QxqNA6t${JX2$$a6u|ajRpuXjXu2DZ+ zH!J7EgRTA2HQ+?wC!bo3Xv_~Dp0~-r4w0emOKUw-t4j8JVdvghWgqgqEdC+wlS=m} zV?b9D7CmEZ#O|W>W=5pAP4w}Vji=5aLwZT*KtPi<`s3lJt6{bk--qJuBa6F4ZLhAR z-3yaT)2eFX4dUO)>ZtpWkV!Pv#(|sJZ@C(N3%z*FVqjLcIP))B3~_FS%}Q?0+2EQ2 zFT!K!Z1gzEEs|hkH?x=cQfC3Zb=4vM-K_ak*_&&!@6Oze2uWri#&Y*|NA3>*HSvU0rEuk$Jb%p z<7IW6z|qtP*@K~Z?EUku?X z2FrurYi^AjisLGDf&|}Tk2l(!mMM)G-Y_k;K}>Px=Z)7S^t+;7e4A z-hFV@LidY%7)tz>wIC%Rl;nXro1>%&=9`-c+NivLCrLX)v{Er; zqtJPjxV(65-NJdp#>tX2#oZfaz}5nmk%GedRya@c7fD3RUYJC zeL$>s8+y9pocZdlGnbkhkQ-IR~8eUSa_FnV?;{P(ZO3kZI`kY<8t)oZP%*O zT2y9*ZW*>N4s~4fSOHKF7L*>*LHk}L`)X2Y^O0cg%)8R^WngSln=T z7E(}ERKqAMD`S<^&R{WQaMSN0I4pw9Uf`EWuCGQUd{9m@cC6TT-tOk##C(Ibm zw#`h=?!SES|Je3)(Bz?nGOY)#Ou{kw;R3_RU<{Ai@?F1h_v&d_DXv1`R)cw~eaD28 ze68ZDUM+Y1RG-LgjZR~R(q}R5FP3BLPMWilg%N?(S&Mb~mK#iK6z`+4No&k`EP79S zNu#&jk0PY8lpDbnk~@V|Y971XtyWXqjO)6^@y0mJ-r0-kW?MYXWj{}QNW>GL{_XnSK9`yI#Xv8S|Y-U(3S#FFxO6IE$ zjv6mGS9B>!?x%8kbB-DuWRyoJK zaB@1?_Z%_Yu}9!vXwbhqwr?z!0XXVIm+N$RQl(UKhsNjgYbTb2m)iYCWc0jmETIgf zRAaS*mE_nqt5Kt``vn?O7`b=IO#M=og+GC3haIezsx(Otqy6#=?(CiH%84CLV9C+p z$y?8xx~~CMTQ}yr>l!)D1k1J3<5<3w=vX23UWP$5$*fFMpAw!l=il i_&&|aS_s$7M3ncGhl#1gnkJ&7GzCm3Dn~;R5Co)1Z;BK_ zq=b%0uhK-CND~Ai5MbT2@95dD-H(|$bMM^wn|lUBMr?zCUG`8KTTj=nV+%MApO)p~FM~O5AjEnvS^{7N z9KZx{?4p8{N10bM$AaK@-rD9Kv=xnUIN( zK>q!t9Z|ImDb_=w^=Gx#Inqzs(0Yf95s!l(RGEy_`h8yjJ;WRgz3LwM>a-_JNsG8& z3X?QTLR?;2rQE=eB6p=vc~HhbKP7PXG+BLviKXnAbO}+mP>)ScW4ik)Q1^K0YCv2> zaQ!Uev*Qvp44E4%u_s z5I0ik%1ku3obAMXY`17mX}UhgSg2tA30bJi8>DgN Qxr;d{Bhty5}_SR3VcIy&e zw{laJMH<3g^AEP%JsHyQxpWFJF2(;@hXK9QxqNA6t${JX2$$a6u|ajRpuXjXu2DZ+ zH!J7EgRTA2HQ+?wC!bo3Xv_~Dp0~-r4w0emOKUw-t4j8JVdvghWgqgqEdC+wlS=m} zV?b9D7CmEZ#O|W>W=5pAP4w}Vji=5aLwZT*KtPi<`s3lJt6{bk--qJuBa6F4ZLhAR z-3yaT)2eFX4dUO)>ZtpWkV!Pv#(|sJZ@C(N3%z*FVqjLcIP))B3~_FS%}Q?0+2EQ2 zFT!K!Z1gzEEs|hkH?x=cQfC3Zb=4vM-K_ak*_&&!@6Oze2uWri#&Y*|NA3>*HSvU0rEuk$Jb%p z<7IW6z|qtP*@K~Z?EUku?X z2FrurYi^AjisLGDf&|}Tk2l(!mMM)G-Y_k;K}>Px=Z)7S^t+;7e4A z-hFV@LidY%7)tz>wIC%Rl;nXro1>%&=9`-c+NivLCrLX)v{Er; zqtJPjxV(65-NJdp#>tX2#oZfaz}5nmk%GedRya@c7fD3RUYJC zeL$>s8+y9pocZdlGnbkhkQ-IR~8eUSa_FnV?;{P(ZO3kZI`kY<8t)oZP%*O zT2y9*ZW*>N4s~4fSOHKF7L*>*LHk}L`)X2Y^O0cg%)8R^WngSln=T z7E(}ERKqAMD`S<^&R{WQaMSN0I4pw9Uf`EWuCGQUd{9m@cC6TT-tOk##C(Ibm zw#`h=?!SES|Je3)(Bz?nGOY)#Ou{kw;R3_RU<{Ai@?F1h_v&d_DXv1`R)cw~eaD28 ze68ZDUM+Y1RG-LgjZR~R(q}R5FP3BLPMWilg%N?(S&Mb~mK#iK6z`+4No&k`EP79S zNu#&jk0PY8lpDbnk~@V|Y971XtyWXqjO)6^@y0mJ-r0-kW?MYXWj{}QNW>GL{_XnSK9`yI#Xv8S|Y-U(3S#FFxO6IE$ zjv6mGS9B>!?x%8kbB-DuWRyoJK zaB@1?_Z%_Yu}9!vXwbhqwr?z!0XXVIm+N$RQl(UKhsNjgYbTb2m)iYCWc0jmETIgf zRAaS*mE_nqt5Kt``vn?O7`b=IO#M=og+GC3haIezsx(Otqy6#=?(CiH%84CLV9C+p z$y?8xx~~CMTQ}yr>l!)D1k1J3<5<3w=vX23UWP$5$*fFMpAw!l=il i_&&|aS_s$7M3ncGhl#1gnk + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/platform/android/src/main/ic_launcher-playstore.png b/app/platform/android/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..06287b973dad961ef1b1069ba989dddb6b08ee6e GIT binary patch literal 16890 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3eadd003 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Oct 26 22:47:54 KST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock new file mode 100644 index 00000000..92beced3 --- /dev/null +++ b/kotlin-js-store/yarn.lock @@ -0,0 +1,2845 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.5": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz#3c9997ae9d00bc236e45c6374e84f2596458d9db" + integrity sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.13": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.15" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=10.0.0": + version "22.8.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.1.tgz#b39d4b98165e2ae792ce213f610c7c6108ccfa16" + integrity sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg== + dependencies: + undici-types "~6.19.8" + +"@types/qs@*": + version "6.9.16" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" + integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.33": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.5": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.13.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" + integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.3, body-parser@^1.19.0: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.21.10: + version "4.24.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001669: + version "1.0.30001671" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001671.tgz#c660a8a0bf6bb8eedaac683d29074e455e84e3f1" + integrity sha512-jocyVaSSfXg2faluE6hrWkMgDOiULBMca4QLtDT39hw1YxaIPHWc1CcTCKkPmHgGH6tKji6ZNbMSmUAvENf2/A== + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.1, chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +connect@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@~2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== + +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== + +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +dom-serialize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.41: + version "1.5.47" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz#ef0751bc19b28be8ee44cd8405309de3bf3b20c7" + integrity sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + +engine.io@~6.6.0: + version "6.6.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.2.tgz#32bd845b4db708f8c774a4edef4e5c8a98b3da72" + integrity sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.7.2" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + +enhanced-resolve@^5.17.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +ent@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" + integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== + dependencies: + punycode "^1.4.1" + +envinfo@^7.7.3: + version "7.14.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.14.0.tgz#26dac5db54418f2a4c1159153a0b2ae980838aae" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.21.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" + integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.7: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +follow-redirects@^1.0.0: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +format-util@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.3, glob@^7.1.7: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isbinaryfile@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" + integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +karma-chrome-launcher@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" + integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== + dependencies: + which "^1.2.1" + +karma-mocha@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d" + integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ== + dependencies: + minimist "^1.2.3" + +karma-sourcemap-loader@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488" + integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA== + dependencies: + graceful-fs "^4.2.10" + +karma-webpack@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4" + integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ== + dependencies: + glob "^7.1.3" + minimatch "^9.0.3" + webpack-merge "^4.1.5" + +karma@6.4.3: + version "6.4.3" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.3.tgz#763e500f99597218bbb536de1a14acc4ceea7ce8" + integrity sha512-LuucC/RE92tJ8mlCwqEoRWXP38UMAqpnq98vktmS9SznSoUPPUJQbc91dHcxcunROvfQjdORVA/YFviH+Xci9Q== + dependencies: + "@colors/colors" "1.5.0" + body-parser "^1.19.0" + braces "^3.0.2" + chokidar "^3.5.1" + connect "^3.7.0" + di "^0.0.1" + dom-serialize "^2.2.1" + glob "^7.1.7" + graceful-fs "^4.2.6" + http-proxy "^1.18.1" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.4.1" + mime "^2.5.2" + minimatch "^3.0.4" + mkdirp "^0.5.5" + qjobs "^1.2.0" + range-parser "^1.2.1" + rimraf "^3.0.2" + socket.io "^4.7.2" + source-map "^0.6.1" + tmp "^0.2.1" + ua-parser-js "^0.7.30" + yargs "^16.1.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +launch-editor@^2.6.0: + version "2.9.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.9.1.tgz#253f173bd441e342d4344b4dae58291abb425047" + integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log4js@^6.4.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.0.tgz#9e5cbed8fa9b37537a25bd1f7fb4f6fc45458b9a" + integrity sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +picocolors@^1.0.0, picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qjobs@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rfdc@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +socket.io-adapter@~2.5.2: + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== + dependencies: + debug "~4.3.4" + ws "~8.17.1" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.2: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" + integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-loader@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38" + integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA== + dependencies: + iconv-lite "^0.6.3" + source-map-js "^1.0.2" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.36.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.36.0.tgz#8b0dbed459ac40ff7b4c9fd5a3a2029de105180e" + integrity sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tmp@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +ua-parser-js@^0.7.30: + version "0.7.39" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.39.tgz#c71efb46ebeabc461c4612d22d54f88880fabe7e" + integrity sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w== + +undici-types@~6.19.8: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@4.15.2: + version "4.15.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.4" + ws "^8.13.0" + +webpack-merge@^4.1.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.93.0: + version "5.93.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.93.0.tgz#2e89ec7035579bdfba9760d26c63ac5c3462a5e5" + integrity sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^20.2.2, yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.1.1, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/library/color/README.md b/library/color/README.md new file mode 100644 index 00000000..13c29c35 --- /dev/null +++ b/library/color/README.md @@ -0,0 +1,3 @@ +# :library:color module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_color.svg) diff --git a/library/color/build.gradle.kts b/library/color/build.gradle.kts new file mode 100644 index 00000000..dda0564c --- /dev/null +++ b/library/color/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.compose") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(compose.ui) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.library.color" +} diff --git a/library/color/src/commonMain/kotlin/io/github/taetae98coding/diary/library/color/ColorExt.kt b/library/color/src/commonMain/kotlin/io/github/taetae98coding/diary/library/color/ColorExt.kt new file mode 100644 index 00000000..6fd29bee --- /dev/null +++ b/library/color/src/commonMain/kotlin/io/github/taetae98coding/diary/library/color/ColorExt.kt @@ -0,0 +1,39 @@ +package io.github.taetae98coding.diary.library.color + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import kotlin.random.Random +import kotlin.random.nextLong + +public fun Color.multiplyAlpha(value: Float): Color { + return copy(alpha * value) +} + +public fun Color.toContrastColor(): Color { + return if (getColorContrast(Color.Black, this) >= getColorContrast(Color.White, this) + 5) { + Color.Black + } else { + Color.White + } +} + +private fun getColorContrast( + color1: Color, + color2: Color, +): Double { + val luminance1 = color1.luminance() + val luminance2 = color2.luminance() + val bright = maxOf(luminance1, luminance2) + val dark = minOf(luminance1, luminance2) + + return (bright + 0.05) / (dark + 0.05) +} + +public fun randomArgb(): Int { + return (Random.nextLong(0x000000L..0xFFFFFFL) + 0xFF000000L).toInt() +} + +@OptIn(ExperimentalStdlibApi::class) +public fun Int.toRgbString(): String { + return runCatching { toHexString(HexFormat.UpperCase).substring(2..7) }.getOrNull().orEmpty() +} diff --git a/library/coroutines/README.md b/library/coroutines/README.md new file mode 100644 index 00000000..ad38e86b --- /dev/null +++ b/library/coroutines/README.md @@ -0,0 +1,3 @@ +# :library:coroutines module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_coroutines.svg) diff --git a/library/coroutines/build.gradle.kts b/library/coroutines/build.gradle.kts new file mode 100644 index 00000000..b0077266 --- /dev/null +++ b/library/coroutines/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.coroutines.core) + } + } + } +} diff --git a/library/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/library/coroutines/FlowExt.kt b/library/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/library/coroutines/FlowExt.kt new file mode 100644 index 00000000..73e8fee8 --- /dev/null +++ b/library/coroutines/src/commonMain/kotlin/io/github/taetae98coding/diary/library/coroutines/FlowExt.kt @@ -0,0 +1,32 @@ +package io.github.taetae98coding.diary.library.coroutines + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +public fun Flow>.mapCollectionLatest( + transform: suspend (T) -> R, +): Flow> { + return mapLatest { collection -> + collection.map { transform(it) } + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +public fun Flow>.filterCollectionLatest( + predicate: suspend (T) -> Boolean +): Flow> { + return mapLatest { collection -> + collection.filter { predicate(it) } + } +} + +public inline fun List>.combine( + crossinline transform: suspend (Array) -> R, +): Flow { + return combine(this) { array -> + transform(array) + } +} diff --git a/library/datetime/README.md b/library/datetime/README.md new file mode 100644 index 00000000..fdaed518 --- /dev/null +++ b/library/datetime/README.md @@ -0,0 +1,3 @@ +# :library:datetime module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_datetime.svg) diff --git a/library/datetime/build.gradle.kts b/library/datetime/build.gradle.kts new file mode 100644 index 00000000..79aa8585 --- /dev/null +++ b/library/datetime/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(libs.kotlinx.datetime) + } + } + } +} diff --git a/library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/DayOfWeekExt.kt b/library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/DayOfWeekExt.kt new file mode 100644 index 00000000..95423ea9 --- /dev/null +++ b/library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/DayOfWeekExt.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.library.datetime + +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.isoDayNumber + +public fun Int.toChristDayOfWeek(): DayOfWeek { + return if (this % 7 == 0) { + DayOfWeek.SUNDAY + } else { + DayOfWeek(this) + } +} + +public val DayOfWeek.christ: Int + get() = isoDayNumber % 7 diff --git a/library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/LocalDateExt.kt b/library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/LocalDateExt.kt new file mode 100644 index 00000000..bab19e3e --- /dev/null +++ b/library/datetime/src/commonMain/kotlin/io/github/taetae98coding/diary/library/datetime/LocalDateExt.kt @@ -0,0 +1,43 @@ +package io.github.taetae98coding.diary.library.datetime + +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.todayIn + +public fun LocalDate.Companion.todayIn( + clock: Clock = Clock.System, + timeZone: TimeZone = TimeZone.currentSystemDefault(), +): LocalDate { + return clock.todayIn(timeZone) +} + +public fun LocalDate.toTimeInMillis(): Long { + return atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds() +} + +public fun Long.toLocalDate(): LocalDate { + return Instant.fromEpochMilliseconds(this) + .toLocalDateTime(TimeZone.UTC) + .date +} + +public operator fun LocalDate.Companion.invoke(year: Int, month: Month, weekOfMonth: Int, dayOfWeek: DayOfWeek): LocalDate { + val date = LocalDate(year, month, 1) + + return date.minus(date.dayOfWeek.christ, DateTimeUnit.DAY) + .plus(weekOfMonth, DateTimeUnit.WEEK) + .plus(dayOfWeek.christ, DateTimeUnit.DAY) +} + +public fun ClosedRange.isOverlap(range: ClosedRange): Boolean { + return start <= range.endInclusive && endInclusive >= range.start +} diff --git a/library/koin-datastore/README.md b/library/koin-datastore/README.md new file mode 100644 index 00000000..83374967 --- /dev/null +++ b/library/koin-datastore/README.md @@ -0,0 +1,3 @@ +# :library:koin-datastore module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_koin_datastore.svg) diff --git a/library/koin-datastore/build.gradle.kts b/library/koin-datastore/build.gradle.kts new file mode 100644 index 00000000..9fb72ac9 --- /dev/null +++ b/library/koin-datastore/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("diary.datastore") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.library.koin.datastore" +} diff --git a/library/koin-datastore/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.android.kt b/library/koin-datastore/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.android.kt new file mode 100644 index 00000000..ad28b773 --- /dev/null +++ b/library/koin-datastore/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.android.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.library.koin.datastore + +import android.content.Context +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +internal actual fun KoinComponent.getDataStoreAbsolutePath(name: String): String { + val context by inject() + + return context.filesDir.resolve(name).absolutePath +} diff --git a/library/koin-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.kt b/library/koin-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.kt new file mode 100644 index 00000000..7c93f113 --- /dev/null +++ b/library/koin-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.library.koin.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import okio.Path.Companion.toPath +import org.koin.core.component.KoinComponent + +public fun KoinComponent.getDataStore(name: String): DataStore { + return PreferenceDataStoreFactory.createWithPath( + produceFile = { getDataStoreAbsolutePath(name).toPath() }, + ) +} + +internal expect fun KoinComponent.getDataStoreAbsolutePath(name: String): String diff --git a/library/koin-datastore/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt b/library/koin-datastore/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt new file mode 100644 index 00000000..7109ba0d --- /dev/null +++ b/library/koin-datastore/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.ios.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.library.koin.datastore + +import kotlinx.cinterop.ExperimentalForeignApi +import org.koin.core.component.KoinComponent +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSURL +import platform.Foundation.NSUserDomainMask + +@OptIn(ExperimentalForeignApi::class) +internal actual fun KoinComponent.getDataStoreAbsolutePath(name: String): String { + val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + )?.also { +// NSFileManager.defaultManager.removeItemAtURL(it, null) + } + + return requireNotNull(documentDirectory).path + "/$name" +} diff --git a/library/koin-datastore/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.jvm.kt b/library/koin-datastore/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.jvm.kt new file mode 100644 index 00000000..818105f0 --- /dev/null +++ b/library/koin-datastore/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/datastore/DataStoreExt.jvm.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.library.koin.datastore + +import java.io.File +import org.koin.core.component.KoinComponent + +public var koinDataStoreDefaultPath: String = System.getProperty("user.home") + +internal actual fun KoinComponent.getDataStoreAbsolutePath(name: String): String { + runCatching { File("$koinDataStoreDefaultPath/$name").parentFile?.mkdirs() } + return "$koinDataStoreDefaultPath/$name" +} diff --git a/library/koin-room/README.md b/library/koin-room/README.md new file mode 100644 index 00000000..e5e5cc2f --- /dev/null +++ b/library/koin-room/README.md @@ -0,0 +1,3 @@ +# :library:koin-room module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_koin_room.svg) diff --git a/library/koin-room/build.gradle.kts b/library/koin-room/build.gradle.kts new file mode 100644 index 00000000..57e96363 --- /dev/null +++ b/library/koin-room/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("diary.room") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.library.koin.room" +} diff --git a/library/koin-room/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.android.kt b/library/koin-room/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.android.kt new file mode 100644 index 00000000..19dd80ec --- /dev/null +++ b/library/koin-room/src/androidMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.android.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.library.koin.room + +import android.content.Context +import androidx.room.Room +import androidx.room.RoomDatabase +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +public actual inline fun KoinComponent.platformDatabaseBuilder( + name: String, +): RoomDatabase.Builder { + val context by inject() + val file = context.getDatabasePath(name) + + return Room.databaseBuilder(context, file.absolutePath) +} diff --git a/library/koin-room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.kt b/library/koin-room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.kt new file mode 100644 index 00000000..25a596d5 --- /dev/null +++ b/library/koin-room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.library.koin.room + +import androidx.room.RoomDatabase +import androidx.sqlite.driver.bundled.BundledSQLiteDriver +import org.koin.core.component.KoinComponent + +public inline fun KoinComponent.getDatabaseBuilder( + name: String, +): RoomDatabase.Builder { + return platformDatabaseBuilder(name) + .setDriver(BundledSQLiteDriver()) +} + +public expect inline fun KoinComponent.platformDatabaseBuilder( + name: String, +): RoomDatabase.Builder diff --git a/library/koin-room/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt b/library/koin-room/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt new file mode 100644 index 00000000..b69e3ef7 --- /dev/null +++ b/library/koin-room/src/iosMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.ios.kt @@ -0,0 +1,26 @@ +package io.github.taetae98coding.diary.library.koin.room + +import androidx.room.Room +import androidx.room.RoomDatabase +import kotlinx.cinterop.ExperimentalForeignApi +import org.koin.core.component.KoinComponent +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSUserDomainMask + +@OptIn(ExperimentalForeignApi::class) +public actual inline fun KoinComponent.platformDatabaseBuilder( + name: String, +): RoomDatabase.Builder { + val documentDirectory = NSFileManager.defaultManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + )?.also { +// NSFileManager.defaultManager.removeItemAtURL(it, null) + } + + return Room.databaseBuilder(name = "${documentDirectory?.path}/$name") +} diff --git a/library/koin-room/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.jvm.kt b/library/koin-room/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.jvm.kt new file mode 100644 index 00000000..52154f0c --- /dev/null +++ b/library/koin-room/src/jvmMain/kotlin/io/github/taetae98coding/diary/library/koin/room/RoomExt.jvm.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.library.koin.room + +import androidx.room.Room +import androidx.room.RoomDatabase +import java.io.File +import org.koin.core.component.KoinComponent + +public var koinRoomDefaultPath: String = System.getProperty("user.home") + +public actual inline fun KoinComponent.platformDatabaseBuilder( + name: String, +): RoomDatabase.Builder { + runCatching { File("$koinRoomDefaultPath/$name").parentFile?.mkdirs() } + return Room.databaseBuilder(name = "$koinRoomDefaultPath/$name") +} diff --git a/library/kotlin/README.md b/library/kotlin/README.md new file mode 100644 index 00000000..d8832113 --- /dev/null +++ b/library/kotlin/README.md @@ -0,0 +1,3 @@ +# :library:kotlin module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_kotlin.svg) diff --git a/library/kotlin/build.gradle.kts b/library/kotlin/build.gradle.kts new file mode 100644 index 00000000..a26f4424 --- /dev/null +++ b/library/kotlin/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} diff --git a/library/kotlin/src/commonMain/kotlin/io/github/taetae98coding/diary/library/kotlin/regex/RegexExt.kt b/library/kotlin/src/commonMain/kotlin/io/github/taetae98coding/diary/library/kotlin/regex/RegexExt.kt new file mode 100644 index 00000000..adc6951e --- /dev/null +++ b/library/kotlin/src/commonMain/kotlin/io/github/taetae98coding/diary/library/kotlin/regex/RegexExt.kt @@ -0,0 +1,5 @@ +package io.github.taetae98coding.diary.library.kotlin.regex + +public fun Regex.Companion.email(): Regex { + return Regex("[a-z0-9]+@[a-z0-9]+\\.[a-z0-9]+") +} diff --git a/library/navigation/README.md b/library/navigation/README.md new file mode 100644 index 00000000..6caf1fca --- /dev/null +++ b/library/navigation/README.md @@ -0,0 +1,3 @@ +# :library:navigation module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_navigation.svg) diff --git a/library/navigation/build.gradle.kts b/library/navigation/build.gradle.kts new file mode 100644 index 00000000..4fdefb05 --- /dev/null +++ b/library/navigation/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:datetime")) + api(libs.navigation.common) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.library.navigation" +} diff --git a/library/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/library/navigation/LocalDateNavType.kt b/library/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/library/navigation/LocalDateNavType.kt new file mode 100644 index 00000000..c1d2b893 --- /dev/null +++ b/library/navigation/src/commonMain/kotlin/io/github/taetae98coding/diary/library/navigation/LocalDateNavType.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.library.navigation + +import androidx.core.bundle.Bundle +import androidx.navigation.NavType +import kotlinx.datetime.LocalDate + +public data object LocalDateNavType : NavType(true) { + override fun get(bundle: Bundle, key: String): LocalDate? { + return bundle.getString(key)?.let { LocalDate.parse(it) } + } + + override fun parseValue(value: String): LocalDate { + return LocalDate.parse(value) + } + + override fun put(bundle: Bundle, key: String, value: LocalDate) { + bundle.putString(key, value.toString()) + } +} diff --git a/library/room/README.md b/library/room/README.md new file mode 100644 index 00000000..476b475f --- /dev/null +++ b/library/room/README.md @@ -0,0 +1,3 @@ +# :library:room module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_room.svg) diff --git a/library/room/build.gradle.kts b/library/room/build.gradle.kts new file mode 100644 index 00000000..e1db087f --- /dev/null +++ b/library/room/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("diary.room") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:datetime")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.library.room" +} diff --git a/library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/InstantConverter.kt b/library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/InstantConverter.kt new file mode 100644 index 00000000..541b5200 --- /dev/null +++ b/library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/InstantConverter.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.library.room + +import androidx.room.TypeConverter +import kotlinx.datetime.Instant + +public data object InstantConverter { + @TypeConverter + public fun longToInstant(value: Long?): Instant? { + return value?.let { Instant.fromEpochMilliseconds(it) } + } + + @TypeConverter + public fun instantToLong(instant: Instant?): Long? { + return instant?.toEpochMilliseconds() + } +} diff --git a/library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/LocalDataConverter.kt b/library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/LocalDataConverter.kt new file mode 100644 index 00000000..1ff435a5 --- /dev/null +++ b/library/room/src/commonMain/kotlin/io/github/taetae98coding/diary/library/room/LocalDataConverter.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.library.room + +import androidx.room.TypeConverter +import kotlinx.datetime.LocalDate + +public data object LocalDataConverter { + @TypeConverter + public fun stringToLocalDate(value: String?): LocalDate? { + return value?.let { LocalDate.parse(it) } + } + + @TypeConverter + public fun localDateToString(value: LocalDate?): String? { + return value?.toString() + } +} diff --git a/library/shimmer-m3/README.md b/library/shimmer-m3/README.md new file mode 100644 index 00000000..f8641e59 --- /dev/null +++ b/library/shimmer-m3/README.md @@ -0,0 +1,3 @@ +# :library:shimmer-m3 module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_library_shimmer_m3.svg) diff --git a/library/shimmer-m3/build.gradle.kts b/library/shimmer-m3/build.gradle.kts new file mode 100644 index 00000000..934061de --- /dev/null +++ b/library/shimmer-m3/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("diary.android.library") + id("diary.kotlin.multiplatform.all") + id("diary.compose") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:color")) + implementation(compose.material3) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.library.shimmer.m3" +} diff --git a/library/shimmer-m3/src/commonMain/kotlin/io/github/taetae98coding/diary/library/shimmer/m3/ShimmerModifier.kt b/library/shimmer-m3/src/commonMain/kotlin/io/github/taetae98coding/diary/library/shimmer/m3/ShimmerModifier.kt new file mode 100644 index 00000000..dbc88101 --- /dev/null +++ b/library/shimmer-m3/src/commonMain/kotlin/io/github/taetae98coding/diary/library/shimmer/m3/ShimmerModifier.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.library.shimmer.m3 + +import androidx.compose.foundation.background +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.taetae98coding.diary.library.color.multiplyAlpha + +@Composable +public fun Modifier.shimmer(): Modifier { + return background(color = LocalContentColor.current.multiplyAlpha(0.38F)) +} diff --git a/server/app/README.md b/server/app/README.md new file mode 100644 index 00000000..c98dc067 --- /dev/null +++ b/server/app/README.md @@ -0,0 +1,3 @@ +# :server:app module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_server_app.svg) diff --git a/server/app/build.gradle.kts b/server/app/build.gradle.kts new file mode 100644 index 00000000..9f2cc389 --- /dev/null +++ b/server/app/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("diary.kotlin.jvm") + alias(libs.plugins.ktor.server) + alias(libs.plugins.dependency.guard) +} + +application { + mainClass.set("io.ktor.server.netty.EngineMain") +} + +dependencies { + implementation(project(":server:core:database")) + + implementation(project(":server:data:account")) + implementation(project(":server:data:memo")) + + implementation(project(":server:domain:account")) + implementation(project(":server:domain:memo")) + + implementation(project(":server:feature:home")) + implementation(project(":server:feature:account")) + implementation(project(":server:feature:memo")) + + implementation(project(":common:model")) + + implementation(libs.bundles.ktor.server) + implementation(libs.logback.classic) + + implementation(platform(libs.koin.bom)) + implementation(libs.koin.ktor) + + implementation(platform(libs.exposed.bom)) + implementation(libs.exposed.jdbc) + implementation(libs.mysql.connector) +} + +dependencyGuard { + configuration("runtimeClasspath") +} diff --git a/server/app/dependencies/runtimeClasspath.txt b/server/app/dependencies/runtimeClasspath.txt new file mode 100644 index 00000000..2bb54f1e --- /dev/null +++ b/server/app/dependencies/runtimeClasspath.txt @@ -0,0 +1,119 @@ +ch.qos.logback:logback-classic:1.5.12 +ch.qos.logback:logback-core:1.5.12 +ch.randelshofer:fastdoubleparser:0.8.0 +co.touchlab:stately-concurrency-jvm:2.1.0 +co.touchlab:stately-concurrency:2.1.0 +co.touchlab:stately-concurrent-collections-jvm:2.1.0 +co.touchlab:stately-concurrent-collections:2.1.0 +co.touchlab:stately-strict-jvm:2.1.0 +co.touchlab:stately-strict:2.1.0 +com.auth0:java-jwt:4.4.0 +com.auth0:jwks-rsa:0.22.1 +com.fasterxml.jackson.core:jackson-annotations:2.15.0 +com.fasterxml.jackson.core:jackson-core:2.15.0 +com.fasterxml.jackson.core:jackson-databind:2.15.0 +com.fasterxml.jackson:jackson-bom:2.15.0 +com.google.code.findbugs:jsr305:3.0.2 +com.google.errorprone:error_prone_annotations:2.18.0 +com.google.guava:failureaccess:1.0.1 +com.google.guava:guava-parent:32.1.1-jre +com.google.guava:guava:32.1.1-jre +com.google.protobuf:protobuf-java:3.21.9 +com.mysql:mysql-connector-j:8.0.33 +com.typesafe:config:1.4.3 +io.insert-koin:koin-annotations-bom:2.0.0-Beta1 +io.insert-koin:koin-annotations-jvm:2.0.0-Beta1 +io.insert-koin:koin-annotations:2.0.0-Beta1 +io.insert-koin:koin-bom:4.0.0 +io.insert-koin:koin-core-jvm:4.0.0 +io.insert-koin:koin-core:4.0.0 +io.insert-koin:koin-ktor:4.0.0 +io.ktor:ktor-bom:3.0.1 +io.ktor:ktor-client-core-jvm:3.0.1 +io.ktor:ktor-client-core:3.0.1 +io.ktor:ktor-events-jvm:3.0.1 +io.ktor:ktor-events:3.0.1 +io.ktor:ktor-http-cio-jvm:3.0.1 +io.ktor:ktor-http-cio:3.0.1 +io.ktor:ktor-http-jvm:3.0.1 +io.ktor:ktor-http:3.0.1 +io.ktor:ktor-io-jvm:3.0.1 +io.ktor:ktor-io:3.0.1 +io.ktor:ktor-network-jvm:3.0.1 +io.ktor:ktor-network:3.0.1 +io.ktor:ktor-serialization-jvm:3.0.1 +io.ktor:ktor-serialization-kotlinx-json-jvm:3.0.1 +io.ktor:ktor-serialization-kotlinx-json:3.0.1 +io.ktor:ktor-serialization-kotlinx-jvm:3.0.1 +io.ktor:ktor-serialization-kotlinx:3.0.1 +io.ktor:ktor-serialization:3.0.1 +io.ktor:ktor-server-auth-jvm:3.0.1 +io.ktor:ktor-server-auth-jwt-jvm:3.0.1 +io.ktor:ktor-server-auth-jwt:3.0.1 +io.ktor:ktor-server-auth:3.0.1 +io.ktor:ktor-server-config-yaml-jvm:3.0.1 +io.ktor:ktor-server-config-yaml:3.0.1 +io.ktor:ktor-server-content-negotiation-jvm:3.0.1 +io.ktor:ktor-server-content-negotiation:3.0.1 +io.ktor:ktor-server-core-jvm:3.0.1 +io.ktor:ktor-server-core:3.0.1 +io.ktor:ktor-server-cors-jvm:3.0.1 +io.ktor:ktor-server-cors:3.0.1 +io.ktor:ktor-server-netty-jvm:3.0.1 +io.ktor:ktor-server-netty:3.0.1 +io.ktor:ktor-server-sessions-jvm:3.0.1 +io.ktor:ktor-server-sessions:3.0.1 +io.ktor:ktor-sse-jvm:3.0.1 +io.ktor:ktor-sse:3.0.1 +io.ktor:ktor-utils-jvm:3.0.1 +io.ktor:ktor-utils:3.0.1 +io.ktor:ktor-websocket-serialization-jvm:3.0.1 +io.ktor:ktor-websocket-serialization:3.0.1 +io.ktor:ktor-websockets-jvm:3.0.1 +io.ktor:ktor-websockets:3.0.1 +io.netty:netty-buffer:4.1.114.Final +io.netty:netty-codec-http2:4.1.114.Final +io.netty:netty-codec-http:4.1.114.Final +io.netty:netty-codec:4.1.114.Final +io.netty:netty-common:4.1.114.Final +io.netty:netty-handler:4.1.114.Final +io.netty:netty-resolver:4.1.114.Final +io.netty:netty-transport-classes-epoll:4.1.114.Final +io.netty:netty-transport-classes-kqueue:4.1.114.Final +io.netty:netty-transport-native-epoll:4.1.114.Final +io.netty:netty-transport-native-kqueue:4.1.114.Final +io.netty:netty-transport-native-unix-common:4.1.114.Final +io.netty:netty-transport:4.1.114.Final +mysql:mysql-connector-java:8.0.33 +net.mamoe.yamlkt:yamlkt-jvm:0.13.0 +net.mamoe.yamlkt:yamlkt:0.13.0 +org.checkerframework:checker-qual:3.33.0 +org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 +org.fusesource.jansi:jansi:2.4.1 +org.jetbrains.exposed:exposed-bom:0.56.0 +org.jetbrains.exposed:exposed-core:0.56.0 +org.jetbrains.exposed:exposed-jdbc:0.56.0 +org.jetbrains.exposed:exposed-kotlin-datetime:0.56.0 +org.jetbrains.kotlin:kotlin-reflect:2.0.21 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0 +org.jetbrains.kotlin:kotlin-stdlib:2.0.21 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0 +org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.9.0 +org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.1 +org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 +org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.5.4 +org.jetbrains.kotlinx:kotlinx-io-bytestring:0.5.4 +org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.5.4 +org.jetbrains.kotlinx:kotlinx-io-core:0.5.4 +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json-io-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json-io:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3 +org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3 +org.jetbrains:annotations:23.0.0 +org.slf4j:slf4j-api:2.0.16 diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/Application.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/Application.kt new file mode 100644 index 00000000..3a4e228e --- /dev/null +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/Application.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary + +import io.github.taetae98coding.diary.plugin.installAuth +import io.github.taetae98coding.diary.plugin.installCORS +import io.github.taetae98coding.diary.plugin.installContentNegotiation +import io.github.taetae98coding.diary.plugin.installDatabase +import io.github.taetae98coding.diary.plugin.installKoin +import io.github.taetae98coding.diary.plugin.installRouting +import io.ktor.server.application.Application +import io.ktor.server.netty.EngineMain + +public fun main(args: Array) { + EngineMain.main(args) +} + +public fun Application.module() { + installCORS() + installDatabase() + installKoin() + installAuth() + installContentNegotiation() + installRouting() +} diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/AuthPlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/AuthPlugin.kt new file mode 100644 index 00000000..b83f35a6 --- /dev/null +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/AuthPlugin.kt @@ -0,0 +1,37 @@ +package io.github.taetae98coding.diary.plugin + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.github.taetae98coding.diary.common.model.response.DiaryResponse +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.jwt.JWTPrincipal +import io.ktor.server.auth.jwt.jwt +import io.ktor.server.response.respond + +internal fun Application.installAuth() { + install(Authentication) { + jwt("account") { + val verifier = + JWT + .require(Algorithm.HMAC256("secret")) + .build() + + verifier(verifier) + + validate { credential -> + if (credential.payload.getClaim("uid").asString() != "") { + JWTPrincipal(credential.payload) + } else { + null + } + } + + challenge { _, _ -> + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + } + } + } +} diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/CORSPlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/CORSPlugin.kt new file mode 100644 index 00000000..0a4dd78d --- /dev/null +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/CORSPlugin.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.plugin + +import io.ktor.http.HttpHeaders +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.plugins.cors.routing.CORS + +internal fun Application.installCORS() { + install(CORS) { + anyHost() + allowHeader(HttpHeaders.ContentType) + } +} diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/ContentNegotiationPlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/ContentNegotiationPlugin.kt new file mode 100644 index 00000000..75d9766f --- /dev/null +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/ContentNegotiationPlugin.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.plugin + +import io.ktor.serialization.kotlinx.json.DefaultJson +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import kotlinx.serialization.json.Json + +internal fun Application.installContentNegotiation() { + install(ContentNegotiation) { + json( + json = + Json(DefaultJson) { + ignoreUnknownKeys = true + }, + ) + } +} diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt new file mode 100644 index 00000000..56fa0def --- /dev/null +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/DatabasePlugin.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.plugin + +import io.github.taetae98coding.diary.core.database.AccountTable +import io.github.taetae98coding.diary.core.database.MemoTable +import io.ktor.server.application.Application +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.transaction + +internal fun Application.installDatabase() { + val database = + Database.connect( + url = environment.config.property("database.url").getString(), + driver = "com.mysql.cj.jdbc.Driver", + user = environment.config.property("database.user").getString(), + password = environment.config.property("database.password").getString(), + ) + + transaction(database) { + SchemaUtils.createMissingTablesAndColumns(AccountTable, MemoTable) + } +} diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt new file mode 100644 index 00000000..24f8a0d5 --- /dev/null +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/KoinPlugin.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.plugin + +import io.github.taetae98coding.diary.data.account.AccountDataModule +import io.github.taetae98coding.diary.data.memo.MemoDataModule +import io.github.taetae98coding.diary.domain.account.AccountDomainModule +import io.github.taetae98coding.diary.domain.memo.MemoDomainModule +import io.ktor.server.application.Application +import io.ktor.server.application.install +import org.koin.ksp.generated.module +import org.koin.ktor.plugin.Koin + +internal fun Application.installKoin() { + install(Koin) { + modules( + AccountDataModule().module, + MemoDataModule().module, + AccountDomainModule().module, + MemoDomainModule().module, + ) + } +} diff --git a/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt new file mode 100644 index 00000000..ad0d347a --- /dev/null +++ b/server/app/src/main/kotlin/io/github/taetae98coding/diary/plugin/RoutingPlugin.kt @@ -0,0 +1,15 @@ +package io.github.taetae98coding.diary.plugin + +import io.github.taetae98coding.diary.feature.account.accountRouting +import io.github.taetae98coding.diary.feature.home.homeRouting +import io.github.taetae98coding.diary.feature.memo.memoRouting +import io.ktor.server.application.Application +import io.ktor.server.routing.routing + +internal fun Application.installRouting() { + routing { + homeRouting() + accountRouting() + memoRouting() + } +} diff --git a/server/app/src/main/resources/application.yaml b/server/app/src/main/resources/application.yaml new file mode 100644 index 00000000..af5854df --- /dev/null +++ b/server/app/src/main/resources/application.yaml @@ -0,0 +1,11 @@ +ktor: + deployment: + port: 8081 + application: + modules: + - io.github.taetae98coding.diary.ApplicationKt.module + +database: + url: $DIARY_DATABASE_URL + user: $DIARY_DATABASE_USER + password: $DIARY_DATABASE_PASSWORD \ No newline at end of file diff --git a/server/core/database/README.md b/server/core/database/README.md new file mode 100644 index 00000000..1d977d22 --- /dev/null +++ b/server/core/database/README.md @@ -0,0 +1,3 @@ +# :server:core:database module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_core_database.svg) diff --git a/server/core/database/build.gradle.kts b/server/core/database/build.gradle.kts new file mode 100644 index 00000000..b2f89782 --- /dev/null +++ b/server/core/database/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("diary.kotlin.jvm") +} + +dependencies { + implementation(project(":server:core:model")) + + implementation(platform(libs.exposed.bom)) + implementation(libs.exposed.core) + implementation(libs.exposed.datetime) +} diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt new file mode 100644 index 00000000..26cc9b93 --- /dev/null +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/AccountTable.kt @@ -0,0 +1,51 @@ +package io.github.taetae98coding.diary.core.database + +import io.github.taetae98coding.diary.core.model.Account +import kotlinx.coroutines.Dispatchers +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction + +public data object AccountTable : Table(name = "Account") { + private val EMAIL = varchar("email", 255).default("") + private val PASSWORD = varchar("password", 255).default("") + + internal val UID = varchar("uid", 255).default("").uniqueIndex() + + override val primaryKey: PrimaryKey = PrimaryKey(EMAIL) + + public suspend fun contains(email: String): Boolean = + newSuspendedTransaction { + selectAll() + .where { EMAIL eq email } + .any() + } + + public suspend fun insert(account: Account) { + newSuspendedTransaction { + insert { + it[EMAIL] = account.email + it[PASSWORD] = account.password + it[UID] = account.uid + } + } + } + + public suspend fun findByEmail(email: String, password: String): Account? = + newSuspendedTransaction { + selectAll() + .where { (EMAIL eq email) and (PASSWORD eq password) } + .firstOrNull() + ?.toAccount() + } + + private fun ResultRow.toAccount(): Account = + Account( + email = get(EMAIL), + password = get(PASSWORD), + uid = get(UID), + ) +} diff --git a/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt new file mode 100644 index 00000000..e6b4e907 --- /dev/null +++ b/server/core/database/src/main/kotlin/io/github/taetae98coding/diary/core/database/MemoTable.kt @@ -0,0 +1,83 @@ +package io.github.taetae98coding.diary.core.database + +import io.github.taetae98coding.diary.core.model.Memo +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.kotlin.datetime.date +import org.jetbrains.exposed.sql.kotlin.datetime.timestamp +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.upsert + +public data object MemoTable : Table(name = "Memo") { + private val ID = varchar("id", 255).default("") + private val TITLE = varchar("title", 255).default("") + private val DESCRIPTION = text("description") + private val START = date("start").nullable().default(null) + private val END_INCLUSIVE = date("endInclusive").nullable().default(null) + private val COLOR = integer("color").default(0xFFFFFFFF.toInt()) + private val OWNER = + reference( + name = "owner", + refColumn = AccountTable.UID, + onDelete = ReferenceOption.CASCADE, + onUpdate = ReferenceOption.CASCADE, + ) + private val IS_FINISH = bool("isFinish").default(false) + private val IS_DELETE = bool("isDelete").default(false) + private val UPDATE_AT = timestamp("updateAt").default(Clock.System.now()) + + override val primaryKey: PrimaryKey = PrimaryKey(ID) + + public suspend fun upsert(list: List) { + newSuspendedTransaction { + list.forEach { memo -> + upsert { + it[ID] = memo.id + it[TITLE] = memo.title + it[DESCRIPTION] = memo.description + it[START] = memo.start + it[END_INCLUSIVE] = memo.endInclusive + it[COLOR] = memo.color + it[OWNER] = memo.owner + it[IS_FINISH] = memo.isFinish + it[IS_DELETE] = memo.isDelete + it[UPDATE_AT] = memo.updateAt + } + } + } + } + + public suspend fun findByIds(list: List): List = + newSuspendedTransaction { + selectAll() + .where { ID.inList(list) } + .map { it.toMemo() } + } + + public suspend fun findByUpdateAt(uid: String, updateAt: Instant): List = + newSuspendedTransaction { + selectAll() + .where { (OWNER eq uid) and (UPDATE_AT greater updateAt) } + .limit(50) + .map { it.toMemo() } + } + + private fun ResultRow.toMemo(): Memo = + Memo( + id = get(ID), + title = get(TITLE), + description = get(DESCRIPTION), + start = get(START), + endInclusive = get(END_INCLUSIVE), + color = get(COLOR), + owner = get(OWNER), + isFinish = get(IS_FINISH), + isDelete = get(IS_DELETE), + updateAt = get(UPDATE_AT), + ) +} diff --git a/server/core/model/README.md b/server/core/model/README.md new file mode 100644 index 00000000..79b66e09 --- /dev/null +++ b/server/core/model/README.md @@ -0,0 +1,3 @@ +# :server:core:model module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_core_model.svg) diff --git a/server/core/model/build.gradle.kts b/server/core/model/build.gradle.kts new file mode 100644 index 00000000..0c6bacef --- /dev/null +++ b/server/core/model/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("diary.kotlin.jvm") +} + +dependencies { + api(libs.kotlinx.datetime) +} diff --git a/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Account.kt b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Account.kt new file mode 100644 index 00000000..01d5cd8a --- /dev/null +++ b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Account.kt @@ -0,0 +1,7 @@ +package io.github.taetae98coding.diary.core.model + +public data class Account( + val uid: String, + val email: String, + val password: String, +) diff --git a/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt new file mode 100644 index 00000000..cc491c2a --- /dev/null +++ b/server/core/model/src/main/kotlin/io/github/taetae98coding/diary/core/model/Memo.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.core.model + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate + +public data class Memo( + val id: String, + val title: String, + val description: String, + val start: LocalDate?, + val endInclusive: LocalDate?, + val color: Int, + val owner: String, + val isFinish: Boolean, + val isDelete: Boolean, + val updateAt: Instant, +) diff --git a/server/data/account/README.md b/server/data/account/README.md new file mode 100644 index 00000000..05250e6a --- /dev/null +++ b/server/data/account/README.md @@ -0,0 +1,3 @@ +# :server:data:account module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_data_account.svg) diff --git a/server/data/account/build.gradle.kts b/server/data/account/build.gradle.kts new file mode 100644 index 00000000..840f95c3 --- /dev/null +++ b/server/data/account/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("diary.server.data") +} + +dependencies { + implementation(project(":server:core:database")) + implementation(project(":server:domain:account")) + + implementation(platform(libs.exposed.bom)) + implementation(libs.exposed.core) +} diff --git a/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt new file mode 100644 index 00000000..dc2be468 --- /dev/null +++ b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/AccountDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.account + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class AccountDataModule diff --git a/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt new file mode 100644 index 00000000..b3a4b2e1 --- /dev/null +++ b/server/data/account/src/main/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountRepositoryImpl.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary.data.account.repository + +import io.github.taetae98coding.diary.core.database.AccountTable +import io.github.taetae98coding.diary.core.model.Account +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import org.koin.core.annotation.Singleton + +@Singleton +internal class AccountRepositoryImpl : AccountRepository { + override suspend fun contains(email: String): Boolean = AccountTable.contains(email) + + override suspend fun upsert(account: Account) { + AccountTable.insert(account) + } + + override suspend fun findByEmail(email: String, password: String): Account? = AccountTable.findByEmail(email, password) +} diff --git a/server/data/memo/README.md b/server/data/memo/README.md new file mode 100644 index 00000000..f17b9105 --- /dev/null +++ b/server/data/memo/README.md @@ -0,0 +1,3 @@ +# :server:data:memo module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_data_memo.svg) diff --git a/server/data/memo/build.gradle.kts b/server/data/memo/build.gradle.kts new file mode 100644 index 00000000..8180770f --- /dev/null +++ b/server/data/memo/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("diary.server.data") +} + +dependencies { + implementation(project(":server:core:database")) + implementation(project(":server:domain:memo")) + + implementation(platform(libs.exposed.bom)) + implementation(libs.exposed.core) +} diff --git a/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt new file mode 100644 index 00000000..2b9a7775 --- /dev/null +++ b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/MemoDataModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.data.memo + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class MemoDataModule diff --git a/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt new file mode 100644 index 00000000..2b0fc26f --- /dev/null +++ b/server/data/memo/src/main/kotlin/io/github/taetae98coding/diary/data/memo/repository/MemoRepositoryImpl.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.data.memo.repository + +import io.github.taetae98coding.diary.core.database.MemoTable +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.datetime.Instant +import org.koin.core.annotation.Factory + +@Factory +internal class MemoRepositoryImpl : MemoRepository { + override suspend fun upsert(list: List) { + MemoTable.upsert(list) + } + + override fun findByIds(list: List): Flow> = flow { emit(MemoTable.findByIds(list)) } + + override fun findByUpdateAt(uid: String, updateAt: Instant): Flow> = flow { emit(MemoTable.findByUpdateAt(uid, updateAt)) } +} diff --git a/server/domain/account/README.md b/server/domain/account/README.md new file mode 100644 index 00000000..33e33938 --- /dev/null +++ b/server/domain/account/README.md @@ -0,0 +1,3 @@ +# :server:domain:account module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_domain_account.svg) diff --git a/server/domain/account/build.gradle.kts b/server/domain/account/build.gradle.kts new file mode 100644 index 00000000..e10cade2 --- /dev/null +++ b/server/domain/account/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.server.domain") +} diff --git a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt new file mode 100644 index 00000000..1c24f4b0 --- /dev/null +++ b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/AccountDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.account + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class AccountDomainModule diff --git a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt new file mode 100644 index 00000000..176e9754 --- /dev/null +++ b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/repository/AccountRepository.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.domain.account.repository + +import io.github.taetae98coding.diary.core.model.Account + +public interface AccountRepository { + public suspend fun contains(email: String): Boolean + + public suspend fun upsert(account: Account) + + public suspend fun findByEmail(email: String, password: String): Account? +} diff --git a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt new file mode 100644 index 00000000..2656ce7f --- /dev/null +++ b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/FindAccountUseCase.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.domain.account.usecase + +import io.github.taetae98coding.diary.core.model.Account +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import org.koin.core.annotation.Factory + +@Factory +public class FindAccountUseCase internal constructor( + private val hashingPasswordUseCase: HashingPasswordUseCase, + private val repository: AccountRepository, +) { + public suspend operator fun invoke(email: String, password: String): Result = + runCatching { + repository.findByEmail( + email = email, + password = hashingPasswordUseCase(email, password).getOrThrow(), + ) + } +} diff --git a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/HashingPasswordUseCase.kt b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/HashingPasswordUseCase.kt new file mode 100644 index 00000000..a6f18ce9 --- /dev/null +++ b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/HashingPasswordUseCase.kt @@ -0,0 +1,21 @@ +package io.github.taetae98coding.diary.domain.account.usecase + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.koin.core.annotation.Factory +import java.security.MessageDigest + +@OptIn(ExperimentalStdlibApi::class) +@Factory +public class HashingPasswordUseCase internal constructor() { + public suspend operator fun invoke(email: String, password: String): Result = + runCatching { + withContext(Dispatchers.IO) { + MessageDigest + .getInstance("SHA-256") + .apply { update((email + password).toByteArray()) } + .digest() + .toHexString() + } + } +} diff --git a/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt new file mode 100644 index 00000000..50a99918 --- /dev/null +++ b/server/domain/account/src/main/kotlin/io/github/taetae98coding/diary/domain/account/usecase/JoinUseCase.kt @@ -0,0 +1,36 @@ +package io.github.taetae98coding.diary.domain.account.usecase + +import io.github.taetae98coding.diary.common.exception.account.ExistEmailException +import io.github.taetae98coding.diary.common.exception.account.InvalidEmailException +import io.github.taetae98coding.diary.core.model.Account +import io.github.taetae98coding.diary.domain.account.repository.AccountRepository +import io.github.taetae98coding.diary.library.kotlin.regex.email +import org.koin.core.annotation.Factory +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +@Factory +public class JoinUseCase internal constructor( + private val hashingPasswordUseCase: HashingPasswordUseCase, + private val repository: AccountRepository, +) { + public suspend operator fun invoke(email: String, password: String): Result = + runCatching { + // TODO password check + + if (!email.contains(Regex.email())) throw InvalidEmailException() + if (repository.contains(email)) throw ExistEmailException() + + val uid = Uuid.random().toString() + + val account = + Account( + uid = uid, + email = email, + password = hashingPasswordUseCase(email, password).getOrThrow(), + ) + + repository.upsert(account) + } +} diff --git a/server/domain/memo/README.md b/server/domain/memo/README.md new file mode 100644 index 00000000..1ab7f10c --- /dev/null +++ b/server/domain/memo/README.md @@ -0,0 +1,3 @@ +# :server:domain:memo module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_domain_memo.svg) diff --git a/server/domain/memo/build.gradle.kts b/server/domain/memo/build.gradle.kts new file mode 100644 index 00000000..e10cade2 --- /dev/null +++ b/server/domain/memo/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.server.domain") +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt new file mode 100644 index 00000000..94600e87 --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/MemoDomainModule.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.domain.memo + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +public class MemoDomainModule diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt new file mode 100644 index 00000000..93f62a53 --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/repository/MemoRepository.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.domain.memo.repository + +import io.github.taetae98coding.diary.core.model.Memo +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant + +public interface MemoRepository { + public suspend fun upsert(list: List) + + public fun findByIds(list: List): Flow> + + public fun findByUpdateAt(uid: String, updateAt: Instant): Flow> +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FetchMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FetchMemoUseCase.kt new file mode 100644 index 00000000..0dc3f4f0 --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/FetchMemoUseCase.kt @@ -0,0 +1,23 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.datetime.Instant +import org.koin.core.annotation.Factory + +@OptIn(ExperimentalCoroutinesApi::class) +@Factory +public class FetchMemoUseCase internal constructor( + private val repository: MemoRepository, +) { + public operator fun invoke(uid: String, updateAt: Instant): Flow>> = + flow { emitAll(repository.findByUpdateAt(uid, updateAt)) } + .mapLatest { Result.success(it) } + .catch { emit(Result.failure(it)) } +} diff --git a/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt new file mode 100644 index 00000000..42afe119 --- /dev/null +++ b/server/domain/memo/src/main/kotlin/io/github/taetae98coding/diary/domain/memo/usecase/UpsertMemoUseCase.kt @@ -0,0 +1,41 @@ +package io.github.taetae98coding.diary.domain.memo.usecase + +import io.github.taetae98coding.diary.common.exception.memo.MemoTitleBlankException +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.memo.repository.MemoRepository +import kotlinx.coroutines.flow.first +import org.koin.core.annotation.Factory + +@Factory +public class UpsertMemoUseCase internal constructor( + private val repository: MemoRepository, +) { + public suspend operator fun invoke(list: List): Result { + return runCatching { + // TODO permission check + + val originMemoMap = + repository + .findByIds(list.map { it.id }) + .first() + .associateBy { it.id } + + val validList = + list + .filter { + val originMemo = originMemoMap[it.id] ?: return@filter true + it.updateAt >= originMemo.updateAt + }.map { + it.copy( + title = + it.title.ifBlank { + val originMemo = originMemoMap[it.id] ?: throw MemoTitleBlankException() + originMemo.title + }, + ) + } + + repository.upsert(validList) + } + } +} diff --git a/server/feature/account/README.md b/server/feature/account/README.md new file mode 100644 index 00000000..950ef5c2 --- /dev/null +++ b/server/feature/account/README.md @@ -0,0 +1,3 @@ +# :server:feature:account module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_feature_account.svg) diff --git a/server/feature/account/build.gradle.kts b/server/feature/account/build.gradle.kts new file mode 100644 index 00000000..d1ed97bf --- /dev/null +++ b/server/feature/account/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("diary.server.feature") +} + +dependencies { + implementation(project(":server:domain:account")) +} diff --git a/server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt b/server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt new file mode 100644 index 00000000..a453acf4 --- /dev/null +++ b/server/feature/account/src/main/kotlin/io/github/taetae98coding/diary/feature/account/AccountRouting.kt @@ -0,0 +1,62 @@ +package io.github.taetae98coding.diary.feature.account + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.github.taetae98coding.diary.common.exception.account.ExistEmailException +import io.github.taetae98coding.diary.common.model.request.account.JoinRequest +import io.github.taetae98coding.diary.common.model.request.account.LoginRequest +import io.github.taetae98coding.diary.common.model.response.DiaryResponse +import io.github.taetae98coding.diary.common.model.response.account.LoginResponse +import io.github.taetae98coding.diary.domain.account.usecase.FindAccountUseCase +import io.github.taetae98coding.diary.domain.account.usecase.JoinUseCase +import io.ktor.http.HttpStatusCode +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import org.koin.ktor.plugin.scope + +public fun Route.accountRouting() { + route("/account") { + post("/join") { request -> + val useCase = call.scope.get() + + useCase(request.email, request.password) + .onSuccess { call.respond(HttpStatusCode.Created, DiaryResponse.Created) } + .onFailure { + when (it) { + is ExistEmailException -> call.respond(HttpStatusCode.Forbidden, DiaryResponse.AlreadyExistEmail) + else -> call.respond(HttpStatusCode.InternalServerError, DiaryResponse.InternalServerError) + } + } + } + + post("/login") { request -> + val useCase = call.scope.get() + + useCase(request.email, request.password) + .onSuccess { account -> + if (account == null) { + call.respond(HttpStatusCode.NotFound, DiaryResponse.AccountNotFound) + return@onSuccess + } + + val token = + JWT + .create() + .withClaim("uid", account.uid) + .sign(Algorithm.HMAC256("secret")) + + val response = + LoginResponse( + uid = account.uid, + token = token, + ) + + call.respond(DiaryResponse.success(response)) + }.onFailure { + call.respond(HttpStatusCode.InternalServerError, DiaryResponse.InternalServerError) + } + } + } +} diff --git a/server/feature/home/README.md b/server/feature/home/README.md new file mode 100644 index 00000000..11b1cb35 --- /dev/null +++ b/server/feature/home/README.md @@ -0,0 +1,3 @@ +# :server:feature:home module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_feature_home.svg) diff --git a/server/feature/home/build.gradle.kts b/server/feature/home/build.gradle.kts new file mode 100644 index 00000000..5afaa67a --- /dev/null +++ b/server/feature/home/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.server.feature") +} diff --git a/server/feature/home/src/main/kotlin/io/github/taetae98coding/diary/feature/home/HomeRouting.kt b/server/feature/home/src/main/kotlin/io/github/taetae98coding/diary/feature/home/HomeRouting.kt new file mode 100644 index 00000000..7950a83f --- /dev/null +++ b/server/feature/home/src/main/kotlin/io/github/taetae98coding/diary/feature/home/HomeRouting.kt @@ -0,0 +1,11 @@ +package io.github.taetae98coding.diary.feature.home + +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.get + +public fun Route.homeRouting() { + get { + call.respondText(text = "Diary") + } +} diff --git a/server/feature/memo/README.md b/server/feature/memo/README.md new file mode 100644 index 00000000..591e5df2 --- /dev/null +++ b/server/feature/memo/README.md @@ -0,0 +1,3 @@ +# :server:feature:memo module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_server_feature_memo.svg) diff --git a/server/feature/memo/build.gradle.kts b/server/feature/memo/build.gradle.kts new file mode 100644 index 00000000..47cabc9e --- /dev/null +++ b/server/feature/memo/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("diary.server.feature") +} + +dependencies { + implementation(project(":server:domain:memo")) +} diff --git a/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt b/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt new file mode 100644 index 00000000..75875b98 --- /dev/null +++ b/server/feature/memo/src/main/kotlin/io/github/taetae98coding/diary/feature/memo/MemoRouting.kt @@ -0,0 +1,90 @@ +package io.github.taetae98coding.diary.feature.memo + +import io.github.taetae98coding.diary.common.model.memo.MemoEntity +import io.github.taetae98coding.diary.common.model.response.DiaryResponse +import io.github.taetae98coding.diary.core.model.Memo +import io.github.taetae98coding.diary.domain.memo.usecase.FetchMemoUseCase +import io.github.taetae98coding.diary.domain.memo.usecase.UpsertMemoUseCase +import io.ktor.http.HttpStatusCode +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.jwt.JWTPrincipal +import io.ktor.server.auth.principal +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import kotlinx.coroutines.flow.first +import kotlinx.datetime.Instant +import org.koin.ktor.plugin.scope + +public fun Route.memoRouting() { + route("/memo") { + authenticate("account") { + post>("/upsert") { request -> + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@post + } + + val useCase = call.scope.get() + val memoList = request.map(MemoEntity::toMemo) + + useCase(memoList).onSuccess { call.respond(DiaryResponse.Success) } + } + + get("/fetch") { + val principal = call.principal() + if (principal == null) { + call.respond(HttpStatusCode.Unauthorized, DiaryResponse.Unauthorized) + return@get + } + + val uid = principal.payload.getClaim("uid").asString() + val updateAt = call.parameters["updateAt"]?.let { Instant.parse(it) } ?: return@get + val useCase = call.scope.get() + + useCase(uid, updateAt) + .first() + .onSuccess { call.respond(DiaryResponse.success(it.map(Memo::toEntity))) } + } + } + + post>("/migrate") { request -> + val useCase = call.scope.get() + val memoList = request.map(MemoEntity::toMemo) + + useCase(memoList).onSuccess { call.respond(DiaryResponse.Success) } + .onFailure { call.respond(HttpStatusCode.InternalServerError, it.toString()) } + } + } +} + +private fun MemoEntity.toMemo(): Memo = + Memo( + id = id, + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + ) + +private fun Memo.toEntity(): MemoEntity = + MemoEntity( + id = id, + title = title, + description = description, + start = start, + endInclusive = endInclusive, + color = color, + owner = owner, + isFinish = isFinish, + isDelete = isDelete, + updateAt = updateAt, + ) diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..146c4db6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,105 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com.android.*") + includeGroupByRegex("com.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } + + includeBuild("build-logic") +} + +dependencyResolutionManagement { + repositories { + google { + content { + includeGroupByRegex("com.android.*") + includeGroupByRegex("com.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + } +} + +rootProject.name = "Diary" + +include(":app:platform:jvm") +include(":app:platform:wasm") +include(":app:platform:android") +include(":app:platform:ios") +include(":app:platform:common") + +include(":app:core:diary-service") +include(":app:core:diary-database") +include(":app:core:diary-database-memory") +include(":app:core:diary-database-room") + +include(":app:core:account-preferences") +include(":app:core:account-preferences-memory") +include(":app:core:account-preferences-datastore") + +include(":app:core:holiday-service") +include(":app:core:holiday-database") +include(":app:core:holiday-database-memory") +include(":app:core:holiday-database-room") +include(":app:core:holiday-preferences") +include(":app:core:holiday-preferences-memory") +include(":app:core:holiday-preferences-datastore") + +include(":app:core:calendar-compose") +include(":app:core:design-system") + +include(":app:core:coroutines") +include(":app:core:model") +include(":app:core:navigation") +include(":app:core:resources") + +include(":app:data:memo") +include(":app:data:account") +include(":app:data:holiday") +include(":app:data:backup") +include(":app:data:fetch") + +include(":app:domain:memo") +include(":app:domain:account") +include(":app:domain:holiday") +include(":app:domain:backup") +include(":app:domain:fetch") + +include(":app:feature:memo") +include(":app:feature:calendar") +include(":app:feature:more") +include(":app:feature:account") + +include(":server:core:database") +include(":server:core:model") + +include(":server:data:account") +include(":server:data:memo") + +include(":server:domain:account") +include(":server:domain:memo") + +include(":server:app") +include(":server:feature:home") +include(":server:feature:account") +include(":server:feature:memo") + +include(":common:exception") +include(":common:model") + +include(":library:color") +include(":library:coroutines") +include(":library:datetime") +include(":library:koin-datastore") +include(":library:koin-room") +include(":library:kotlin") +include(":library:navigation") +include(":library:room") +include(":library:shimmer-m3")

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 X(n()U@;() + val backupManager = get() + + backupManager.attach(appLifecycleOwner) + } + + private fun initFetchManager() { + val appLifecycleOwner = get() + val fetchManager = get() + + fetchManager.attach(appLifecycleOwner) + } +} diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt new file mode 100644 index 00000000..749e953e --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt @@ -0,0 +1,55 @@ +package io.github.taetae98coding.diary.initializer + +import android.content.Context +import androidx.startup.Initializer +import io.github.taetae98coding.diary.BuildConfig +import io.github.taetae98coding.diary.app.AppModule +import io.github.taetae98coding.diary.core.account.preferences.datastore.AccountDataStorePreferencesModule +import io.github.taetae98coding.diary.core.diary.database.room.DiaryRoomDatabaseModule +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.holiday.database.room.HolidayRoomDatabaseModule +import io.github.taetae98coding.diary.core.holiday.preferences.datastore.HolidayDataStorePreferencesModule +import io.github.taetae98coding.diary.core.holiday.service.HolidayServiceModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.KoinApplication +import org.koin.core.context.startKoin +import org.koin.core.module.Module +import org.koin.core.qualifier.StringQualifier +import org.koin.dsl.module +import org.koin.ksp.generated.module + +public class KoinInitializer : Initializer { + override fun create(context: Context): KoinApplication { + return startKoin { + androidContext(context) + + modules( + AppModule().module, + diaryServiceModule(), + AccountDataStorePreferencesModule().module, + HolidayDataStorePreferencesModule().module, + HolidayRoomDatabaseModule().module, + holidayServiceModule(), + DiaryRoomDatabaseModule().module, + ) + } + } + + private fun diaryServiceModule(): Module { + return module { + single(qualifier = StringQualifier(DiaryServiceModule.DIARY_API_URL)) { BuildConfig.DIARY_API_URL } + } + } + + private fun holidayServiceModule(): Module { + return module { + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_URL)) { BuildConfig.HOLIDAY_API_URL } + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_KEY)) { BuildConfig.HOLIDAY_API_KEY } + } + } + + + override fun dependencies(): MutableList>> { + return mutableListOf() + } +} diff --git a/app/platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/app/platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/platform/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/platform/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..2cccc8f32e775df4691b242383341ca8d865ce73 GIT binary patch literal 1252 zcmV-^{}1zY>2ARW z$88%)lI)r@kqK$@|7-^LZg<&nBT0&sRlVfr`u{k_?qV8AvXy@Cpw=;4GC2BX`UZOkU(_4~?h zPB@VXC!BCb_HdBh3^MKZd>FGO!-N_N?D73K1x`3&!X2OSk2?+=i zB!}x*1Pi2L+jhK7zNVgS+qP}nwr$(CZExTAMzuTJc4zi(|Haw&yzjh4^xu&iNs8oL z7(UTL&IdFC%zyv=BT;-}5L3*Zc^nu~T#f2D|`Y{H_QFhFa z(TLwM!oz-yO8n>!1~WPdA-gprA;xf`0vV+Q5k3FvNzuoX6vk*JjHFOvLP-iB#-Ah; zF(#4>#284@5u+oCQOlnglXDf`B)^Ad8H(?QqDC^yexc9^6I#omC~-M%{$3TE!Whsl2PGyB(sIbde=;~ z(wO7bd?d-e9dnJG%dEgDj?cr}S~r}_T+QAV4Vxbp;pc%LB70lcTXEKlecI$2=8ipM7!w+RqHk4ldZj2 z9CF+0gsdHTwp(^=v-YrKN|Jdz%URX2H6AA&TkpP)XDbslb;41{$`s^#wQ0T$?0-g` zRx+LBUUnJh_}F{_qVN|mrsotd_?TKJ6R{ye-fN0N%I zr_X3zDt)4|Q;u6-a_84Bl+-74zIsbGOeyX=UI14LWn!spY13q_)Oo*y4+cv+Hp=58 zB`>>5aNmLzSrOs)^q*|A`wtsBXy7c78G01}aH)%^>>SbaA`o>M!1*2%WDz;^9%lhW zKL8Mc=vJiZ)G|2|*KPojPXW9I9v-CJ4KS!>{8!)|5c?9ieg}AdigLfgq?UCzfqPeg z7{q=A(3f;a#b31<-2i;BcySK+GSWtXP`XlE9|PBKA?C~z0KIHK*My3tZ?@<@@CGrj zKLd}}+Uc7Krn9%~8E)h51$^zjj(BHJ6U5X@%hLdWPpg=%K+$wZ1! z%kX{!y1yDVc*qW0!D8w#%MLosV(fv_9SkPXr4Natem_E>US;~Aa6azo2vF^w4*$@w Oc#Qz_-+%uAqX7V(hGAy_ literal 0 HcmV?d00001 diff --git a/app/platform/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/platform/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..f95c40b62d9e3f0d3eaacc4a158d8d3105125346 GIT binary patch literal 758 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 E0$dVLfB*mh literal 0 HcmV?d00001 diff --git a/app/platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..384ed3ba2d0ab36e384ba238c40147ef5f892915 GIT binary patch literal 3334 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 Q_x|_(_x|_(_x>-_81Ls~rT_o{ literal 0 HcmV?d00001 diff --git a/app/platform/android/src/main/res/values/ic_launcher_background.xml b/app/platform/android/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..d37e5882 --- /dev/null +++ b/app/platform/android/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #EFEFEF + \ No newline at end of file diff --git a/app/platform/android/src/main/res/values/strings.xml b/app/platform/android/src/main/res/values/strings.xml new file mode 100644 index 00000000..05c32c9d --- /dev/null +++ b/app/platform/android/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Diary + \ No newline at end of file diff --git a/app/platform/common/README.md b/app/platform/common/README.md new file mode 100644 index 00000000..6a42ceb4 --- /dev/null +++ b/app/platform/common/README.md @@ -0,0 +1,3 @@ +# :app:platform:common module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_platform_common.svg) diff --git a/app/platform/common/build.gradle.kts b/app/platform/common/build.gradle.kts new file mode 100644 index 00000000..a1ea8f56 --- /dev/null +++ b/app/platform/common/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("diary.app.feature") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:data:memo")) + implementation(project(":app:data:account")) + implementation(project(":app:data:holiday")) + implementation(project(":app:data:backup")) + implementation(project(":app:data:fetch")) + + implementation(project(":app:domain:memo")) + implementation(project(":app:domain:account")) + implementation(project(":app:domain:holiday")) + implementation(project(":app:domain:backup")) + implementation(project(":app:domain:fetch")) + + implementation(project(":app:core:coroutines")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:core:holiday-service")) + + implementation(project(":app:feature:memo")) + implementation(project(":app:feature:calendar")) + implementation(project(":app:feature:more")) + implementation(project(":app:feature:account")) + + implementation(project(":library:datetime")) + + implementation(compose.material3AdaptiveNavigationSuite) + implementation(libs.compose.material3.adaptive) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.app" +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt new file mode 100644 index 00000000..efdf640b --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/App.kt @@ -0,0 +1,103 @@ +package io.github.taetae98coding.diary.app + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.compose.currentBackStackEntryAsState +import io.github.taetae98coding.diary.app.navigation.AppNavigation +import io.github.taetae98coding.diary.app.state.rememberAppState +import io.github.taetae98coding.diary.core.design.system.theme.DiaryTheme +import io.github.taetae98coding.diary.core.navigation.calendar.CalendarDestination +import io.github.taetae98coding.diary.core.navigation.memo.MemoDestination +import io.github.taetae98coding.diary.core.navigation.more.MoreDestination +import io.github.taetae98coding.diary.core.resources.icon.CalendarIcon +import io.github.taetae98coding.diary.core.resources.icon.MemoIcon +import io.github.taetae98coding.diary.core.resources.icon.MoreIcon +import org.jetbrains.compose.resources.stringResource + +@Composable +public fun App() { + DiaryTheme { + AppScaffold( + modifier = Modifier.fillMaxSize() + .imePadding(), + ) + } +} + +@Composable +private fun AppScaffold( + modifier: Modifier = Modifier, +) { + val state = rememberAppState() + val backStackEntry by state.navController.currentBackStackEntryAsState() + val isNavigationVisible by remember { + derivedStateOf { + val visibleDestination = listOf( + MemoDestination::class, + CalendarDestination::class, + MoreDestination::class, + ) + + visibleDestination.any { + backStackEntry?.destination + ?.hasRoute(it) + ?: false + } + } + } + + NavigationSuiteScaffold( + navigationSuiteItems = { + listOf( +// AppNavigation.Memo, + AppNavigation.Calendar, + AppNavigation.More, + ).forEach { navigation -> + val isSelected = backStackEntry?.destination + ?.hierarchy + ?.any { it.hasRoute(navigation.route::class) } + ?: false + + item( + selected = isSelected, + onClick = { state.navigate(navigation) }, + icon = { AppNavigationIcon(navigation = navigation) }, + label = { Text(text = stringResource(navigation.title)) }, + alwaysShowLabel = true, + ) + } + }, + modifier = modifier, + layoutType = if (isNavigationVisible) { + NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(currentWindowAdaptiveInfo()) + } else { + NavigationSuiteType.None + }, + ) { + AppNavHost(state = state) + } +} + +@Composable +private fun AppNavigationIcon( + navigation: AppNavigation, + modifier: Modifier = Modifier, +) { + when (navigation) { + AppNavigation.Memo -> MemoIcon(modifier = modifier) + AppNavigation.Calendar -> CalendarIcon(modifier = modifier) + AppNavigation.More -> MoreIcon(modifier = modifier) + } +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt new file mode 100644 index 00000000..a7e70294 --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppModule.kt @@ -0,0 +1,52 @@ +package io.github.taetae98coding.diary.app + +import io.github.taetae98coding.diary.core.coroutines.CoroutinesModule +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.holiday.service.HolidayServiceModule +import io.github.taetae98coding.diary.data.account.AccountDataModule +import io.github.taetae98coding.diary.data.backup.BackupDataModule +import io.github.taetae98coding.diary.data.fetch.FetchDataModule +import io.github.taetae98coding.diary.data.holiday.HolidayDataModule +import io.github.taetae98coding.diary.data.memo.MemoDataModule +import io.github.taetae98coding.diary.domain.account.AccountDomainModule +import io.github.taetae98coding.diary.domain.backup.BackupDomainModule +import io.github.taetae98coding.diary.domain.fetch.FetchDomainModule +import io.github.taetae98coding.diary.domain.holiday.HolidayDomainModule +import io.github.taetae98coding.diary.domain.memo.MemoDomainModule +import io.github.taetae98coding.diary.feature.account.AccountFeatureModule +import io.github.taetae98coding.diary.feature.calendar.CalendarFeatureModule +import io.github.taetae98coding.diary.feature.memo.MemoFeatureModule +import io.github.taetae98coding.diary.feature.more.MoreFeatureModule +import kotlinx.datetime.Clock +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton + +@Module( + includes = [ + CoroutinesModule::class, + DiaryServiceModule::class, + HolidayServiceModule::class, + MemoDataModule::class, + AccountDataModule::class, + HolidayDataModule::class, + BackupDataModule::class, + FetchDataModule::class, + MemoDomainModule::class, + AccountDomainModule::class, + HolidayDomainModule::class, + BackupDomainModule::class, + FetchDomainModule::class, + MemoFeatureModule::class, + CalendarFeatureModule::class, + MoreFeatureModule::class, + AccountFeatureModule::class, + ], +) +@ComponentScan +public class AppModule { + @Singleton + internal fun providesClock(): Clock { + return Clock.System + } +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt new file mode 100644 index 00000000..5699e549 --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/AppNavHost.kt @@ -0,0 +1,28 @@ +package io.github.taetae98coding.diary.app + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import io.github.taetae98coding.diary.app.state.AppState +import io.github.taetae98coding.diary.core.navigation.calendar.CalendarDestination +import io.github.taetae98coding.diary.feature.account.accountNavigation +import io.github.taetae98coding.diary.feature.calendar.calendarNavigation +import io.github.taetae98coding.diary.feature.memo.memoNavigation +import io.github.taetae98coding.diary.feature.more.moreNavigation + +@Composable +internal fun AppNavHost( + state: AppState, + modifier: Modifier = Modifier, +) { + NavHost( + navController = state.navController, + startDestination = CalendarDestination, + modifier = modifier, + ) { + memoNavigation(navController = state.navController) + calendarNavigation(navController = state.navController) + moreNavigation(navController = state.navController) + accountNavigation(navController = state.navController) + } +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/BackupManager.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/BackupManager.kt new file mode 100644 index 00000000..15d65eca --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/BackupManager.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.app.manager + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import io.github.taetae98coding.diary.domain.backup.usecase.BackupUseCase +import kotlinx.coroutines.launch +import org.koin.core.annotation.Singleton + +@Singleton +public class BackupManager( + private val backupUseCase: BackupUseCase, +) { + public fun attach(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + backupUseCase() + } + } + } +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FetchManager.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FetchManager.kt new file mode 100644 index 00000000..d73f96db --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/manager/FetchManager.kt @@ -0,0 +1,22 @@ +package io.github.taetae98coding.diary.app.manager + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import io.github.taetae98coding.diary.domain.fetch.usecase.FetchUseCase +import kotlinx.coroutines.launch +import org.koin.core.annotation.Singleton + +@Singleton +public class FetchManager( + private val fetchUseCase: FetchUseCase, +) { + public fun attach(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycleScope.launch { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + fetchUseCase() + } + } + } +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt new file mode 100644 index 00000000..a159440e --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/navigation/AppNavigation.kt @@ -0,0 +1,30 @@ +package io.github.taetae98coding.diary.app.navigation + +import io.github.taetae98coding.diary.core.navigation.calendar.CalendarDestination +import io.github.taetae98coding.diary.core.navigation.memo.MemoDestination +import io.github.taetae98coding.diary.core.navigation.more.MoreDestination +import io.github.taetae98coding.diary.core.resources.Res +import io.github.taetae98coding.diary.core.resources.calendar +import io.github.taetae98coding.diary.core.resources.memo +import io.github.taetae98coding.diary.core.resources.more +import org.jetbrains.compose.resources.StringResource + +internal enum class AppNavigation( + val title: StringResource, + val route: Any, +) { + Memo( + title = Res.string.memo, + route = MemoDestination, + ), + + Calendar( + title = Res.string.calendar, + route = CalendarDestination, + ), + + More( + title = Res.string.more, + route = MoreDestination, + ) +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/AppState.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/AppState.kt new file mode 100644 index 00000000..857335c4 --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/AppState.kt @@ -0,0 +1,28 @@ +package io.github.taetae98coding.diary.app.state + +import androidx.compose.runtime.Stable +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavHostController +import io.github.taetae98coding.diary.app.navigation.AppNavigation +import io.github.taetae98coding.diary.core.navigation.calendar.CalendarDestination + +@Stable +internal class AppState( + val navController: NavHostController, +) { + fun navigate(navigation: AppNavigation) { + val isSelected = navController.currentBackStackEntry + ?.destination + ?.hierarchy + ?.any { it.hasRoute(navigation.route::class) } + ?: false + + if (!isSelected) { + navController.navigate(navigation.route) { + popUpTo(CalendarDestination) + launchSingleTop = true + } + } + } +} diff --git a/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/RememberAppState.kt b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/RememberAppState.kt new file mode 100644 index 00000000..5d2657f3 --- /dev/null +++ b/app/platform/common/src/commonMain/kotlin/io/github/taetae98coding/diary/app/state/RememberAppState.kt @@ -0,0 +1,16 @@ +package io.github.taetae98coding.diary.app.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.compose.rememberNavController + +@Composable +internal fun rememberAppState(): AppState { + val navController = rememberNavController() + + return remember(navController) { + AppState( + navController = navController, + ) + } +} diff --git a/app/platform/ios/README.md b/app/platform/ios/README.md new file mode 100644 index 00000000..ec95d002 --- /dev/null +++ b/app/platform/ios/README.md @@ -0,0 +1,3 @@ +# :app:platform:ios module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_platform_ios.svg) diff --git a/app/platform/ios/build.gradle.kts b/app/platform/ios/build.gradle.kts new file mode 100644 index 00000000..183108c1 --- /dev/null +++ b/app/platform/ios/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("diary.kotlin.multiplatform") + id("diary.compose") +} + +kotlin { + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries { + framework { + baseName = "Kotlin" + } + } + } + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain { + dependencies { + implementation(project(":app:platform:common")) + implementation(project(":app:core:coroutines")) + implementation(project(":app:core:diary-database-room")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:core:account-preferences-datastore")) + implementation(project(":app:core:holiday-preferences-datastore")) + implementation(project(":app:core:holiday-database-room")) + implementation(project(":app:core:holiday-service")) + + implementation(compose.ui) + implementation(libs.lifecycle.compose) + + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + + implementation(libs.ktor.client.darwin) + } + } + } +} diff --git a/app/platform/ios/src/iosArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosArm64.kt b/app/platform/ios/src/iosArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosArm64.kt new file mode 100644 index 00000000..b235750d --- /dev/null +++ b/app/platform/ios/src/iosArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosArm64.kt @@ -0,0 +1,20 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.app.AppModule +import io.github.taetae98coding.diary.core.account.preferences.datastore.AccountDataStorePreferencesModule +import io.github.taetae98coding.diary.core.diary.database.room.DiaryRoomDatabaseModule +import io.github.taetae98coding.diary.core.holiday.database.room.HolidayRoomDatabaseModule +import io.github.taetae98coding.diary.core.holiday.preferences.datastore.HolidayDataStorePreferencesModule +import org.koin.core.module.Module +import org.koin.ksp.generated.module + +internal actual val appModule: Module + get() = AppModule().module +internal actual val accountDataStoreModule: Module + get() = AccountDataStorePreferencesModule().module +internal actual val holidayDataStoreModule: Module + get() = HolidayDataStorePreferencesModule().module +internal actual val holidayRoomModule: Module + get() = HolidayRoomDatabaseModule().module +internal actual val diaryRoomModule: Module + get() = DiaryRoomDatabaseModule().module diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/IosApp.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/IosApp.kt new file mode 100644 index 00000000..7da8cf8b --- /dev/null +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/IosApp.kt @@ -0,0 +1,24 @@ +package io.github.taetae98coding.diary + +import androidx.compose.ui.window.ComposeUIViewController +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.LifecycleStartEffect +import io.github.taetae98coding.diary.app.App +import io.github.taetae98coding.diary.core.coroutines.AppLifecycleOwner +import platform.UIKit.UIViewController + +public fun compose(): UIViewController { + return ComposeUIViewController { + App() + + LifecycleStartEffect(keys = arrayOf(AppLifecycleOwner)) { + AppLifecycleOwner.start() + onStopOrDispose { AppLifecycleOwner.stop() } + } + + LifecycleResumeEffect(keys = arrayOf(AppLifecycleOwner)) { + AppLifecycleOwner.resume() + onPauseOrDispose { AppLifecycleOwner.pause() } + } + } +} diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt new file mode 100644 index 00000000..f5cb9b85 --- /dev/null +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.core.coroutines.AppLifecycleOwner + +internal fun initAppLifecycleOwner(): AppLifecycleOwner { + AppLifecycleOwner.create() + return AppLifecycleOwner +} diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt new file mode 100644 index 00000000..2dc42ffa --- /dev/null +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.initializer + +import androidx.lifecycle.LifecycleOwner +import io.github.taetae98coding.diary.app.manager.BackupManager +import org.koin.core.KoinApplication + +internal fun initBackupManager( + koinApplication: KoinApplication, +) { + val appLifecycleOwner = koinApplication.koin.get() + val backupManager = koinApplication.koin.get() + + backupManager.attach(appLifecycleOwner) +} diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt new file mode 100644 index 00000000..e0e722db --- /dev/null +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.initializer + +import androidx.lifecycle.LifecycleOwner +import io.github.taetae98coding.diary.app.manager.FetchManager +import org.koin.core.KoinApplication + +internal fun initFetchManager( + koinApplication: KoinApplication, +) { + val appLifecycleOwner = koinApplication.koin.get() + val fetchManager = koinApplication.koin.get() + + fetchManager.attach(appLifecycleOwner) +} diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt new file mode 100644 index 00000000..4211f419 --- /dev/null +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/IosInitializer.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.initializer + +public fun init() { + initAppLifecycleOwner() + val koinApplication = initKoin() + + initBackupManager(koinApplication) + initFetchManager(koinApplication) +} diff --git a/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt new file mode 100644 index 00000000..16b5b1aa --- /dev/null +++ b/app/platform/ios/src/iosMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt @@ -0,0 +1,43 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.holiday.service.HolidayServiceModule +import org.koin.core.KoinApplication +import org.koin.core.context.startKoin +import org.koin.core.module.Module +import org.koin.core.qualifier.StringQualifier +import org.koin.dsl.module +import platform.Foundation.NSBundle + +internal expect val appModule: Module +internal expect val accountDataStoreModule: Module +internal expect val holidayDataStoreModule: Module +internal expect val holidayRoomModule: Module +internal expect val diaryRoomModule: Module + +public fun initKoin(): KoinApplication { + return startKoin { + modules( + appModule, + diaryServiceModule(), + accountDataStoreModule, + holidayDataStoreModule, + holidayRoomModule, + holidayServiceModule(), + diaryRoomModule, + ) + } +} + +private fun diaryServiceModule(): Module { + return module { + single(qualifier = StringQualifier(DiaryServiceModule.DIARY_API_URL)) { NSBundle.mainBundle.objectForInfoDictionaryKey("Diary Api Url") as String } + } +} + +private fun holidayServiceModule(): Module { + return module { + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_URL)) { NSBundle.mainBundle.objectForInfoDictionaryKey("Holiday Api Url") as String } + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_KEY)) { NSBundle.mainBundle.objectForInfoDictionaryKey("Holiday Api Key") as String } + } +} diff --git a/app/platform/ios/src/iosSimulatorArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosSimulatorArm64.kt b/app/platform/ios/src/iosSimulatorArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosSimulatorArm64.kt new file mode 100644 index 00000000..4caa8802 --- /dev/null +++ b/app/platform/ios/src/iosSimulatorArm64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosSimulatorArm64.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.app.AppModule +import io.github.taetae98coding.diary.core.account.preferences.datastore.AccountDataStorePreferencesModule +import io.github.taetae98coding.diary.core.holiday.database.room.HolidayRoomDatabaseModule +import io.github.taetae98coding.diary.core.holiday.preferences.datastore.HolidayDataStorePreferencesModule +import org.koin.core.module.Module +import org.koin.ksp.generated.module + +internal actual val appModule: Module + get() = AppModule().module +internal actual val accountDataStoreModule: Module + get() = AccountDataStorePreferencesModule().module +internal actual val holidayDataStoreModule: Module + get() = HolidayDataStorePreferencesModule().module +internal actual val holidayRoomModule: Module + get() = HolidayRoomDatabaseModule().module +internal actual val diaryRoomModule: Module + get() = DiaryRoomDatabaseModule().module diff --git a/app/platform/ios/src/iosX64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosX64.kt b/app/platform/ios/src/iosX64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosX64.kt new file mode 100644 index 00000000..4caa8802 --- /dev/null +++ b/app/platform/ios/src/iosX64Main/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.iosX64.kt @@ -0,0 +1,19 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.app.AppModule +import io.github.taetae98coding.diary.core.account.preferences.datastore.AccountDataStorePreferencesModule +import io.github.taetae98coding.diary.core.holiday.database.room.HolidayRoomDatabaseModule +import io.github.taetae98coding.diary.core.holiday.preferences.datastore.HolidayDataStorePreferencesModule +import org.koin.core.module.Module +import org.koin.ksp.generated.module + +internal actual val appModule: Module + get() = AppModule().module +internal actual val accountDataStoreModule: Module + get() = AccountDataStorePreferencesModule().module +internal actual val holidayDataStoreModule: Module + get() = HolidayDataStorePreferencesModule().module +internal actual val holidayRoomModule: Module + get() = HolidayRoomDatabaseModule().module +internal actual val diaryRoomModule: Module + get() = DiaryRoomDatabaseModule().module diff --git a/app/platform/jvm/README.md b/app/platform/jvm/README.md new file mode 100644 index 00000000..958598a6 --- /dev/null +++ b/app/platform/jvm/README.md @@ -0,0 +1,3 @@ +# :app:platform:jvm module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_platform_jvm.svg) diff --git a/app/platform/jvm/build.gradle.kts b/app/platform/jvm/build.gradle.kts new file mode 100644 index 00000000..67e301e8 --- /dev/null +++ b/app/platform/jvm/build.gradle.kts @@ -0,0 +1,94 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec +import ext.getLocalProperty +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +private val localProperties = requireNotNull(project.getLocalProperty()) + +plugins { + id("diary.kotlin.multiplatform") + id("diary.compose") + alias(libs.plugins.buildkonfig) +} + +kotlin { + jvm() + + sourceSets { + commonMain { + dependencies { + implementation(project(":app:platform:common")) + implementation(project(":app:core:coroutines")) + implementation(project(":app:core:diary-database-room")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:core:account-preferences-datastore")) + implementation(project(":app:core:holiday-preferences-datastore")) + implementation(project(":app:core:holiday-database-room")) + implementation(project(":app:core:holiday-service")) + + implementation(project(":library:koin-room")) + implementation(project(":library:koin-datastore")) + + implementation(compose.desktop.currentOs) + implementation(libs.lifecycle.compose) + + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + + runtimeOnly(libs.kotlinx.coroutines.swing) + runtimeOnly(libs.ktor.client.okhttp) + } + } + } +} + +compose { + desktop { + application { + mainClass = "io.github.taetae98coding.diary.JvmAppKt" + + nativeDistributions { + includeAllModules = true + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + + packageName = "Diary" + packageVersion = "1.0.0" + + macOS { + appStore = true + + bundleID = "io.github.taetae98coding.diary" + iconFile.set(rootProject.file("asset/icon/app_icon_mac.icns")) + } + } + + buildTypes { + release { + proguard { + isEnabled.set(false) + } + } + } + } + } +} + +buildkonfig { + packageName = Build.NAMESPACE + + defaultConfigs {} + + defaultConfigs("dev") { + buildConfigField(FieldSpec.Type.STRING, "FLAVOR", "dev") + buildConfigField(FieldSpec.Type.STRING, "DIARY_API_URL", localProperties.getProperty("diary.dev.api.base.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_URL", localProperties.getProperty("holiday.dev.api.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_KEY", localProperties.getProperty("holiday.dev.api.key")) + } + + defaultConfigs("real") { + buildConfigField(FieldSpec.Type.STRING, "FLAVOR", "real") + buildConfigField(FieldSpec.Type.STRING, "DIARY_API_URL", localProperties.getProperty("diary.real.api.base.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_URL", localProperties.getProperty("holiday.real.api.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_KEY", localProperties.getProperty("holiday.real.api.key")) + } +} + diff --git a/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt new file mode 100644 index 00000000..2e449433 --- /dev/null +++ b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/JvmApp.kt @@ -0,0 +1,37 @@ +package io.github.taetae98coding.diary + +import androidx.compose.ui.window.singleWindowApplication +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.LifecycleStartEffect +import io.github.taetae98coding.diary.app.App +import io.github.taetae98coding.diary.core.coroutines.AppLifecycleOwner +import io.github.taetae98coding.diary.initializer.intiJvm +import kotlinx.coroutines.runBlocking + +public fun main() { + runBlocking { + intiJvm() + } + + singleWindowApplication( + title = "Diary", + ) { + App() + + LifecycleStartEffect(keys = arrayOf(AppLifecycleOwner)) { + AppLifecycleOwner.start() + onStopOrDispose { AppLifecycleOwner.stop() } + } + + LifecycleResumeEffect(keys = arrayOf(AppLifecycleOwner)) { + AppLifecycleOwner.resume() + onPauseOrDispose { AppLifecycleOwner.pause() } + } + } + + destroyJvm() +} + +private fun destroyJvm() { + AppLifecycleOwner.destroy() +} diff --git a/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt new file mode 100644 index 00000000..4f4e4cf1 --- /dev/null +++ b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt @@ -0,0 +1,13 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.core.coroutines.AppLifecycleOwner +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal suspend fun initAppLifecycleOwner(): AppLifecycleOwner { + withContext(Dispatchers.Main.immediate) { + AppLifecycleOwner.create() + } + + return AppLifecycleOwner +} diff --git a/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt new file mode 100644 index 00000000..2dc42ffa --- /dev/null +++ b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.initializer + +import androidx.lifecycle.LifecycleOwner +import io.github.taetae98coding.diary.app.manager.BackupManager +import org.koin.core.KoinApplication + +internal fun initBackupManager( + koinApplication: KoinApplication, +) { + val appLifecycleOwner = koinApplication.koin.get() + val backupManager = koinApplication.koin.get() + + backupManager.attach(appLifecycleOwner) +} diff --git a/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt new file mode 100644 index 00000000..e0e722db --- /dev/null +++ b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.initializer + +import androidx.lifecycle.LifecycleOwner +import io.github.taetae98coding.diary.app.manager.FetchManager +import org.koin.core.KoinApplication + +internal fun initFetchManager( + koinApplication: KoinApplication, +) { + val appLifecycleOwner = koinApplication.koin.get() + val fetchManager = koinApplication.koin.get() + + fetchManager.attach(appLifecycleOwner) +} diff --git a/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/JvmInitializer.kt b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/JvmInitializer.kt new file mode 100644 index 00000000..5924cc2f --- /dev/null +++ b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/JvmInitializer.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.initializer + +internal suspend fun intiJvm() { + initAppLifecycleOwner() + val koinApplication = initKoin() + + initBackupManager(koinApplication) + initFetchManager(koinApplication) +} diff --git a/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt new file mode 100644 index 00000000..fc0b90ea --- /dev/null +++ b/app/platform/jvm/src/jvmMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt @@ -0,0 +1,48 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.BuildKonfig +import io.github.taetae98coding.diary.app.AppModule +import io.github.taetae98coding.diary.core.account.preferences.datastore.AccountDataStorePreferencesModule +import io.github.taetae98coding.diary.core.diary.database.room.DiaryRoomDatabaseModule +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.holiday.database.room.HolidayRoomDatabaseModule +import io.github.taetae98coding.diary.core.holiday.preferences.datastore.HolidayDataStorePreferencesModule +import io.github.taetae98coding.diary.core.holiday.service.HolidayServiceModule +import io.github.taetae98coding.diary.library.koin.datastore.koinDataStoreDefaultPath +import io.github.taetae98coding.diary.library.koin.room.koinRoomDefaultPath +import org.koin.core.KoinApplication +import org.koin.core.context.startKoin +import org.koin.core.module.Module +import org.koin.core.qualifier.StringQualifier +import org.koin.dsl.module +import org.koin.ksp.generated.module + +internal fun initKoin(): KoinApplication { + koinRoomDefaultPath += "/diary/${BuildKonfig.FLAVOR}" + koinDataStoreDefaultPath += "/diary/${BuildKonfig.FLAVOR}" + + return startKoin { + modules( + AppModule().module, + diaryServiceModule(), + AccountDataStorePreferencesModule().module, + HolidayDataStorePreferencesModule().module, + HolidayRoomDatabaseModule().module, + holidayServiceModule(), + DiaryRoomDatabaseModule().module, + ) + } +} + +private fun diaryServiceModule(): Module { + return module { + single(qualifier = StringQualifier(DiaryServiceModule.DIARY_API_URL)) { BuildKonfig.DIARY_API_URL } + } +} + +private fun holidayServiceModule(): Module { + return module { + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_URL)) { BuildKonfig.HOLIDAY_API_URL } + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_KEY)) { BuildKonfig.HOLIDAY_API_KEY } + } +} diff --git a/app/platform/wasm/README.md b/app/platform/wasm/README.md new file mode 100644 index 00000000..38264ccb --- /dev/null +++ b/app/platform/wasm/README.md @@ -0,0 +1,3 @@ +# :app:platform:wasm module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_platform_wasm.svg) diff --git a/app/platform/wasm/build.gradle.kts b/app/platform/wasm/build.gradle.kts new file mode 100644 index 00000000..31ef32bc --- /dev/null +++ b/app/platform/wasm/build.gradle.kts @@ -0,0 +1,73 @@ + +import com.codingfeline.buildkonfig.compiler.FieldSpec +import ext.getLocalProperty +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +private val localProperties = requireNotNull(project.getLocalProperty()) + +plugins { + id("diary.kotlin.multiplatform") + id("diary.compose") + alias(libs.plugins.buildkonfig) +} + +@OptIn(ExperimentalWasmDsl::class) +kotlin { + wasmJs { + moduleName = "diary" + browser { + commonWebpackConfig { + outputFileName = "diary.js" + } + + runTask { +// devServerProperty.set(devServerProperty.get().copy(port = 8080)) + } + } + + binaries.executable() + } + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain { + dependencies { + implementation(project(":app:platform:common")) + implementation(project(":app:core:coroutines")) + implementation(project(":app:core:diary-database-memory")) + implementation(project(":app:core:diary-service")) + implementation(project(":app:core:account-preferences-memory")) + implementation(project(":app:core:holiday-preferences-memory")) + implementation(project(":app:core:holiday-database-memory")) + implementation(project(":app:core:holiday-service")) + + implementation(compose.ui) + implementation(libs.lifecycle.compose) + + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + + runtimeOnly(libs.ktor.client.js) + } + } + } +} + +buildkonfig { + packageName = Build.NAMESPACE + + defaultConfigs {} + + defaultConfigs("dev") { + buildConfigField(FieldSpec.Type.STRING, "DIARY_API_URL", localProperties.getProperty("diary.dev.api.base.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_URL", localProperties.getProperty("holiday.dev.api.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_KEY", localProperties.getProperty("holiday.dev.api.key")) + } + + defaultConfigs("real") { + buildConfigField(FieldSpec.Type.STRING, "DIARY_API_URL", localProperties.getProperty("diary.real.api.base.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_URL", localProperties.getProperty("holiday.real.api.url")) + buildConfigField(FieldSpec.Type.STRING, "HOLIDAY_API_KEY", localProperties.getProperty("holiday.real.api.key")) + } +} diff --git a/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmJsApp.kt b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmJsApp.kt new file mode 100644 index 00000000..ff695bbd --- /dev/null +++ b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/WasmJsApp.kt @@ -0,0 +1,31 @@ +package io.github.taetae98coding.diary + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.LifecycleStartEffect +import io.github.taetae98coding.diary.app.App +import io.github.taetae98coding.diary.core.coroutines.AppLifecycleOwner +import io.github.taetae98coding.diary.initializer.intiJs + +@OptIn(ExperimentalComposeUiApi::class) +public fun main() { + intiJs() + + CanvasBasedWindow( + title = "Diary", + canvasElementId = "compose", + ) { + App() + + LifecycleStartEffect(keys = arrayOf(AppLifecycleOwner)) { + AppLifecycleOwner.start() + onStopOrDispose { AppLifecycleOwner.stop() } + } + + LifecycleResumeEffect(keys = arrayOf(AppLifecycleOwner)) { + AppLifecycleOwner.resume() + onPauseOrDispose { AppLifecycleOwner.pause() } + } + } +} diff --git a/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt new file mode 100644 index 00000000..f5cb9b85 --- /dev/null +++ b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/AppLifecycleOwnerInitializer.kt @@ -0,0 +1,8 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.core.coroutines.AppLifecycleOwner + +internal fun initAppLifecycleOwner(): AppLifecycleOwner { + AppLifecycleOwner.create() + return AppLifecycleOwner +} diff --git a/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt new file mode 100644 index 00000000..2dc42ffa --- /dev/null +++ b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/BackupManagerInitializer.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.initializer + +import androidx.lifecycle.LifecycleOwner +import io.github.taetae98coding.diary.app.manager.BackupManager +import org.koin.core.KoinApplication + +internal fun initBackupManager( + koinApplication: KoinApplication, +) { + val appLifecycleOwner = koinApplication.koin.get() + val backupManager = koinApplication.koin.get() + + backupManager.attach(appLifecycleOwner) +} diff --git a/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt new file mode 100644 index 00000000..e0e722db --- /dev/null +++ b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/FetchManagerInitializer.kt @@ -0,0 +1,14 @@ +package io.github.taetae98coding.diary.initializer + +import androidx.lifecycle.LifecycleOwner +import io.github.taetae98coding.diary.app.manager.FetchManager +import org.koin.core.KoinApplication + +internal fun initFetchManager( + koinApplication: KoinApplication, +) { + val appLifecycleOwner = koinApplication.koin.get() + val fetchManager = koinApplication.koin.get() + + fetchManager.attach(appLifecycleOwner) +} diff --git a/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/JsInitializer.kt b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/JsInitializer.kt new file mode 100644 index 00000000..5c28f4b9 --- /dev/null +++ b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/JsInitializer.kt @@ -0,0 +1,9 @@ +package io.github.taetae98coding.diary.initializer + +internal fun intiJs() { + initAppLifecycleOwner() + val koinApplication = initKoin() + + initBackupManager(koinApplication) + initFetchManager(koinApplication) +} diff --git a/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt new file mode 100644 index 00000000..82a324f9 --- /dev/null +++ b/app/platform/wasm/src/wasmJsMain/kotlin/io/github/taetae98coding/diary/initializer/KoinInitializer.kt @@ -0,0 +1,43 @@ +package io.github.taetae98coding.diary.initializer + +import io.github.taetae98coding.diary.BuildKonfig +import io.github.taetae98coding.diary.app.AppModule +import io.github.taetae98coding.diary.core.diary.database.memory.DiaryMemoryDatabaseModule +import io.github.taetae98coding.diary.core.diary.service.DiaryServiceModule +import io.github.taetae98coding.diary.core.holiday.database.memory.HolidayMemoryDatabaseModule +import io.github.taetae98coding.diary.core.holiday.preferences.memory.HolidayPreferencesMemoryModule +import io.github.taetae98coding.diary.core.holiday.service.HolidayServiceModule +import io.github.taetae98coding.diary.fore.account.preferences.memory.AccountPreferencesMemoryModule +import org.koin.core.KoinApplication +import org.koin.core.context.startKoin +import org.koin.core.module.Module +import org.koin.core.qualifier.StringQualifier +import org.koin.dsl.module +import org.koin.ksp.generated.module + +internal fun initKoin(): KoinApplication { + return startKoin { + modules( + AppModule().module, + diaryServiceModule(), + AccountPreferencesMemoryModule().module, + HolidayPreferencesMemoryModule().module, + HolidayMemoryDatabaseModule().module, + holidayServiceModule(), + DiaryMemoryDatabaseModule().module, + ) + } +} + +private fun diaryServiceModule(): Module { + return module { + single(qualifier = StringQualifier(DiaryServiceModule.DIARY_API_URL)) { BuildKonfig.DIARY_API_URL } + } +} + +private fun holidayServiceModule(): Module { + return module { + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_URL)) { BuildKonfig.HOLIDAY_API_URL } + single(qualifier = StringQualifier(HolidayServiceModule.HOLIDAY_API_KEY)) { BuildKonfig.HOLIDAY_API_KEY } + } +} diff --git a/app/platform/wasm/src/wasmJsMain/resources/index.html b/app/platform/wasm/src/wasmJsMain/resources/index.html new file mode 100644 index 00000000..9eacf2f5 --- /dev/null +++ b/app/platform/wasm/src/wasmJsMain/resources/index.html @@ -0,0 +1,13 @@ + + + + + + Diary + + + + + + + \ No newline at end of file diff --git a/asset/icon/app_icon_mac.icns b/asset/icon/app_icon_mac.icns new file mode 100644 index 0000000000000000000000000000000000000000..8464aaeccca79ea2855b5e06057b09ac0cab3cb9 GIT binary patch literal 120272 zcmdS91B`D?_wU)ZjnlS`)3&>RZQHhO+qP}nwr#unv`_au?>qO-++1Yt{O`;pQ@d)f zwJMd`$jR;FWm`BPPaw>^Kk*5S*nvAt5|W7|cIDnyIsgy`?=f;Xgjm zf3kpotoc77hK0GYBM=bSKkh#R3=;BRYk&}q?5rJu|Lr3E^CmVjHZlG88XzEGAkcrt zzbp_C$UhIzKk+~9pY4o(06z*q%>Ro1({}%I|K0wp7z`2^`2XlX#y})Mz`~9u2F`Yl zT!c~vPVy#>gsLWvP8N2ygiQ3m85!xBeiVV={@Do_2pkLy2>3@E2vtBpLPGw(>_0j{ zXu$v7S&;wkEa-nb8xQoa-vzuJ_RlAh_QE8AkbMxWEcSwyG#TVqc0~U?=++_2^$!u! zIu$>48Po4~j0E`HX?l@GDqmcnSK`I#?bl}dxFWXaa0J&EYg)diK}|g`_~~6S*r~}8 z)jujUA+H(`!(Wg!CN@bH+~0jCBj9}&G9949fB5UrYD^lJ_k1Ob=F?P0}tWZm3$ECh*P%tqn^Y^Xw$HJo^R&Kq(FqRjZbY zd~t;q9v!NB&~5!-qlBIW4l)_?g}2T0Zs29i2qgmgAw>xL_`uh^#qgNBsQSe zR?*aaWX%iw*PWm#pvA(4l|Inl&CCIYI3;7@2p2FCpjE;U2$W3N1+4wSC=gTkEz*cp zG2h%sm&mnx*0{ic9>lnQwi3iXapDrEpF}Hy)#9@xE0I=j#X4pC8kAH$>~)p`a#gn_ zVHusdja><$p&&DQbadb#jJ(iZ`IWK4a?SF@n9(F}h;VUkaDB*tjp<0d+ylhA5i#GY zz+q|2)Vc0ktl+e~EocWq6CT)R#zwjlu>2U?xKg^5>Z<+Yw-+WM@E^z}&k{{c z2VR-bGstooU+6vp#3j+ce$_oaI-H?H6Jpci8o}Wl#i^50Gvr}IeIN3iZnxoM!O9$u zH-1ru7EYuc=$kTVX!Yz5W}J=IPehAyN1{;D5%Si{-*iZb|)B?lGatMPZGIjf>(#6 zj`+3{njicE{lQcQNUJnT#C~{L^XXQ}mY%VD`*UQldn#55`?(5KAWdi%-+@W;!a1Dl!F5WE1y>-xE<_j|vfR%BJVQnxMLt9^ux6U5aAz$_``P6Ss%cxH@T~rS6n{yUbBuF;oXZ z5Si8elSTUf-)52bb_^~bHXFKUv!Z zV2!wS#_uH-Cpx`OsVjPhgAz(bhBs_srg+%sfe!5uD`Q&7E&%*1jV$C^+!MXLs418J zf|KXVV%LR=$>H+lzLN2%N18xhXxLFEW_T3UzBBh`iL>T_zjSLg zW<93wk{&*OD_&iEE2^ic;42>09LK^8l~UWk0bU|puItm=^AlS}lq32|I_d&T$hOI0 z&3}!IfBBzKyqhynGox@`_c2yPv09^ldS-FUu@B7?tOa~>;_|f@g5bm>{W*# z3xrvi64orH4FE|Y>G;?BU=fO~{UFnZQajVpMj6MpbX+$}kLXXc|Nf&t)99gNv`#vO z3_+*oyrf8WjWff%)w6$m#h6Qep3sieK(=6V? zkBh?EPFoC*{Luxmzvq^uDEuJI$L94K9P!32*&|>((!wzu^LqpKZ$O~cOXt}td(QJTKYJ zo^3yYrq?!=g-8P6j_)sH zoF1`Iv#4eKF%MLF(@7>cK&`hVgm%2CqYgQ|6|a|Mk>W5*z_)}Gr$IhJ=8}C zFq*E$SvkZ8r-|>EXG!9IA!f_mYllkp-EGPyqSqeifSK>fr;*}iBgC)8*2*8VOo=4u z+2G?MLOY+1vt_w6jZjM-gC}#X?QHm4)^8n1okA0&ArZ0$U#7P zTAGAeq>{Z0r5Ny zXB9cvZDX!W6MC|T8BlP`BzJNm;ePN}ed|4Uwgtb&wN}`+@fD6bTA43-L1B|?Yw2in zeEaq4E|>XWFzP{<(J_U!L~+i(3_g!BubFi>6}u%RH66Ebjn{-+Ul8@d1yLw+e0Zh& zoxez0&m7wn(lWsP$nkS(-T{nmk{!1cUDW=J`lT~sN&~@>QO%sOxlqfAAI(? z!j4&!n2)rRKdBS$Z#hn?TJ609F~moBEj!IiECL!mbohweb7a3YjdgBx-Vfi2s;&qJ zz}wMyQyvMfGn}e2Y)|yV<_-1%qz=j8i^g39E3W?Jfj$XP4Nt>w>42;UhDO#r*Cc`6JtCk934F|wX{c}rL=M42I zDt3-qVDhF-j~4B6HmXN4^?)pFoIXv-#(=J6(nI)W7-g`B=aABMucZF4iU01`RdOcN z0jj$Zh@d>5p)Yq6>L(=jj-lJ|CQ`Y~p~O=g8mEnPtWv<0eyBiDGRxYkC z)@n7WdBUbeiD#}-a&$0FEM7;-;SQMRKWfxKTw{fYV$()Rp~lze(Vo%veT})DDaqiJ z-urcrek}Vd$Dx_Zqv#lXe=fX*A_jGQ9k2gU2)*3exj7A>|A!PJ%oSgf`7~ifkP75S zj#k+$*J1Z425Et?jXDhp=e(l=J?+?0lS-Omj2*v`c;{>VNIky_q=2GOc{L5`RF~#%pUIk~QAPP#b*h zZK6Y^j-}*Y(AZ6e6andLAVzi#tum%fUL7 zTLQ-aia?weq2W}m(}fydRUv-_0o(hhwBV(UeJD>k0Zp680X)pYvY|VHwsJ5i>IDyxDCuVmr_}TfU&@@sygp&VfKm z8$U-!y$}WYUnqoJYpCNvxlz6K(crxHvhJDq;Ha>8g%TVk*Zd$@qXyav(f15uMTpND zZU(+?Qm5%)InCwTwlzVzSMehIOcA`u~+a?8|eaZ%((xf%4mwweS=h045ok@B!gm z0lY_65<%7nfMVBu9+E`)C{lqdiTu^W=P*`T_8LtkbMNF|2FHs)psKyZu0Nrg=&bV5PNgL;IT}QtMK&zP zax%&8BsN&x0O-ADNb}?JQ>ct|N3IhFmnUn?EaWSn5&KW+h z*^iMR`+G6X9i1F4^edCH`Hk}9j-AA$7%t|#8X2AHPTftHGFB0szh}p{2}EV~MhTFT zf0RAH<3Pt&9!m+1HCr;cliu?*0440yJtsyNsL`B7gcXirvx7B#d%3m$hy>t2AQGxx z{UTsoCHWge(g(13vtYx%;`3W!n!SX8WYXbyjaP-};8{k?#;^RkH2vcu&_!^8oPke8$rkm z;T~tkv&o^!=&h#kg&;evImU{ou8cJ`;#mCkil0x$*pGX=gg2G%$9C3~HEJ^y9kk+y*CBW1q7{D6zAyRs7rXX%(xTBfrT zW=mT_|4B2m&;|ePUaWdoJ|WlPNXcEWfurGzDBOFhNzC^Lg59^*B|EHEV?~*!lk&|4 zMQz(eR=Sxjro zdJxLvM$z&3jQ`dmq{HV(zRGLpX$h;lsL=4@dG?kTq6+!K;3?Axv*i5Z(#D4u8PUXm z4DwZ16O-M-;V`}jVh+y%u9IM*Okro z%3}Pwd>q<+{#y3Kpq}BN=R-ag6<7A2LZA zBn*DVm^n7F29f8-60d0B6`9wo&1wgbI8rtsxf}cE7pDIw<{BBD>`s<8KPSGp7xYK4 zUkI8pQ1?@YKozmzPP=F#OD%%T5sbe#r){O*UY@?&**eeHC+xJlv~$%J3b|Q<{jeNo zurEK*fbVg}-LfyWLsmVuUMwpvIzJ~1UDZ`wm2Y-|>|tSC(ck(}yaUtEw(i-l!QppcVsK#l%6z3=;-@&7Rb8h0uuk33VkpzB z)r7RbhM=Ag%1ba7kxy)`yQI>AYsqyZ+U{kQdW`tg(aT5l{nPlAt zotO8`8K1RqD-MrTxEr6244Bl0un&qWtS02ifv)PqmFb$mk@w-c&F;Tz?Br3rg+W`#eWx*uI_Q!w7i@F zjD&hnfKpGYcV=Gp{?$9pV#CrL@7P~usq;l5%r|vm=X=ORb{bdMCte(d248A)LCW_q zSU+PgF7Ne0&<_75mbu1OTbrY1x?R8E#o*|t@ncAVCn76OY3WKeLWNuHj&KGRhpxVXQkaB!IS zzx;OL_3-t@?P6cSKcta3HvROHN(Rm816r`)+cea3M#x_BMYd^B`h@MNK@Ybm1iLWM z{B~q?OfQYrntm9ecJHD#Ix$xjN&Pk(p?NZ%1W@;Fr|%?bF!?;C74D7b-dDKCtaoSP zdGqp1SzJd2dqbzdhbCb%<}mI5kW?np4#x4@`zlPpdPBaY&K2LqmzNFez`WMDW`5o` z$0_x*yrn39cQm}alVpW!IwEtOP7x!tj(c*>7=o9Yp^y^2w8_!)G|=vsED7ev+wC+e zvYH+3l_tOw=h=ty1EVQ?t8w6Dlg_CJzVP5B5%O(PJ4Q_;%SF8*(eB-kr-or6`!w%gA&l4)Z2oQXI;D>G29g@P<%< zp;AgHW=~L>r6`)>HxL+cXA?sDjs=2W=F98@aL}-XtnDEb`0U5_*XM{oI6flPm6uE? zI=%kkyr(x>|IGZ$6=>ja%eeVO0^f+!*HpKk6*`sdmH}MoA+*K@KFxAnP`4oRCY#oP zO|n5?%r7#tzCHe}FQ9*_8i6uM=8D(+*duGiLv`z$>dnEu9TMRKWe+?w*eI?%FN8BQ zx6RcqpyTp)Y2}or11zmLi04Nlh}902+7DS%g?70#%0l>Te)&jGEv{FgZZ+PS@c89H zYz+tRI17*d9%H`{aHCS3O{jMU05^(h=ai+1Q z^JHl+@Z0#v>xwu;kjc`X*z?)pmISokl4owLvcF&0BVpSwol~S zGF4W%V4a8`6YrQ(pt`pCB6bkjJA^6Ov@%wrTZ=Abm{g_(+camd080455d&WxJ<0$i zSO11Aa$~*|HW(Xlquk*i4Bpa38LW)+8^t>r;cETzlQ9>Twy0W;G{hDIK}Rbb=eJsr zc$|I%?C&5b{J|Qi%1`s|j;AL91c19vAa?MVaBl@J88wJ-go^NYE|%XgbQxbRI1i?+ z2>mh)0r^XZeQ>VF&K_+ril$k|kw>Lit*LD!^^sj@ZSUOYX9dBRgA`(+F*lP!aItUi zyOwg+GBnL~)*(Udd3H*Dsw>!&gEOl+TE_TidZu z!qCsj?4>rH*>Gqu5CHgzXe`0)$)VdrbmtYoax)@YglW^WyCu#5G&Ec9coK>S2wP2X zP*)C5D#LI4L%VI_2gPgr;gVos*tEIXBD*6*`J!)w(rYi-o7S>ZYBimzU~BIIu09JN=cbfseM>Up%XEXkG*fmF=5*$Dm}=`~X(vNi*~4 zj)AtmVi2&lwh9vwCV2?%bHPHja>0rZf;LSJyV*)g=x6U+i7RDFqTj2N?KkCnZ^Vc7*aHfJ^0JG?sLiVgf>X964zO*Vy9&=e!@4hvIpAjshCGk68p|zq}n#RjUN@xDsABDn_Y5y{|w6* zzYG>UQ?r*%AeEhbi?jB+7NnDtCf_Do1H;HM{Z&?tX&SX>Em6uRXf2h19?z#@M-2}l zcp4h9G=2@vH>`U@nTeMzAAN41USETM;nC~ay7AX1(m!~Fau?TF0|!ddEm+<6qB(UZ ziVa?6&33gsmqBph{lVfM+s)^!1megvJaDEjBp&ixdF{xXCZ>!{xscwLBU*!wm;e3< zYX1iu*qpe$N+qQ*oaVGw5N?}ZOc;kTP!i@V_73+g?#1G`ZIpuHX^-iVUxS&E*pw=t z&BG~k&6Flw^$HBCEmEN`)p{+;c(mnB&$Js?BR33fI1*12;7y=JCP2-0m9!MLCJ?aB z^TctA!vyquWM=-k&CL%-PoP#gYkWNIvW)@QiBwBC907Jdd1n_*E|Ir-7>Ro8D8{r* zLu%N!B`%%`edvEDB(#&_@3Ij@pd7uAMw_JvJ)z4MESn%Z@>JQ%`bmb^y5Q{l z+;cEqmr{4>>e-L&SUIzIzRnos98+Tdh2GHN>Eg}ceWGz4z8W4oHJB|@42*`x!z@1> ztcP<(1T=cnZAwrg3zqq~misJYC9zgxL(61~uRUr3^|TG2MyM86Ze<*gf@=xvF<4n(1SI zEG-9MGP}-LX57w^YHgK#!llr(Ak+tgi2#25h2y2VLk;0|@41J8FA(%Flr#^LfDKkZ zz#CMpulU!37xx;+2V__qk`RyRm1iOWv-?pJ>3@>03rn>g%?g`2No_`!Z3NU5dqroz zB_!+K_?Cd0X_nq7&=XgH z6LNqL3YdU8a1Kqq8L%vXUKRX!`2|oZsM8MZE@Zf3 zK1skr&Klx5?CWJ8*{Q`a-{h~Lg#frZjx=W|^BIj}61rc$Qvuzx}FN2%MFZ4H0VVP!BSdlxWY@eDiA-tYT8mMQx@qkfV zx!BNaPEjJp8{z|9p5z{p0wBB-{Z8|fd9gI8mOi9cz5s^_3TqgGTD?O+WwHEXFgQoZ zjSQpSA)W#4cFGmHr#wmI6zK=_V;;`se5JxoM?k}^BO?VR&5`7i0}=|VFz&IVeffag za~QK>|2hNsWkD%fSEkT9<8pBvo=EsvDA1)#IUxseIkE1?CS!)(HZgCHL7V1{kePy# z*#IS1$T40C%fk?bY=#z{))VLsb9Z!TgakbrNgNuAQsopg{j4LKmgt?ntMT9^3q*{; zU}^QYGNX}k<`;BkTrSi#EcCJ;CIb=7N2Go^_!~9ysn%w|Wg4QS?CErS{Sh}Hu=h52 z_Vbf4#vO^iT^c1MAzTW+sUM$st8woc4!o2^?1EXoJcK4@5oaco6?Fk4n$yNkVl=hbIf)wG%;G=n6B#}6`u`St_< zr+=JMWbE9`CTd4(wqS5-wsQ}dbj7MCUHe zNW;3*!o&iH^JSR-t_0Nz1aW=aA8*kJ74)1n`rI$fCkos>4K!-#D5U_@wW2r&EW99v zQ(s<&0(QtEbc0}Lu34zDUeZM%F=TJA2am&HgwuZiRC{e!{LOv=gxKyyF1v9Wbtw5+ zz5j-jga++IWO(jrs4$Xys8I|%42A(o5boumRMbB@E=}F;mWXxU>YciaFXp@um7G57 zTyI7yPn_N0>qP8rqay_nnuk{56z`hjP6(^W5`M@atC!{%i$h-gbD#5|QkQSD zg#PSUym^qWEFpZC6D?p&8ydc4n|ODQF1kddx*fKjM|$o~b(CwAMu~OWJ~7#3*-#}$ zL_>$TE?a*liDeSS0_?1yLJRIJBjD5g+&z1Dnw(>tX_Mwh!TXARu(Vq;=(8@FS=86J&l_N2T(FNo*w<-+*S4_!b?0WOB>B+1}=@AD~ zRZv+zy|duWu!$G<^dHq{wXEZNTB?T1A<(+IYIIbDq+ytkj$x@5u->v&#!F$wNyNVp z0w=~M0Fu{1d!o3-_#CR4CmjQqug}AxP7zK!`;^$s2gEsJ+ltLaR^kc2qY?*`Wr*3@ zkD1S>s-{uX1zgz&bi>(J0_jf^;OjqfF}r=K^>)C)+J7-M?*cSYBjsltC>)r>>`0lm zI*J2%=a>7o9H|RCf$Nn*&`p#gZ;G=`Om~lKdYXT)7jeZi@L-MG!FINTBCiNJ&U>e7 z@DtpT<3BCmH(33?c?tuYpCEmP!dcqJw|0b05LJ!Bbb!tvA44!>a0rdwRoeSl*S-C7 z=GxaS-je7B7g6K2m`ua*O;JqIbhWJ5RI|S~IJflSW(XM_!{m^Qi8W$+qW_nhLvW)? zw`w8B=^Uy@l)q+Di?UZ!7iZCvGQXVVNC2j*8J_1A{iX0%cdOm1%`~lo`ZdmI)b9|> z?=U86u=aPgVaU=O{i=-4~E7C-ilI8vAQfbttp?@w$<>{~e8m)g(gFv?0CW6j!rl zZB?AOAsKJFPdBafNq?k4v5I-b4=%voBUQVmk0tCkW+!<17tLti2z_kwjFIdgbMZyOikAOO9a-mEKx2%BIP5FnyhXVX-)# zKGX2CsBxt#$gtqCB<~Wz#{@U8z|ccX@1(Nz`3Mc=^7NgsRk zsM6S6b}-uklq(-EsKmtw-Ig%q!meSxAC+e(t9=lV^1 zw7ZKp+?)L$u5y@;s}Z`5iXKk(Uqqk+cuDrsGI*ejF<54U%s?^-{CZ$msT3HpCORFp zbY9EFXJqF*_2s-DMEPD$K*(Ki0nWB){ryi2z`szXG|E8a*P*zp{ANH_PTNT*r@e(g z{hty@H!`@4gbj@*%?ars+m1pA)`G?}6&~+^IAD^ZJNk23bsQs8z@AA>L`o#~Dlg!r zS(~s>{_mnv7W%TfHf0IXDsiatbs_|s3`;j%l#YS1<7?A@$W^q*4;5-KJNSj>sP?X)sqP27yKH zqUK#%VWpKD2O+N@!;g&EJMQ`aC@lE|6=f(G{gz7cw`E9XGRx;nQ;5lf=MoRhJ8iqX!JHWmyIgoh;+s`$g#1f{%ImJg2j-t>oi%xepN+{JRacd ze}ZRLxFlf$#AW;3dQ^=&5s!mdpZ?Yc>8>M^ghEx$E9$o%!-BKB4293-6HNjd)`=^c zr-kRg1CoLUW>*s~ST9=6?k$_O@Q!?Y!e0eDe>44X<{a!Ru5ppF^qzD+roBJ#Ft9S( zcPv-P=Bk$Q<`zGXqZ-hJiOf`^xx+4Q9K+r?;wR(UaCnk-ZExO0jI=9AG>F?=!}5zX z74nNRnLZx*iC(FXBbd&7#KVdBPbmqGKl(`u=F9#|0 zAbkj`5S`94)SBU)FZm~@S##Su5<4;TATQ0d%G*vro{r{8g0R;I0**&cNk^2qWim>x zZdGX$y0cHbDn00@?>w+D)|U1CS65fyB16RwAiXsWE;2t-eJ7G(q9>^lXax;bgWjg zJ(LsYo>Ph~MN@K@QELm1sM(NWwFoBRZ?;lUDID1rGL>u?%9z?VDB?(d{cgqVs?L?{ zY(m`=H8qlB)Iks0yi7b`_+ipHc@Z`fbf+_mO2A`TljmTS`c0s5Z;b@lf#*|1H{2`PNi*n*XwdN0S!*QNODO5i`A zyqS2hsU`ZTfn$Dys1(3FeI;uHxw;vzpDqmqiSh}0+U_-e&1E;aw80T^_gdxI6r+i& z6kHx?;!%g)E+LjzH{=@1M~(Gq=wD@6L=jf-TjXDO))hDmC%4vM60+Qf9 z8MHa$E2Pt6aRApu+EAkUFiY5D8<1+enfO*yLBV)b4Eo4Rd5^-NZI&hZF;n4LT#*=M zpOM&7jJPJXR;(>EmYU}8`_CsORBGGl(bsDyekd^VBSN8lcM(y|}3Ig1{eSBsfA zUFD-&%dJXlyygR+4umci_W|!kVln>;O%a2R8SCG}7=8(ZDEkWpf^Xnsvjdl!jN=-T4m;(|#Q2zOte34) z7U)pR?SvuYxU2i6yR(Y6gX$l%HPb$Ll|FclZ_kmhkNv6prLOzstos#$v(@?;{?9=W ze!^%x+D3$;4o$WT%v9^BY87s72e!ZHuMiqtL^|AID?ER}jNkX3`Iiql5*u69Zg*GJ2`FKj*d5-7OAf+?AcP zXeI}IxlApZl9cx7gRa83nNCT*8$P2|j^@^1o;MWp_KRi7>54y-JpCyGj>y*E|0ykr z(Ksi4rl16X%yGJo=I|u$8We(Ft1rVw*whGLmyin!Kx9~m_eZJJG8HBV64U;&?EqmG z`yM$yTKDk=Zfg-sG>edi5BSYqsRntO16amO&$ zKyTs)hvQ#za~|{dpqaly+6HbpJe;N?z9u#G8};>;q15;JaxshbdsSwc$=}<&u;#;b zg5|7s^&tiD6(K2aZbmCsu+sMzGO)Nfkz&fctyl#?jH+h3r8_KT_*RiS>?l17HlE@` zS8zTQwW#{HP58!lAxnd7^5yu+@GkGGa3=E$B##&rYSSlH4Su{|fMX*gDV#RTqgl_w z$ZT{nEq_kpCR7*79~qbpIOyTKyDc+Us4mYCp|8s{-3)cbBpjXcUng$LZv+;=j4m5` z+Xn%2?p##Ue@5TP4nwVW^7T=o&xw7s`*^G^MT|U;No#o z^8M3D1`Kdimq=Ue>Y=cQVIS8kG8_SA*Ff#zPZ*Q-r`91hCH0SPLbcbwevQFSX?kBm zR7mD<_gHQy8+AvAlC_;I(MVI+dm9Ph`MS?`LLQ$NO%3l5go0P@Qy|L(>HZPZ48?9r zbr6F}EXcUprs7LPzP7hNIfb3`sh=P@iZ3%nX!gGvW$BZxHSKixVH6lQq|l}y*e_qS zGd1bsLv_5rN3WnCMpRAExD4o7a2CLrZ(d0}B=#pf*v=(wjqRk69U5RYK#3Y7^p5w` z2O&obP66N8mxHNenmai*Uo$=oJ|Ne{2pWke*)Ni2;ik3-EZ?53O_{E;zyvhmAD2vF zn05r8`{fAlYpR&r+(nTRRZzTynD<}fqus`=Z|I4(QInL`6Qt7>wo^>@nzvuH!fk44 z=nu~zYnt5_$8xs7)GZSh;v<~4cXUR55$mA8iq>mkUx~!Vb?Wx*RO~iA7#`GMt|u^4 zr5HUQh)n%{8jYIau;pPiyvhv}uro}?u+n15mD+<+)2=&^F_ISyEs|I!B`>~}uy(+r zocjSjD<&xbLCXRS`c+DVxHyg*v3VtfWW+m#d+@vxwxjqVY7>tl(H0vY{`c3qkMYWC z+n{amL+>!l=x-?VwEEN^euKWJ2L@7fh&6%~Ovi{5@xaB-E0s|-)Zk;ru>pwN+4L{CLb#`^7ECX` zCR8Sh!08lN1z3p_%w}~O%edMYtp6EovYU|iHJ+Pq0AXEf?f|g%T1^x#XHQZoWLWUIQf4!PSl4L*XK^ zR&+Bi8uPiZdx{iWd%wQ5SqCmG^K877Pw^`a$U6tmbmkr6cq;{x%Xkn6b}jNHOGWkO zEJ1<+qp2xei78K?x1T=}fkn_VJAz=A1Y1JI?uLjY%~-B~uc}TJ|0ZaHJd&GP^@2Qj zM4?5-(^jUZxl5P+zW&DOa1bk0yO4&E4GuQ6x1xkh6O?si!-R(v6jrfZSn{k_$rBW> zLdw+PHBs${w*@((*qCG351sCvWaK?J{S_Umh4J8;H1D2g9y;`tu_jbw(^$TOm2 zm{9>YhypJ@=jS$R_7HjEw+tzc?7kcr?CWCrGhA@0#;|d=cG7*0{*#=#Q3j}qfyBr` zIs`ILXTK?zHU53mGm=AUCxkUbdaj{h&WW(^D1vjNJEpo!8-e=ow1pe(37W48}X!)na4F$jUalj=*$T$JNFV%``89u+@^uARS8OW*;nn)r!gM}{0?VPQJOc$6-}&)9^h#HiTP zoeZ6*CkLKVQ@@IHdAv@V{D3DJ)=WzpH70Kbn&ok^gYk@*&+JnhhvJs$m!$8;3$!qP zZ|#af6@$qQUnN?PqZ^|Huq+yvtN#$l-@VkGCSGC=s|R>_H5zsCkyq&caHW_0=+^h# zS1I=J_&b;7;+3RvIK0u!Wr)?0(-ED*CO9r=DNX6b7ck^mI%`1FdgNFuDb!fK1&Unv z$d|Ns*C*tbdzW<`c-8|bsT@U>-qjAiFy6x{Kf^7%?^pt>Mxf-h;u++Np-sN*6h*&q>; zq$n?Uu-db0)Ib7plj?qB*+WYvFfj)@`{gO5h%hIo{&ntyuMy)EOgB1f!Pa>0qb3*I zo4G}8d2&;|A4O=e?lc+Kb*u5Veiv5?Lbc(bg8FMWA&pd zQ5r}y3Gn0>Tl!F?*tmRzj1a|7^f2XhUUg?Kd19qg)1!x(bva9j0#t)^iWuceMJm6C zElC_xiAp((aCuzm9*Yi9?vn?!a*hDMicWE805UOxKo@8xpEwPu2sZECcaNWenGXYQ z7zqiNmHe5Yv1nbGy+3zw6{*d;H>R{D;w6LL^EzwQ0!IIghTh%^P<#i4bD*A;Vb~M| zk7xOOuP1B|KY6qU?*}qsS>w?eYC$hC>LZek@eh0QyN9rapdq0Panpsej^GN1+I8$$=9K;;HdRh_e6pe@9(9gs53)m8@{1CT5M0A6w(kCTa*XO^K zND0u_Hg$4#KuGabx+NoGyf4qVhx27Db`neTnoWOI*R#H_IE{;WIJBfPkM((9RY?vD zZ=6XFFYSl~5(Hzil4m}+;=b#J0X60551Q*zl<3A~w{$EvzkD^b%Ilc@)x2f-E{|Q+ zxyY;8Dx{5hl{^XqMWcnzk^*GNPMCdMQiS-aQiAYQP8`v z)m6jVnHV;MUxX8~{-~T;sQ5&C)oXWtIc>DOL3?0%H*1KJ9vHU_Q%$r7x5nq@gM%t%bxcI>|b_=#5MgQCo*7&0S+R_AhrhLZj{MA=ps6RNsa+8+ob8v|4S)^kIbyk!u1pLsMpCN*#mvk_` z-C;g5B>I)Y%b_??;YXjoRaO_>(s5Cbrx$<^kuwo2MVv`UGkcn;a4(bRY9DBbJN^nj z{A9Grz<8eXcu8mW#N8@~vx$EFke*m5Bkox>Lgt3t;^$vLv?}OeR+%P$BDi8*ze5p8rO7=&d3N~zin+s06 z%Y2J2H=61{)%{8iuHtVSN;xj-D=FKWw8M{IdiC?ZD^I#2d+Lh<42v8ACtk&NR;k<_ zRG^Xi&9eP& z%pi5l%FcfXRJV?TV$4&eeQj?p-!&*@?Hk*qGJRMcIVj!ZU77*}OJ0_=8Y3W%JLB6I zKzBVUBbu8UTHapL@I%{*>(~*=@m=`4axs)m{mdp}qFyk^DqzO(*Ix+!YSJ`uUdzRx z#g(-KNBexgaxSj?!Lo{&&n)AKH6q-%xlnn^2!d$mt}Iax)sslSUt2y?K} z7GheH{Aal(6#gE)w-zMUFUs!FC&QUJM?Ej4T5^Ad^Sy#cb9uILGQWuNA(>6RuGAY1 z`Gnln)Qp_!sBzooD-jft4G(vxF4#h0x#)ZR^uuW$0`zq#RW34+J(h`|1}NOgo3A-d zHZDXIzAk)+U?wQM=fja$l46J+$$$;-fQM1phfb&DBQb6z5||JR-rbO4hwaD)p;ynu(*8a8{I9;IIIUj{wUlts|UWt&w1|D7h$uA}kHM_rElq3-B$AmxV{ zc_CfAdBzWB`fIorLl3p1X2H~V33Hifz|8_eQC?Tr<+@fp1IF41{5?IOPQ6~iahGaj zP_89|28yAKT&h8FnrNXcx0d)Y#m#q~%9D;F!rFClCsAx-tS2gCj@_m3TlHSr(+<`vC{DX=>2RF?3E%34M{ zHA*`)#pC(%QjVm0YKw-)x3+EcToP(3z zvN>xj@SLW9Z#`n@LCUyl9Ur|JRqPfNqylPkHrO&v*}W%q(!wp6&TzyIhkSf*z8>rs zlF9S@!Y-g-H)4xNf}Gm?WO=*yB7^Vq#LlGPM|^fGrS6KLpC=t&PHowjOI*`ysh#1N z@V;#@{04QY)$6MluLJdxQ<9f?fCl-G#|>iqj3^H%NrxQE5cObN71-C0b*ve?%LmGF zsxEz82Kr&9&mS|3+`;l`)-Kp?Im+7=HPkt?nFusRKpd~Pp9p@X>-8FS|AMPxe?=^7 zRAnv4247{V@cK#8Sb2AShFfly@ zd0?k)gmO{}2!Vs*Ao(%67@OPkzdh`IyPJU+?yU=ktA-!T=Web>^Z*QT6a7qWrO(H{ zU1yH9ZW=My0yivy&Y9DJQ_pkgOyJa=($vUML^W3`6XY{yJJa2tp!Y$04-&!O(#P8F z)rxuHYuR&;e%&YE(09Q4BVgh@(&=b&!z3Z2KTz z-)pII{9G1GM4((aiMBU1QVN5bGhzL!h6vBJQ#BKolx1-Znnu7g?9M(Uv2XL!8wIbr zov*oKxx-}MUv|kZl~%KEL#{u)l)p&pMpG9rVsc4x5n))@DD|0(x#XW3(ZBWwYu4ttWKl-+7c$6 zSdw%&s0D$e8aR>qqlks()zTCi?icuCa#*#1uy!`YFzC6uaz#lPM1FD*mZIc7rLZj* zH2TFAGH7?4qCfkr0>(vhl6VbbA_N8xfk3BLmWva$og7=G;#Pr!%10mhuPZw*_TkMDv20Ksxs26yctw?bp|<6 z*gLs&k2IDP)%Wh{f_aHbo=-U=|M0=N+0Mtl{CSUbSAFj2AJ%?{Tsx6W3=T>YY?d5Nh5+ z98H8^Xe)xCv)$ZswJg|G1s6regL{n%V^$A|nq*Q=38;&UXc_$TDtMj|vbtPR(@eJM zFFBepZH#Rh;)4B0)yl1VO!4odMutbqZ@qdG$}CHYJFC!VJ_LTBR0a;Vs3#T%pC`(Q z6|8?WFFw zxsd&$OLEAgw(CZ&)wA9q1leDHF3~hi*uIH+=>rtSd7j`Vh4Bf((k%|AhiyRnjY#vL zIZ;2R;Dy3$X3U7@Uj_n3=b7UdOG_y6tTx56;f1WcTS|sg)T0!D7%JOKh0$fvf;J$E znB|x;H0{}?tYBmONdh(Vbwc*$j>iOIL&{BAZp_2Lvt?7nzFcyVK=of^6S-eDA4)-| zen|KJzC{nnng$75>*wOi;`5TpnkQLm23bmA<kb(KIX`(OtZ4Y$(F8M{z#b3 z_F=pc)%+J3E}Hr2)chV01h(W+h_Ahe7+!tSA}yQv;QtAgW-2YoGjhcuDu1Nc;rO^2 zYvkK68v`9m$Y}p|OGuJ9(|COIfW04vd|zO=1tQvNp8q{dE}nLCTl&u_|8vU!obvy- zPFW*IY?n6OwaVY&oJ>bqV8)=8aQo< zgD{SF=7!1?0&X6o5uFBOTH@YrwmN|^!O$v`MY@4kC z%$}WGy2KUX6GI1#=S>>}_lnMoGohyp&r@tOnGItS0lB5NDwdJ-2Dw(}(6g;gXRjek zPwq~e@mqMiu|p%Uz4?XmPilC7npBI?>MjONAQVn<12Gy4txkGx z8|*QFFCi!asB$&hLe8te(a5X2)K;~S0v-=-f>y2C(boxFcOxPVLz3abLzMGhN%3TlNKI~fv7^Df}FFvp9 z+r*)e-S-@O3_+X%kBMc+lg8M0@*XW)t0-fH#RNtX3UH4cBlRQVqQc%Q5_=ONzO~wz zzPrtZ`qC8-*}jIWn3~yBi>_fsewJS0D)qw0dTPly&U$gxI$6>NCqQG5%)!AiUZrF& zv&42}s~)h_dKSo@xXno0v4v6UNY^ZsRo2O8rg#^!eN381 zm}T*1>I0fFnCFL_Y5-^t={VRZO>YT%^s@U(j%oFdzT%f!yYT#et*QAFv#ZcAdAmBH zrjS_PZ&$HWhdtx|bPW_Nj!| zN;TVX^nw@n^6|VB#vg;ar+{EXYIp6cxy^bWb=Gv^b&E302w|qego1av(EC4LerkFc zD#DVoBCbHw2zaVJF^GT^W^M^Ck{o>w1OLvT>zG)z0r~uyT9~r23QEh-a@!bx?e=LB zZ5^lVQWy04JM$0WCc3X=|6Pnn!7yNnJ_qL!4(-9{H=m>mub-yu>G+AlI|0 z+JKVK2uN~E=FplvWZR`EOH3%OPQe8OctCs_X|k5Z2lK?7+(&$A2cldhB#inZ3FadxkeC$Fz|dl>So9N zW|5z?&zdQo(}Y5X3}|Dj1C)5itfv`T7~S(diZYh<^Mmm}uKbTH|KrO4xbpu~uEgWg z4Q5TEUzOhKQ7W~*tH;M)yI09>xykFvo2TYZ^f6;}KZtgzF0U{0mN?hgGaXkfm@@j% zTDsvI)Nq)MiMU6UKT2wiws8A#jhgKc>{Vcz8^rc}e-y-QPgE#>-8j#aUjKO?Hl3C& zeIJhSvR*TO7ZQYoZ}{ev?%Fsqrl z8u!xfNGEksf!F%8DV4hnPyx_`k-b(!7M$TnR^5S1!*``|GlC;tftAl_e z&Nzle&!ySmNEUS`8d?PM=TC!3u{vf|8z5&;ylxHCxZT0Qux3F*ze}xjBitWdE_>Nj z5H*Z$<{onvmuDNgm@hsrbg}bOS98ZB0<2N62Otkoe?IQMfm1ajx+106=G}Z6r3#7N| zUyd?f_6Ev6YCA0{j+nJlt5wf_ zm}<`|8xlRcgDg8$B$2bFn{wf44q}3TUZEZX1MY2|n-KOdvZ2C~4A?~^FiejJ>}i3I zK>C3HpIZK>mj9{c|1N5o^JUldInL`O=6(W4@zk}7ZACUQvg(eMuN%vSf;S>@?&=vF z1IZD3IVo$5veNUl?`j~~qH)gmeqaBLFX0kfx^vCGFevRHGsnuD!PPi2x8WLenwxj5 zs)lXxFYV)q0q*cpa;c-q1VLSII&HYHR^mBD0dj9RFO0&8283r+lX1IOVu*jSUf zcD((pAa33*yEI#Mh`2_-zPO}*P~9(A{kJ4=4J#uYD(J~Dn1P`Y^leO9zcmDyl`u~} z>QkRs8E|hway3L-hzCqC`%|P7qK#<=(a7z#5ipoT$812YRQ=L!$lfjf2x`e-i~#yoExeEfm2 z4y3=SDayHW`UvDxf5dt1j%i#*gzRnZCRMb8oo_&fTCM2^Fl&{hjbfWOqsL#Am|+kD z+j!fbS2U#aQ(xKF-8?jn;$c(ZJGV=vzZ8;5wti=lw*qu8d=P@vS<+daKA4eD0};lS zkR2cl{096#)twW+`1l%yo01VAU$$`5c*x8tcY0`l%p7>!wW61jiS+c9u5P9O-Q%RrC>0Ld z{%R#Nom0UK*7a+n(W23hsQ>~9mENP!c*>VTzO#cA0$mDh&8+hN!Ucw--RYjSE3z$9lUfPflU};v40tubCIF|+qv^}k8c~xTOCwY zU7J)Wu?5?ygms#)=$P*kYj}lmwOS}S$abDErQ6_!a?ltok>OqdQlbx6(;zD9joQ@j zuRxGMA0~4iMqCFj(>5rb^pRFbd(1aCO#~jnZv4M*+~hX&ohuvv%4Ia#eX9)TR|g%< zp8=uCOt<)xmRYS7%Awj##)9_@(c3gNsq**Wz^#=UbAl?+KiuTTjDE)GIpQ9n7|hg( z7ImxUlt<}Z(eUKsnfcB!u9s2nSbjLYd_>YS*iUM%M>o(Rhnz+t?`M!u-T#0)6lasN=Q+{RZl16^HAm|uiwGXG)#lIUb2m-7fvIqluDu`QW#vaHlaCX14smVwE(k%-e zxlfFx{Ir-MN7zqI)2=WdvyS?_T}>{CxwMv3*F9F}_2mkjZxJMKq0h`8DkU&-!iZj2 zo2^)Wq?^E3)@2zHs!~)`z74Fy?CH#@p!&auY6{`4Li4vxg{Nsj+#277-7%n(pCV*1 z14)EJJ}ejUypdv{4>dxvuxQt}M!)4WaJEkeX|iw71+ye%{VE-)9C4a!JX8tNzlS1AN zB5j5J09&PXCUz4+@(WCc(!!xaqWhU$H@u^IBp+E>gU10nuz{|G6SWJ4c&AZA+Uw(= zapli7UZ(lUd5;;_y__F*zPf|k#zy4qy}ten#YecQDq65?rtIdY17SzW8VULi9e>=A zGW%1+zDx|q(D z@KO}~%9K_Z7j(8?U3|y&WK5H=ePz}WzrD$Vv*JAqkE7dH9Q#Y7ZI;KF#!4=;8SUq2 zS7xlf!N}gHhxmYA$UIo-@toI1L2IUnhw;&B{ra=~%5$NLHhUEYP{U)uZ)~ld?)6#{ z_{D%qZ1b1yD4e{PiP|=0%2i*&wTH*JJq)g$5&W=u@Cpd1>?^y=!e>VeL5U{(YXGg zk>9KRVD)~nxxXX-8VG-}qyJ!+{GbWHYxEDd5FDfKg&!xPr})k{@rM#jX`qqCEsRU? zc_>JbvfLcxf0wHe(=8q!X{cd`&l&1kQGSa<_7; zy~`yskm(0>y7|Y?BT$bWXIa>eIVUtp8XCxaBkC)y8K4At|Kf-upAQtgm#a8Ee^{ct z5gwmN-t&weuGFPW5T4?~>%bEDkhuqGrbB?Ie$-Vp9j%aXV<#oNqQitKk0nhn=hGzU z0dLgk+=kJapWDmU;zV}=i5Y5yXh{&HPK1b4o8dzi*01;p#Akw$9rpQa&{VcMB4?lASh8YdoueJ_l1XNj( zDq>_7u^E>zA2~#;zd zDjt4ac-T4=i*08 zK@e*bO;AK@c_^32Z~r?uFINJ&p2YUXAZNl_>)X2pGPP7}ahtjaGVqr3S?nKxkQ>+a z?uZ!sk!n~A4dsO`@m<8CD(Dv{eB#Lq{T=x+YM3Sr>FBq>*bKWB`Ke3UQPu%OTl+Bt zgx}PhN~8%W=OdqpZzaDlc08hb?0Vh7&Bbo9VycY=*Q%nn)h#T{(277V->^8ZM z89X-=9!F2nGx1DRaC1eT&DqADmsr~>;uQ>0MKP-2AAI}mg9c<^aU-Ays!9K^A+tlI z^Te~k2hq}^9~+aCzuB8TYF(ed$EOpl30+0OD1Mos^ya-=_H6fL7;&_D@Oexxu7P{& zbD1p7{IaXV7NYR5*${KE&|Er96~&(`XmDDoX%d;NO6?XNy{lcP#4meh`*bjd4G+Z6 zp1JSe;f!P{e}e*VLIi}DuKEiEX`&)903+IM#1Is_g4BDFPZ}@;Vj9|Kiq1xvFAFZ&eLX+Icwohbme- zio7olOj2h=Rv`gNNCl3z?<+3=$&V%~I2<1Fudi1Mac1HbRc$pb_a5u&(|G22DZf;g zrc@Bq_5i*X4lmb|?DQo#!kr!qwxo#T&b#}yo)F-is0}Yka`Hb+?XFqeZPCWA*^3AA zS{;0|32GM#egX`Jk#UmA$fGYPUsKz+KjH~q?U2~VLg-wi6t~3-hWjt^ zSr+0N(R&q!TLkrAq2f*G9WgbwSW_6to;(e40>lsYFnd)O8Yt4mrKj**Owsea$ox_= z?%%O{0V+}Y{2Rmk+iVd6>#s2RWmJJ$*XN_%{{>u*SoTW0m&*QdFWcpHM`t>!<0q$K z2?zaV6%6L;(!odOe^k!HAagw~pByqVbJ2Lf+bSU}*9|Z1ssy)WdoVQ;#S1mlQe5oS zMLan*>#gk!<3m}uDy9W6=wp9G#efEH< zM-_rNJ(XT+WNSpoe`#Lh6@-vRLBOYIqj2Jq(}!uCHzsM_)l3 zg8dRJ^4Cxow5gO7o-Hu4v;+!m4Xp@G63Oh5FU5?9sUUY%0FyQTxHODTxiLcc?p z#o~^GE&h|LPV@eClQ6_-Dt?^1>K` zmZG1obtz!G3#QC5`W1TmMdHjr`8k|;6lA7kzxa@je=Jqc#`$u|?uWVoZM3Ig7!+LO zkiRc5=8-azA8D*pM}Vb7G{O~vIB=0T9#)4nNx@QR23@&d1JiL3p?NW|^G|sXy6AU)YIV$KVeOzn1*944Uv}@t9DEr{^Z~o8s*hV1M4PX4 z&@q>3CXYyCSK>jnz7w>Nv1TRl$5X$fn_7}{z%TKozbC8?kTBMaSY$Zj`$8`#uhrdq z3U}LuYbYCaQ`WUJ*{0Kuve{?_l;32_;RmMta^678)ap+vq@z#TZzt4w72Y!xWp*(+4!ojdR9Zd1jk27%8$|EbQ z&?`=tB67Ww;9-r74`wZ;F`R@x+`OlA1cv&@zyUe>?CZfHI=v7~a+(XAdZsL_VU^eN zBScC@scA~KKmtqH_Q`3kdHWY0&>6EEF84Su(k6iX^aHRAR^+bKr8s)z$i)x;9<04F zP8m5(X4B;aW&q}Mx6NmFXPm&sxvP33v02CV?1Y9yV!6&(hal~^jMR0mjI_*iJ+OP; zJn#n)ebj7HyU2;6t_e@YI>(}%o82VETM+1inO?HNLQPNb9H54^I=l%ETz$N=6i?YK zlN*LQE}tP?sV*yoOKj+?^x1#dm&mPP>5H+NbU8V2U!22ok~jHCE81F!;<%MKs|TXH z%L2Sx!(SuSDSH<|Cjs@F&z$D88+K6Do0if=HFa7otJ3A5^|^YNf6)A~@@Ye5zlEH7 zcZtl)pQeX!H$N`KOHVD$;Y(G2;&^DZl+i+OWuc3Ajh1i%i_AtbST zT0Fa89bG?z3s1uQ#?LLo-;pJs2=G_ss~O}orMV*a(CM*|X3oQoM7oHHf6gLjhqkv) z9SjhUK#Jgp)<}h1$|h}U+p?)zyK*9u)maaZR75sK{iX9riw74*uMl80ZR6D8BXLvz zeDA%u>3c9LZ04iIjevSTj=>2W$`Qm(rY&TCQm&V7UHZU6^(K*v>!+>FmZ+V$-E zS!oYx2&ZUuO{3O6w;5*BjNfbJmr0MC>L=yB}> z_CDxu!SD>v`F;knV%7veUA(`{ssbyywo@*2B92YOfA!;laCnr?vN;6J>as9;Jq+DD z^~Vcm^x7C9MuP3Om|WNc{Xus)p~AwIpcIGb4$Af*3EV!{4V4 zudD<_x`En!)NKiqhhsM$PASqU)c(HaOL)zd)L*jsAvlVhhC}vCH1V{wgzXm43?~dd zdv9ivW>2WCHk*7qT9SwnC%i-Y*WUF($7WARDkcIL`HO}x60$yl0A(lq9geU+hitMC zdqwOEvlVDk@qH8Q3!*o*fmIT*>Bg;YO4W>kl7`%OL68x%_YVKOP(JkdRAtH$*(ggq z02kq_(HGeNt_5KJ%VswIS)qKDAM---Y2T!<%;wdoZkkVdq}hx1b2Cd=FEab(5?FsK zo+zYhqwqu=5lj|UM78;}qBB|IJemO=uQ|86{p7|y$Ch?4SUnC=Jgd#(o>Ck^V+oii zL^W(Z8WnXw-+-tfxBjR>xYY36F#hMZbdwNxe&%Z;s@)3hFj%Fx7h$5}&}sW9V}?&6 zbAGmdX0?L&>Dn4kG`XOJk+JHDLA7q5FmaZr!&ae2hF1f9V$;*K3(C;h+}(ldx@ub( zh_n`K>W?^7E?li4+wG3y+3@Ax3X-S_=EE)nXF`^)yg2TWQCLwtLHJmPg9YKESMR0{ z*=s3HV48dV5vVCX=8+PTK!|V$(p5$DrLo;rfD!R2zal@pg6FZ{Y!gMFKi>TB&PB2LaJ!qU>;+Gt%D41=V`FpZ^&t$?vzmN4CmOU?X2Tl zL=3gRy^arlAT;?fR)efgoWm2?!AG>Q2wF?hd%xB~b#a=Jq(hw^qGz!P3=EK3O4t?% z#JFp?f`ys~h7iP?EX7Whm7&OXA3+VOce>~?>Js#g*=9M#_I^hvxn;88#7slcvt=}F zvV;h>3CZ_l%UCeqTe$MKUS^V}90g5A9A)zR7*pMJl371$0ZPek+JLo26UbZh31oS2 zO+Q*E{#054>x?POS9A*~+fS|Df2^FQ8W)+Q*RD@)rwgI%Y)H%>a1@RvD|hd`oq5=* zC0!%#@w{yKZ#SQ`JscLS+>ilIttIxXVZ41w!ODh48nH(SI!OkfO?RqjM_A}-b1nqpy@gi?I98CIV`juMB(EtFfd&AHWN9= z9Vs6k+e#(pyg6~)BqInCM=%yBBQjdd1apEAi+1A1r#HLRCpL{&Xp3ngxBTM8CK2zi z+xggJRd0fe=u&-76t|RUmsk9wXaY5f@;KdeyKojA;8q{jDfAO08B^%wo8BRj=4%aF z56FQ|=ph#vn{2dij^eyk9}V9U*c~B{3(K8EYIJvN$Gs+$i)CX(9%c|)lX_U@Y8m`0 zn*5K~gjrm!1Qv~aWrO_!)=4>!Rkg0k5E_4R3cvnFdgMR4CBi?B=^?alHL>})XpSD( zjGt&NfJrAM)SSTC=`>Zc$I&l+SW36+bWzze>dKAtY0WM)x+pfeF?1qoyM0s1(JTVv z%V<{A9ww3z()je%4Sy-((mYz6ze(|YoR*BKXAAh?)l7$8?bI{bP2yl&;FTNqx%>*j zC(ecNjuUAbV4h6Ci_LWX0u(JV>;)0!>Z3H3j=({@J<2(&OLtIbQGM>zk$^u+_A%$t zfm29mJ?Q__#RG9-IKtqJ%r+b~mi2Aqt=_2$`(ENZ(GIS`FOpp(fNmM)7Ms`&+FmD& zUgKD`qGWH{2J+U;qCdabJjo=a<+(h6pYd7KihkkHHK!5}k){}m_#+l@8;L|qzUzA7 zJZcG>M4-OLpXPIQZHtW}uT5`ULuPe1tuZ>K2RV4BpmUlZOTuzbe zIGjs3WE2UpkgVcMb9Qe0@$b3+wi7z| zNC0tH+7E=XpK90;4vkHIWi1hpbjF@A@4JX`9_rf%tlU;Omjz8y3L7UY3&hW&GzoYz2!Vi)`WdtbQ>^w zKh1A?Pj>gvj6`NNz8kY0*O3 zSma&Dw}%_0U+{b4-ZBo_DhW>t-29fvoTQ$nv-9nm5AZK^3X`dYvny&F0zF5n&ew8Z zj@7bgQ7!KUR{sw3Yq%F)kCVsFr|Q|ROS3>@MZjf*!`rGo4N`acVo6YNLKA3$2mTk5 z#{tdBAEvW(kKz82Mj)9-A(Mj*ZbR%w4j@^NU)1!@0Kl%Iv5voHR0uhk0)sa4NSbv9%hZ={{?DmJV<}f~YM>O33MZue1lZYQB#67xl^x3m z4dWSt?263VjJ5Wg+&f1v)%)?V!hT{TC){tiit39V;M7q`SWtDOyBOJoG275(5x;=q zfaJKu$G&|5`4PvN5oC?Zt9BW4>S6O!9GWA^2HJ83u>Ux;v2Hz|<1Ea1tV_ui^KZ(Q zYd~baIZWA0%#W2fd$Fw|&4|B30KUcgV0coa;^aIxoL#w7dF%w#(}^4En4Yi(~AQ>yl% z$@cD(6jIj=Hr@DZS03g-Bb-N+x7?{M?{|@q30JH`s0Wzw4u9~|zg5wt+g-Z2BFuam zr4-nDkFtKlo!?_EtD!4Kfd3|EZ58d~|0ju5jim}+aYI$bkva%mw;%aK6S%mFir4i@8R$!q>X=o%*G9>oeLxwTlOS+o|x) z9Sq;;rtl*UC$FtUTTOI*w-Xo0pnNB{!+{q70yZ9PQ zAAD3J1SVz^NlbFx^DfMx(If8Liw-?!iKpwpq5ecg zvK!2bEfMt?KFO>)_1n;|7+B8S&k6yTWL!HvihSX|j;lye8D3J%-^QW}+Up{Il!VNj z8rxMx;oZx^GZPRI^4;)D{TgAtoeom=#eX|@(U)d=H_Dm5 z6YK#Y2-36{xw8O#Za0wnhO!YxhYbjP({EBf-*o7oIn4^1#shSWg%#^{tsFVb0Bn)} zMv=j&r$IWM4hrh;^GJfO6(BHuNy5x79*2*;DjtAmtWAuFG(Yt(tz<@c1@$sKfu|NT(p9)tuT()M-VLWzYHU&(A0E7sEzh4RDWT?feAI52OtBpw z^3cN1Ujh^7xKUTH$o6q{H!mH{x6x$- zTCM@kX+&9n4|@)bI%qQq1pkm`slorj%#4?o{DU((0cW3xDnBwvFj~!LQZ0Gs{GCFE zD&MBeMz?1v0Cs0Q{3#{}n|#ZoRm1G@_lX@LWo`$$ljkK#Nk~$ z?*y3bATYIPw=g++jz*T79P2v-NZ5JMBq+(@X&bDopUjPKzV_fri?=M#TzTRQ+brrv zHCZ{|ePlu1YL8CYeyZOwHDyqh7G_ASGc`IY77FW@43>E?Tz4`PGZdyHFuS;gJk(<> zDti5WX>VXa0`=!Q)CRcoE)LopCt*#~3l{d#D;JcAHMg!Y zD!w>c6f)Cy?&?vcy%9W|hht1GY$w}miUDvmQsv@NZsO_my+*gM_E)vj-OK|T2Q)BLuc=Jm)uPH{0vGtb?RM5+TM2U_TpAd& ztjfekGt_t9kg)(9$J1EUIj;&_{>0r_^4PUMDbMS1DKm#T4<|llkuDUUIrdCrY_UFL ze&|3UDR@E_?d;{$v3)wjSD3Z`T97WK*iT<39Nbw6aQ4TS_n+UZLBV=8>53W~Kpohy zvi3H>rdy^2lFDA>5_BJYqX=U2P*{h1HDV*&co;Wje_4U(lY2kW)`aVEDCSZCg5;iRSOeF5_F{f{h#klVf$xFE)TCV1nF2T^#@H1ffJ9LE!^6G7`@aA*55Kw`lhtk) zc%tmMLF*EIfBiT0n2W*q5P~l%>;9|)49r)fT!7{B#3x|Cp|wcs5MrM1T!bMGGs!MO zONmFTrlrkPNNQ|vJqTlu?kwgx)aF0}3LRp?PPLEKEhMPY>$kl1>VpgiSM&ajW8x;# zA7L#yIGS*&u!haEpk^f6!^f2cVAeMBEA$nsk5=%a#hfZg)qux3lp2Us-Em`6uU9Z! z+6xQZN%i4)_M?)i1!m}ET?FwY{Oh?KYnq+@eKjTS=M_Jf5_^g0yhZK^!E%j#61E9b zcXs_(BYiQpb&c~eQX)uG0`R~~x22cD`+o6=B$u0-Q$h%Hs&mJwYmM2hQpDr@jGEhd z+sg5t#0;JkKxAwEN;?;dHfF#LZ1vsjrvTNDYlB1UbBeZD$4KhZBeBOFS6Z!)lgDe_ z_WQKYJPl-`XEo;JDmauVUrMBSr1*qu9yE5{vIh`(9cP5~=Wwx-_-mAXUwOpAPY#R3 z`xo4iVejL$CxBk0`Y#S9@HbP%Bm>FMD}Qaj+MYA{(Tr%TYJVw^QH6|@=Z^+iXl!Ox zP+h~atTvDH&L;aAu#FA~DfbVf3RxgfBv)|_KF7$4DYAcAeIUX#3gESbnEu?O{K=); z5`yW@v%H5AUc8{}8uG;*6Nc)mRnXe7=o~Fda^{^k^OG6-iIu+Ei83s_R9Zr}0tcl3 z8UD4OPHfd;9*@l~Kf~A;^v^X*CIDT>CbwwaqhuN~E_B>m>7|BqyPc6}b(+$AFA$l% z|JJaFY%#*O_jlYMiJbdgVgXk>)cu)NU2wnIRef0V<)_4|P`1V!Sda!2L9c1fK(~?| z!)0;N3>`xX@-XeI<$?urg5daxWi|Rt{!YwV4h$oddigeHL2RH81rRS7_(vB6ydI z=>E3EX+@on4!44SmlCnS=y!0OOqRz1ubccGE}D@t8We0l`AeUK(e+i$61#p{vPf*# zSWw}(#i`YG*j5ota$&eh z+nbL2&8W;2S$p*;#>4pALLK=Ow>RUhKOv`hpBl%$<3skUX117vC&KTh#bM zy*pXr!$V_q1&c-vAorKYKrzpcL2K~x&0S&_&9ZIV zwr$(Cxym-yD%-Yg+qP}nRdY}Jon$7NOYU^VBI6$VerOcs@EE|!Iz1e z#p77GRG)dXVz(1-SGqWVyLWf}Q8~%QEO!s>M8_5Ma{WWIjvL3Qw*B8tlE3a1z6|)P1qN!cl&e9aMTbISgh^ zvroR8y+hFK{2aA<^xm6xt`e(_h~E$6tJ8(EWZwsf z70>N5vZZjqq3Jg$TP>$J_Qd`CrxFOLiwJe2PqhfEc($=8O)kaKRLWgL?TXm6FyBw# zk3IWKYhw-+!lF*QqBRk9rEckp8?m~`WP12R&8~u;*-&}HSqmgX_I3h3=IK*O`j+qQ zaanGZYJpK=<1W1K5rgIVwk<+03XG4ut%C)BzFEAg4n*M$Js*90{~mWOrZx6mr8Ei< zU)fy~ciRaprL(-iDV&m8HGE1`1|~{>?@1AO+trWw#SoW%U6pJd?q*tDUz|K+2;S^Y zMKls-_a-}FxG$h-YT7CVRTf`#RiuIK&nSQ8$|NwS9sn{0VzjjcdO7ko0T&6UJG+Q7 zNx%{7u^yDXSB8?63i-xv?eb#8!pGGrl<_SmF=u z4iYKg)vLF#%k-Cvsq9p)wv&sEZxZQKJ;+Xqy&hKLa&cH3#|_)*8c(>i$2VRlkA8*k82zi&lTp>MvUTMXSGP^%t%FqSart`ioZo544J6ryH3t?)t3R zSSEqI3k2N#VR{_yI0e;gPy8}Wf6@*nC(H1B%jnamp!(iHp}lQ1w}f6Vokxc&nB*D#={| z|4bYcNUegEwxZkMHe3dsEG=Fyh>#0f6aNQKQmw~)=YOHqB^g%POTiR75$jpp@Og6c zm_2n=+rDQ~CB6sue*`-x*G=yoHG^i9H5bZC2EU^KqEY9v|vd zp#iJta^evxsQ~(oFKgXEA&7qH5xa7zz|!w4qASBJ=!9)OwL(;L6mE86EG^g2ms{f! zh1F}uIKTCa-PVF{R0JKbU44!`ZLLBlPEHr#j(IXSHn(RY-SXH%wA=4?uml=&gjT3T;%E`Kx$H0oJX342PLrSeR3IlfMJuujSKwI+UB)K+_z~hk zsK&HG@oX&*S6-y>9k|Un68Iz9FexQWlsdwrH-5L>LjclhpR3z`-RnH<7l()!pI|Ch z!++qs#5#fN{ht=x{I;2`x|y$|iEyU2ePVqwM#|LY-n?%jLr- zw*xKwt;nNkEa5}IJ7CM@8DfW^9HJ z_QF+Q;aSDoM8(Df`T~V<@RPOJFY;tx;MP>;H*+KFaNy4)Ea1X^nx|HnasCmR)0$Lh|?ZY zmcVV+<_J?8;@H>AERHe3XfuR!9iB5(Y@a}*R_vhmSc6e#f_StPnQI9UEz3Y~OKbh2=mtyU?9nn_ z;Q}CCqf{b$h`|v??p^8NAi~U3comil={JbmD^9y{0nTADuCf3|cJCs9(&wdsC-(2a zNe3QbT$na|0#A6+Nt9yHb!Xd;sO_d3+H%!v$#Dc#`0ceubBEwa0)#e2e&)%^f2t*X zd!XPL6hvOQfjZ=-jUVlTx!2zQ0g14)fonz#=pgvN)S zDy^QC{eZd=d4qX04&DfJyFNkc^LYpbU0lWk;!<=eHwrXTjfPBzpTJ-~Ff7i9z=(JRSmYS*{GFWM~cc zk(@nN3Lv&OyV-rbxg~Dn-f7I>Nu~_oFQu8SYX%naJ@QUq_y2)d$t$W%t2=tswFuL; za#fYYPdVbC7GVMP0;lhp=q7i^?-OFF5)nC5?I2@vSade$Amc+%zPCETAhtnjT_&f(DwM?E1KL zr*kTeCzI)!6gs%;7I5BVG?49rBsY6jg#n85pbv)Qm-Q-Y^TJ#){bl;L&RC3X`Nv6L zRZaeIx4BpGM8#YQ+t0}vk?G_9?x{Q+QeHg&XG%SG6>mCGDZ_3<-P&wb#if*@t+`13 z90kU}ALJYyjDno?a^x5$8*9ulhv7|*ez;iL3R}Gq!w=xrF3Tcw!=N|FWMd%z@`j|` z^mJt}d#X_{+ZfYc%g^I==*GA~6U-`|P*67+5$6v+N0U=bhkly95J^H{VkaT+iAn2p zuh^X#M3uwioxGjG5S7&l`H02YZ8z_~KG3hbO&bWBMM4t2Wn8K1!)|kPV`=b52hTQF z5cGhk)n-v=_^(vM*1duI90nlDMn%Izf!I83H}TlibKHIy#Vc9bUBvYX0&HY^i<=eE zIYaz_8Y!2CYTKYd;*Ai}{b1{~a$!w(u+eUi#5n8yN5U@zCoTTqZ%M@3TolIT+`^Km z($pNP8Rkl_+lFR_sd3G2`Mq`H5V}= z!>lEAt{sTXZCgdI1bb^`eM;_=qGf=GAyZmDJEb-0PI}j$c_=P<8;3S*-|Jz3d z@D5+2Q)?IIQxaBJS@P)HG|As>ibTThu2DaI2qbnOTEFBRT9-MQ3K5Idu1b8;g{CYg z3$!lptNMJ&S@CW`s;~3O>HinAI*F1!NA0s3x%uO;IC+BR6#(lLw@duEUbN^3m_-m2 z5y9#!^U=q^(_ME!{8x0c(~s6#>ApT+FO`f^yZs1LBoOh-cUoOxo~>nP8JCQ^?}o!J zvO;7TIf$4MO_!|Xr+#VuUblkO&Zv{hbB_e=e4$1)I9oz+TZ<>Qa*|1I5yzPEj1vr! z0)7p7a|3hm(TjQSGqlIm9B#r*Zg67;PF7CkteL!AekbOW1)}qPkEeLWuugiP+grUY z!KwP{UkU_%sx=>bkULSjLP%tA=IGG-aKdj_0}7;6QKq#q?xxw+FrrU~~k~ z>^-a#QvSAhIz=}*p*uwXn1*}Rdtbnbz2CPO_h7S3eS6A{_N1F`M7*6RoyiT*-u6Pr zPm2Jk^q^|h=Ct}OpK@-TUH?Z*9zGRN6E5Ce;)E4*`cGIiB+BinG;0G`ld0)62nEKH zURa<;O=wj{i`9$TTy4XNmCo(H@^(guV*jz#up00AAtNJ~D(5&=-cIIMrPQ1tY0&6CR%il~D=t9e2(_)r| zhx#5|pA^sg9)B#CHNv_n`xh~Zp?AxF^f6zW6f8v&p-Kr~9}n+mYxW8P&8N$pk5X$1 zs^ccN)vwEay3CMueZEmosp2>qi!<2VgK~vVOB3IXCoIl{VnI6P@NSrvL8mv1Cmbrx z;n-Vpeo5;uY5gUwzohk-wEmLTU()(ZT7OCFFKPY1mbA7>T>5j=XVofpK_|Y^2TgF< zZ~sLpLk}Sn&_*J|dxKuta2oiuHJ8sKI`3HHFHgFC%ZvZI@8G}8`#vavnGhnflr;aZ z0d70b317XdM^X<9vKoc0d*0JA5Nn}xAl09}7|(VWOKrmaDfDB9LyZkCmJxAf?srMJ z!;;LOaeen9V5lJo!9wEC*wb1hOG2IJh# zX6A#ZXur)jI+0#Y$T%i%5vli}OP;!?^^5Vw7a0KxqW3UMnB~;L(G1kg1SXT{nwlecJ|1R3?KmFTT2# zyv}z|v>@7o^_gNt|Cf6&I65Y7->+^>`y(aFl5rOC9Fm#Ta<#f}UKX4!_}|!iQW#P3 z-2;QA1COk@pjG72G3HCmb&p7~kp2e_qWjbGM76Q0$01gHo0_QGJKuqROR&OpQRI2g zeaK~uGq)E6`=k|`ai6}-F)iZzlboT!gRWB%`2%7>Uq%@q-;yqdaU$6mpa%mwOJvZJ zS%8eyHkx;{#?wVdgOCtznCIEu?O;U@Rvuq4T?gV*E#U)FY8w{n%B8}wvB+c1e|gqE ztkanAmfN(nzf2wW)^|kR;6yaCUaB32{8|f^KgrqQwl}QCLa;o%TzVfZZfh%j(Uvusb1+Mvzziaw6)*-tcG_dh)J78>g?Ku@hbUK6fVBgv?+HA>=h?{AKWoP%17f64+Ek zbzvkYfSs`EiFIV^Z~mhN>D!BAEO+aKut3n8epO3i}LwMf>YOAUCTVM6#TBxJP2Er#D}H{ zlJpIf>C>R=w%1SA77B4XQF|w9htH?QE}>Hswi~>;mb-r89iPY*jH3Q2>taH_&ypnX zk((#mlmO%^%8OKrz^7D=yaulS{A*f&)2zQ~*55SiZ<_Tt&H9^W{Y|s}rdj`=NwfaX zrZqn)yS7iv)XFpj-|c8?#dM9G@ z$*w|v6)K3|)`MH^pGOYRF@8O{Ev;kuD$kD6Pkrt>l$f3jh^UYBuKe~GT5!rAiK?|n z1$4*v|FfVqLQtq!;>QqVbmET{?cCcyLTtdoK5O^gWx%QXf(n&ehnYF`_-VINbmt`< zhNi?VSA#awv}#Rpb{5s3#H)pL9%KIa;$6rwA?{@^Q|gHg+#0&57$6rS*B|H1k3yl^ zU64c^YyXU|>ceA6NYmZo#cDq3p!k^h*G=GISu5F2KAYbt*MRU$33LrqRl? z5j&%vi_$IAg0J2Zxpuw56%O~soXC}(FyXAX4J-e{$<=s8kbN%u%k9%k(0S9c(b84Q ziB0}R4y5VpK@UkEjiNx#Z}O9oej)`kS6!5}TP(5u-aAqCQGCp-Sxh3FCyfDHg~VJ6 z!zgx)MItYAnyCq9`2P=Qwbv9WnRjhbq#~utK#my%vV$WaTTsL}&n{OdvPXu8ppzz| zT|z7d(jp6Lb8so61IHgua1QbjiK_!M`a;nP^+xVI9q}VenyWac#12JTXqsbN#_pq4oXH0<_ctx*RPyUyV>&kn&Itke)BMyV+BnSH<#jt73p%{k@=iflPrIrz}1KXjH^%I{brDrs8O5d~GEu49p|kCUc(98D$%@ zrp{I81K#X5#+(L~Yqn)W5~GR`?7pK`VNOb=&-P5jn=(TW&8DVo>A<=_{|v~_SO}sB zD3*$KmxX`>nT$EVLfb!N>E!nht|sUC8De_(inR96ciJR1OHYqEXCQ+NX6n)N{Y_7! z%;FmG=q?qDbM4zHZp@SJO1^tyVYoA*)n@EUv8@D!GYr?C!PnJjYr@K&0qWXA=h9zW zq`^Z4K4>`(x=}md=WT0gt-cG>Br_U*<>Dhe9b3dqts;FZw&qJRb=bqeu5{%al$grG z$}TYZ?0&o~8R^iJfc5IQ4^~_bvyV^y&$F6-yVgqjK_mR&7OmXYCw-)&%dtGWF#3!T^%+SfR zy?N|zhSQbSwE$Cr{BKL@vLyLzwmZi_FdslqX7YM%SQ78hlG1`79 zact&Ofa$R17R5#vxKeO>h#xgc@Ao*r7Ogjk+2tR+LZ)`f8rGIupHl`fEzO(Y+N)p; z(v-!0i|@Pwp!({kB^6U81VvW+P{^1ms}x$wsfjm4IPAe*>OyLJYuI3$4fwRWSAIgS zlx-cH+s%ge@7auOm%1<*7fpJ3f7Dt!pr5ewPEu@lOwoimGmE)6VpUUe;;AF z?;;j&Ahb%`ox#7D=wcrNDEUH7&Ra*Mfv_jNw_*`oFHDp(gVgJ-oy4ppB1n>JfuBIh zf41G2D-$=_5rsi<5L&UZn9Ir>FXP?r;h&Ri7aal+%GjL;u$P^W; z96}3UQl#hBku>5Kc!v~ddhj*=*&KSkz>6oHgbu@*?hYvd=ers}kO|8nv0{dy{XiLV zK&9r#zv^tKx!*m3HO>*TE42M11k|CeUI#I)SJMCXI=gqH8u1GdFt=xJ4E^$ktc!Jg zQ=OruL2@lp&77R={_(p7^2?NC6aDYiq?gn8;tb+$nMd{N4{ewN(|s!N)~^2bxbzRm zbACqJ7_Yhm4kh>fL~t^aWc_I0?6^FJX?|A~ z+4YdP{DleGpF?4~9<0uTcaoGdM7(_J|N6p8L#8dyJ4}g5n2GCVrBp{fYE+VYJSrc{ z61lmMrPCk_b#ffhb6-6bihrycJFvb2~Tve~(Ed8Muv@E_X(seJ|RO zk{G6A6Oi*2C6qL9+L%;YWV2XBJ>@=EHCLd1y^HhEbWuisO5 zkLIP9{ISBc?GC8^SflmD$R!zaryhyO#ri6VZ6{;057&PEQF1fB+DbU;KV6MpZRwd> zdQ5cr1dIVGs>xun{WJ>dD(AKK))FG`F@E#C79I9Qb$q-QZuPt!_hx>-^^MEv~0%nhbxrw!8+mErT`#q|s&0AG2ooFWd>``FGg@zr?kMR+mA%uw6#IT9g zT-M=PXP$n*-f=9bl*1+I*tT@tRrtK#mh@I8IF+vQFHb_8Q~=$B$*fGcy5|h()kjc{ zD&UyjHOd$#6RG(nfBEFk#5-jZ2(uP5N1)wPYA5|#48xgHcTB{6TEXVxOtBuaL~R#EMZ>>gXs&#uq#lp4}#<4ku+K(X6&>CrOgzfUUY* zCN;U)Xpq*n+AuKUy$%U?s@uJM!P1+61ytiFILEX>?U1~FEXG_Chuas?ZsW6o*{!jR z11`8>Ud=`p^Ne&47C5opI50RnwAJbhoIaS7O3AV&2&uPADgUU|Wn1QX4LcP^nOD!N z^%*h2sHI`eiL9(E1&X^TgrYf7-QYRl;U~pYIA%bt3yci3!-#P=P-3hMd0k-JD8nh< z*fu+Zd~O9(A5FS1n6NR1y-ubkSf~HwimKNThAnt}&{P}8QztDqqRs(qnKgM;%&0wC z2bg=rtflGmr=WXAIA>K;*5jMJ8Rh7Ux}Lxf^tz`dW3GQ0y7oO(6kkB7WCdjA%`|!x^6M zO~2};hA0yrCZ`_1jYSn$m%SV}n%u;_mbU_G#^mWdVSU|knWYMzLQ)P~(}W^%Q$NXz zr)r+7g?KQvWh?uNi!9tIzL^#8Ly<%QPawm~v4SSnbV;4RBW3IyWzsPi@WUUr4QFMF zDH>Thgnq}SXR?=VNS9is5q9`UMOtah9mM7QBHtF2tAxn^5*)xt=tfTEcP zLmUHN{ZdVFf6+1yp!rWqelXVML4Zw&(b0RX&g~8awt?n&%s9?V97PO#BWp)EDpwTM3gfo-wC6UVE!MqI&D0PKE!uttJPJq6+~iJHKT|?Hc?QIEI6DEBB{M>O*-LHvdE6 z2N+a!p`~%XVsiQD*SlnWnUA?ts!|U%Pcl{b;&}Ps#gM&J&(M*}>dWaOe~3o}4p-`A z69nLV*CFB!21bO`Swh(FQ|R2yaG`Q5+w&qcLGt4me#=H{AonDU+l)TPl|cYM-%347 zl-f_Y$*KYMA<*~9ENi77bNyMW{@C}2n!NihYW)2B_%CYfl-om};lYSnjDx`wwWGPr=1ZqwgmT{;q}}Gu2nc&TX;kCpGUU2K>64Py5a%@=bH#H#Wad z?v;I`u)e~|RU^+=4S!SVHz6S3wQQs;}XsKL`f_Mi^DNE?fBdY#(0Xz zx2Xxs;&LvIG!!S#d%7tFA!_95iyjc@R>4eHO#a><5B!JWDQP8Jf&Efa3(ajOo*{I* z+ur=rd`T&a4~@ZOmc|J&Y9&@~?Y^$Bk-@M=fn@J+4rioveY{IeaP(WhzwpMuT|rR)F;EHPNrnSAuSw!>8sfFGF4A&=jCl~D2jEqMaZlWa)?$Qo#UTz zyRUyL5(|wbgvoH%wTP+PI|{#tzVHGtl*# zX~v=@)uUjpM1v$ot&S`f7*N6aUug15_GsvZWvc=m4r*Z(9Ya?ie^jal!(yG#s+NE9 z*&7#@$4`4-0Cc+(BU8V0kPde}hvG^A)gI|K0n zsAOV()t2No3@k{7Vi~PPT29RM*(0c9T(J?)zd8M_@);Ua4Up@CA?JnA4h${63ilm9 zFq+Dee$iuXNJ(lrxPElj4(^?OAjIf!B=YA_YK3RTsje)+>enSM+r^>iw;>rPI5qxC z>94XPyC6!L%3L$FSjgwAFIo}W%Ig-8$eFBH7cw0`BfqyScnDLs)NnTs-e0FVEhP7* zwKo5QzEO&f8>Vqngu68mJ7$qOcFz~wyiwm6n=p)V0>J1)MQg9N>25};p1ynTI`4-$ zBgyR1ef#-On2%Abd2GX@PO`eMz`V?V(J_BvHsrm(Uvxd|?@{wi`^Qxi1!OL>{)V~s z!#v=COrTCTaD@%gUq~DgsEf2qIu+_n09f*o@(|H1 z=s@A>?N@_h2v`k`PZ#;P3K$92RXLvi`^msDka$_id`n5B8Rgo4E83gRR3x^RQj(80 zPw>-*-U z$?DxTdcei=XrfN-y8Ha$k530H7bIm}_`tG+F#5s*YCG!V-jq={4?quKItu4$t3%Pa zyQSI**;$|GfK^>{kfrMp{ABR!F z`Z7OaD(bp{1!??XX+c6zv8peyJj5bg668>u8i!^q8rYycxVWNBOc%OlugW)KeSejVpoSq#X5FNT>;Cs%4vua1(KT5`SUa&>2kO*ydQ@xrBiz1y6 ztz}=Q*2&Z8UzXNHUaP+ky6sU!`tlpq0>A5}ngVZ#j+bp|&4AmSXqvbu0GoSKzj#q# zJ$T1UVGPAkX2JOHF~Y*gngril3&dHJ0OT7+eUtrH=#|^1uk56VS@-dYy{ZG^MN0JV z;!ui}3!){rZS~GEwPu~FNj3UENXQahip(3m-!WfH{M zCA)%QV6_&km%y%MFs<=LL3U6+nqjURkc!9y8;jKfK?-Z+@xzXGI=IteZ$$#8)aLed zC+4-U=zu9-RwSEkQ#Q3LFLq}K)~IiY$811|lsVI*Dn|Yu_RvSWF0NW0|2rUwkXJLW zdlp+#So8mO=g9~TtGt=5rc#629w8E`-4q`92R7sy3{zN&9--z+`=glE&=2WlpyvLm znd=TmjH_y5hL#jvZ!P?m;Fhm#p6|r(^9G`(_UCJr{F9ob_=cXBfA(e1XV$=P*Vyz6 ziHg3b+q|52VZCLD=+QFnN7L8hMgwN|8VG*ox*YVA?yJ}(+7kciD`U9Y(PA1_q}%8i zY~@RQu^)oSI+9h!l{QL_OD7jxzxX^M7pzHVMCbg`0{n@`pcu}Xqzipu1Qw=>o7Jj0 zcGJ=~09`r-D(ur@d&xViMdIW^$MnbNKw1rZcKNgxpqgiCX|1|clu+)QgIc)cR?;Oa z{gqnjU?MR|ESa&M{HP0VUvveb(nl(_2#JKLec000^?(;C0Kll`pE7hvf6BxPb6U*0 z(^65bdjQZaoHi6uC<6og_q-!xv{eX-c})F_A}n*A8xa{xnX*)=tY5FGQ!Ipg5rJk)454x`K7oKUL#gE6z={$+E04%?XJfzmnW}d>erPCN6 zPJ!prX`GMv;HqS>A;?>*z)ck`?6;iQy1BFv^ggg@UujxZIn$`jV5W&mPllXJ_jJ!C zL#+_}Sf8>k;r$bmIF#$3Ls?=~3`>gP9mLVwT zhHT;1yMYgDU7X}ES&E1X_+j!i?JzUnNIDhpKYSC%{_umwRV5v zh*^=bcnl5qcdwS!Wj#*GnQ|6cKfE$6RghK9vBJ^W6dPTB%*%GnGG-Y56_iOFYpR7C zT*~FCN-kV}5&0{TUXj|GXMT@Xa2R7%yq*o8m5Sr{W63CXI$hM-1=(NO8ldv#^;b}^ zI?PrDW7gBtyR>B$3uG=7l77-Zz^I+#G#j!~imDg-vj(x3u1aJ}2$?0a>E^W|XRat| z1S{m>@Ul+L(9IgW$xZNnLI_Aye`q?eBgxnEUVWt5bie(cs85&4jiBcvJC8kCMp2sUD#A^p-9!fb7D_4LN%@PA|L2Y4Zu(P}7yot4;^)>Ss{8*V?C;ihQ5 z(a`Mg6rS7)r4_=}0P|~u;I7|HEv{LxP(FMw<2g4YwV)hedP*xd#?nxO4Mz~QVihqa za2)ed)7GUT(dJBTZcnDppq7ZCr`mxt&!}h$IC?OEGrmw@N4jUY0AI&T7 z0y4B~lmv)gO&D$^jqansRxC=HoLdluNgWQ#frl;|zPb&sT`-Zws65*DPMK0$;2G@ zrBE#CjjveT!iQW|4V1DDgH=mjiY}&Yjyy}(7~=Q@#~;ioJqtfy>ud~WW11z@WwKD^ zsjz%a|LF7b3$B1!r=A#eJ=TmsdBJOD7d}X@CUh^Q^&Orj%lQ5Hy&C8^L$z$r0;n;# z?iI@eWYt4gu?5UGc}~EhRQA%YL&mV59o3ziat}mgr={B}LoxE*p)1g4xiHqE(AIJJ zOIP+UZ;bX84|qGXgTQ*Hh1t5eddQ0(ck5#bTYZ%eq~NsehvpMRnlU#F3`<-_+7ZWdbFY@j9lzd(&Dd1Wu5J%TY_aqP* z+?s7FPD_sZd(lfHE5?(m;vlC1Ek5AKTzq+gDW`9smeM?+>zrQ*##}9Wr zEBc&Y)ov+8mslyLevRnC`kgjnlGugPUdT@iG2D$X4u5^t`)l#VBB!b1ZkxQTfa31hJM z>Hu99evnj7d!EoKiY>A(-~GvY{395sB-FEm4YtWjSIdV z5=)4;PftvxI~GWHrKdu7$3*5`gy)L=?Y@9z3vGLsM=>J$We^g8uTbEFw^FvmIV3D@ zsv9f~3D!-%!?^dqyJlLd;aSC^bxAV2tgTfIh0FmvZQwt;zHkER_m*??V4ljQbN&@^ zv|tJ=0}IS9gxbd2C`Om4>wcdZPtc|>6W4HbDdO=K?uZw%s$9J z1G+SJu?o_3@XeS1z(eDHl}(_>rsA#h5QC=)>IKwXE&GF0F&p+b-$C$C_OA zPtxgHI2%^c&^uqJA(XhPZIf3!g|`&M|H^WTn3BHbPfY9{bsW05593}OW6v;&LAxge z<)1Vq_0sPbGjf3cTTgOJ=PW}eSJ`{%mVpy_C3ZP*d&LUe>Ky0S6|1`C=DAZmlggw2 z6H?t);3T=`_~0RTX-J~pyh&4h1(>X~q=RVJ#Aq=5*-qaV8DrbFNa>$w-kb{7LK~&mob5nd13u%wV8NmZdd}84qNudz)LPpN}X~l3ZV! z$Q3>BXkr_$3aG*DkExqj>LSJkE#69I>(&R!xcW}>=BcsGn|}Em!TvIJX>|i2^r5y9 zL#T?xwC`g%_QwU0!U)}|1_i29bW~#B^3-u19;eAI>;GbfkzpXM?_YrsP`(_k(HIE_ z$P4%=l}SeL@-fNT^mUiQ3Ln9Oqn91u+FULjIj*)ST{;id!SJPNW*#x)V241D!vt3) z%;IcJ|C#F>*#?D%d4sOBc0okbIjH`VV)O(R$7Gzn=17m_#d76B3foaILIDdjxO?G& z)`*9|>V}}y`q4Jgo|jRb(vJz5htw9xdF(TY^)~Rwtj7)6>*^#<5eIJPB3^FLV?iVY z0?8ZY|FGOQXq!Op6O1dpEg;XN*cHcC{%fwbJ;tm&c}KrLstoIXJKpNfa`Cp;^Tit9 zNt2s3A{IXjhxtNd)fx3yCI6*#n0$nMDs?CxN*Gu7G*)aW(}o9@$LM*687J1M59iJ0 zJi6@~rxaGi*`dJBn_F0Y0yZwb3FNgCL*4QteW69jB;zEX5{?BkXx>lfWNMBAzF7hw zm-BYLrqI=@d*n{Vzqs4YPsA`r*!|?ynPKkw{V!kvt8ZYWu^0~di8hn>;4Zk(SL+U^ z5M-`d-B6b3vp%F;5GUtycalc2CLmQ%tfrL9OUw96_sk)3`9F6Tu1FfTypK$-un>`8 zKfMyLPzT(Q1eW)Fy@p2h>u0&85XS@r^Zv4G2XvWFULp9dLP64aP%t@icysk|B>L*M zAAEi}d#7|ald<=oz)Jd|pxv1%JAk%3uBO!VQ`bRTrc_^T^u2ZzQ5M2@#M5yI`bVWl zDz-zD0K?U@Ks^3hbgBpomxzK55DdRr49hmQ^p=MfuKxy)$^6O~h-S7G2VXJDw@Wxv zjthRwVnI0qklHc#iWZdiSP06*mr@R$8in6fS9+}at-3sb+K*Gf!#N5M@Tq)_spT&Fy6(%6R|c40b*YF ztSWA6zaRbk4(Y*iXa{AhOup*$-c8AVCV27t8hH5B87ZCO0TA?GFMS5zCIU~c$kIS_qxVLKsipuF&pG$O|o1GfcIc=SFt z;g?mm+o6OR02q;Udjg!&mx~`&9+9O%b2wV6R(t%A7_x)|C@2(rY6d2;p;u63PS_y!uPD)!n~c@U1)aAQ3FvoB${UgYC?j zJf8}nJv$^DtOZsel}Efv$&cB;gxzJ1vH8O?amgKY*9@$%BNE0C+|Q=KEN)QdNi#gw z2_C#5JC*Gs+LH`dr5$YMcDMIncIrm|m-8MmhSn8>y^Z?w+=7+qP{R)5f%I+qP}(+`Aip#Q(4n`*0(6W49xp>a8-eGOM!A$?ss=cuqmC zss&jAa>@bc0{Vg56#@O33WmJ8*v}$8?Ff%rH=0E+ABs!S%X}P%T`5Ai*dt1bEZH)P zKLh5NlDP=-DFw4mgH|2jiM(Q8!o23nxuaKs{%pz(F}~@ow#A?I_kZY5%>C;>s#`xC zK$L;ngTt;?0rU=Qu3<1(#f-DOc&rV9=4~FLAwehK=n7WR<+XBYar#aBmPnW8sYf#= zxy>J3H8iHTnfVbMqYL3pZsncd z8eQSdNzXxCIAS&>XH^p;o1v;oB|zRy@Rx&F2Iz2lf1aYp`c0O{K~lYix$fmEd7Pc4 zfMm!DVv;{rU#Wk~Tw@~72u7TrJEChQ^#dndpqHNORuCio`6Y@{bfgTNCVxU{Z%$OR zkpX`)HJbQ3;Z=&9ojZ_~%s)`0>yn;7 zOe*_Iyn+ABMs6)ONlzMS-V{WEP!BM^t5DBhCmyrCS#~J#BvGRXeex%Xsy*>0s3-!pO!T&VlRJO-gBilTCAE%XC!%r)AEXj?wmJ659`w zZkQu;*p!A;?4^v zEo-fpIx#)5i1byk3FDUxD(vP2LI3EIBJ+cf1%$zW@iC*YD)y6$N0%|F*qD>d#xQ0B z8t+}&W6<>)(0Tsa%^&;JnIP$X%G&xf72==i_!ZX1_fei%fLzrLtkQll*T)rhtkvg& znX)ZvuG(u{#2-;S5~ zVE|L*yp6hLo4DCV#Tf+ zpc&;LgUq*(GE=`WKL7v#3r3{*X-!4UV9-F>zf0&@yp3wUd&gu0bIlFLDU+rh%F{Cc z?*N^4)J%arfmC4Alq}G%!IC9xUcog9w1=4Pord2WG0eh(09u7ZG){t_{z2-@^m4v% zJlmd;Gk@&l9PtI|XIvEVC23Ylcu2Oe>MBgZ8FUd=#9Y_`;W6=9_RoT%sz##b2|(6u zr9%$F-_9m9`u`HFS!Fg}oovQ{P%##{ATUTXGFNhi9X{VW&&o@-tf?ssyGfnsL=0zw z=cA>j6fziMNfKR?RGSFhl0(nig8gHiZdSnD)nqs%I+e4TAqefa0i;2@eh8450U z#vD&U#WNZU}Rf|9_)3q9Ji7qIn|)y)VO z-rPOw{LX0(4v+3jG*W59)bI!R zb|?!g0Fr~fE&bA|Xo5ch$^bSII>6oVt!pHI9`4_Q?0xj$W5iCxPCK;$bq@#k-l+Wc z)Sh}TmzjZ(@V?t>aP}xyh{=>7@q!tyzPMmeqoAsnQag@bb`Hwr-O=9#4UKi6TFZj8 z6pIL$cCY7H_km&p5<9AlEch@0cRXPav94S|*n-uk_QKx`4Ir+@Cq_Nhj|>H=T=X?A zltwiHAh^N@w^2D%?EbT9(L~#6Lymq|B1&9d24B97Vl^N`7!_t{8HL$z#Zt`YyNd{5 zR=@ifi*V8Iujz5^*C}1pjbrs@Su}^f@}MZ);uv&B-qJca9zxHwCD;bhI#k8+xz_OP zlA98!5G=ZFzcz+fb|xShp{p6^u*IdO+oluBxfvy1AOgAOu7gHRe!=he2SBNL(F$JG zLDT9QY-Hcpu**kAfC$j5_iw)_nV^xR%*VIIUFoymC^Wy;A;2irYX)W4DRyzY;c}w; zsJ4ukb%V*Vzv%d*h{JohzTx+feBq@BtDBGCs~X8Xf;i+Qo;P+lRK@XX;ZiITXVR7F z-B=YrS4j%r+P;3(i@XDL?DueV?KNqTE`0nQ6T!txJk7XTn({kkW2a@r?ASeii>50E zG%|aNQL1D+0Tlw$H2ILI@(4V0zg(%{-42b6hfPjb+qP2b=>Sis1g0BibSE?(ABcJo9)L&BIIi&W!YY0CeQ z3RNf{*zd@^1G|^&KqPD>R%U(v`EF2RS?Z1Q%7eHulj|9UwD@IioHar-j*2eVxF zKwaVs9j1kgpu;807Z(09n5+i`FE5Ft^BjxV@$Y`Y=i4Isqya$9USzspfoYpRO`i(` z)KD5UHVD)5HADJ?pr%w>6L8o;S4-LJaqgNyp7OVAY5HELWwtgT_&d?|BTf}n=~%Um zD5Q>e{7Bo>7@;039#I*Dx66ysi}`}3lMGN05VEt(-`ZfpNc0{ZP5zq&m%2=eyXG*e zkd108;7hb{0~;AB(mX!^{3Fkzbwuk=!<|Zs7@%nM<)Lmwmo$EQIlej)>CJcel_b1w z^!_Usd^Uwy!kdy;c{6k*_<#;|TvuWWVYgcK5waHn^-BAzNU z@P)^@{2@bnZg0v|MCE-{zP<8$SBUB;eUcDxxc!zG9};q8+4;QZ&~y{knwa#eNwx1C z@1aY+M!apVkL`5c=_j_Z*Tnv0_r(qoIqVzdkf@HhTk(DGkF(E`@o%fusH2sOoSj&g z5n-$nr9{8Gxy!wjA8=0Nd>!<{!bv;RXrbfPujodb;bhQhXy&^`?z%oM8)e3NSUKks zu~-&lXyl)sKluU}ns2kxL8WP_3}*KaH`rufWK5%9haIX|aKKeI}8|Zf3X*<%!K09aRX|&offPU z>s&LlKEN=csD@^%5_tfLI)wlF(AB;nht4+SBrOtAYG5|A!5#Yo&j=fj{_lu5385MK{Cp&bk~XrQkf`-NnX zUWhmqQ@|Ozn>DR}hdKVlvHj~ndY^hP3HiVf35EBM)qQ(IC305|wRd*hGAF_XE|w8` zo}}@t2(ygXu}%A4n5Saf{sZes=i#zD!mX$`x9;{Id7@^mWBU0VH|J*tAc(HOUK_@~ z)mKQIiY$Wz<$^j^u;oDnjO3xATfe-uYCeKkO8edz&LOkOIf0&$*a z7Ll>X!eRnCHE4hwvr2U+e(b1f!v&zu2mW5=0W0T5ifTokS_h92vk}WJV-m=jB7RLd z$=-7XF0}mSbm?YWO~p((t<-v_$Z~pQ>qqbpanzTuaXkH*>a38euTPp-==ZE3L z>9q9Ywyk0K-xOf%_0>9lK=b{3s-K-r@wg80a*kU?l>OiK>L?;IXD1FWn%Vlb?JZijA`EW)FuIea>xI7>9*$UahJE!nI&c z>pWw1n|$bf*{#j)r`t5qrMYPqgrTP^;6Tj?%E?kUU;kE>fV4p!f~UfD$brlvK*lxW z8*j6(^1-*$tf7kr8+G;-xeQHdfH7RoODud6nW~I<9T-VEHd5~kkggrH?RK-so5|U- zoak~*AZ0Tg^lGvNIF%zrbB!ZRfntm_UQ>#*6-Z`Q-Q>au%?A+=*{4?>da+bZCNDot z~Gd1*5sRfOV8R`oNV9)$oW$1nWD$)DGo12SZ10`Ex5cMqn4K#phH? zCm5D@)-AI>v(#$Te=fKk)^?|*>MD5ku|3my`9lZQk&g#t;+=vYg0wZSL)SR82zmP- zhlvNjBFC;?R$D#=#1vfcOf7vKy;Olsl;HuJUl$%9W)*D^mjL$5VAkcE@~o4fio~Uw zO{wZ6Qg`-@M+I;5z+LoI_LXxlX>|G+;QXPXYp_A+%l(|^6`CQd*Ihgm@?w=G8lfv} z8DjuGYKkDw%DC6b*Q~!(l&`^J&7S7`rl{z2^e>yRW@#T55O%nm)=UjMMBbJYh{OK5u5eFB3tqtp~0)jtm*GF5`lq$ zS?1YBF5le-*T8sIFNt^>-?ZtvctBdq+SJR|m3z{}tztpCZ{Kfk5HX7^;ST#e<1^$% zbZywyt^)V3Dt+Ya4B3R3mQUx)$2cwE1WeHvcsKtHVTvG!Qij&dP@+mXeCu>_0z6Yy zpoW#=Z{Opwk6c4<|8Tcb3jIBo$@F#6$$zw|K87G|k)&Txl&$In(lD{2 zSMQ^RqFa{mpP#MY9N3D01HciX!1o{Hs1ibZ{_1Hf?z|WHqd6YSo|$Z0b^ppA>e{e4 zWrRc$sYe15Zzkf@G%WK~aB@*nYPszN9E(RUgfu%+vx9iQw62PVfSmTMi~+CP$) zbxyp`jIKbSshU^cE3*ec4dI<1;G>$T6|~qhk;7*4k|Q?PV=!csKpQrlux?(*54%##2@Cr#HR%?zhvBdew=`HF5g5};_ zu?Vv?0bz}G(2@Gd;r04T9{Q%`%Vz-potE0m?`5WD!VDB3RM;&)uy}SAFxPUzSj=Ec zz2(%3$Yy0yq8cfg=$;6IZGPFx15zfH*kh8(QffJfbbOse+S(8m3mu7X=Z%|abbW!k zcWlKK3z(%n0d;yFen@KJ86%+IVMQ*$fp!8eAL+LUO-SqlJWPaQ1DnSMDToY=!*9>N zO+mKGy{ZDqv18(xFSzAypHF9Jsiag*Y&Y2rI~RY#|rjA0Z1oSw(OCxbUwo1qBetpj0w?TGNH- zW>R0no3uUwClD17bF8efF->*@geKLHW?&+q(%uVR{TE3~lzbdyNnBb#Y^-LpxcdVe zaj+n2)cuydQYl!MIZDbxj=piY7-N_tqPusDk7{M9;_GE5HoAG4Bj{j!gg?i70m_Yk>V%so#@lL(y z%JSE9oG$Db8?VBfe!H6-W+f4KKN7#KEW@KG9D(R|KgOXQA}QU$7L{RqYjCm(#MbZS@p`(xpc~mJy)k#;WUfyO(GPYEhW8Ib6znwUO-8(TQ@)+6&XG-L ztbKF(meHU|ufE8h`SXiBU(JC2c(?Q1S|#Pq-pRKGb1*j+(5{`sR<8U1GiCE{#o$SabsIrkcv| zCJH$#O=9@E^ZB85<4?||19}?>DC-D3W5w~|pr*R#>Ydk}99%e#7mEmsx^Ef!L9XZ< z$Kii(%e}QruYyT~Dm^6Z5%0xp>4%#@f*slYf9d=w-L31g7TW3xYtFRO$21we0^;VP}C@%x`;iuxb+uiS$1s*-XkB|CVS-)Qr{$l zZf6PEUHPZCDoo7@@eGcCjgZWd+Nujm#z->H2xM*Jans){m8jXi4s|P5Vu+);&$Z4@ z2+5|4k-HSR{b#^ix1t9*_9J0=(CW$@BS9ec33l@o6rjqtzn!g11+G_gE=(oZ!y>xJ zccz2e$d@7HOUqp?aLu24+kvZB-LPswllNUb&M#GAfsL&|apF2eyT^$(-gvTeo7Gv|u@rfFc4T|5bN>P?sY!Q! zpPpG=HRLL>AV^eL&f_a)U?+_l@-^0eVg%)uzV$b_y!ZuF5l9|Im9VN~1f?PM^6%So zsG1siF26YPdbX2DLr0&GYPvZCoI#m4xn?`LIMTf$bcB%Yt<`!Y^_f;g%2ow^CMA&o_Q#WHu~kdOq3wZ*xPV@6r^4Rsl<>%LDrVF)1&l zOC!xCE~|uJ7i&+mHcIN`UzU4Q%WVdDRh{_-U9rjGT!!FM3@$8Nyl9hE0|*+Q#OyPd5v}Fgn1~tP!GBC3 zL_?sV#09HBkF_;mMOM@sZpn^EW~=_5kR@EMCy5Y`Jn=SwX1yfgspC< z8w=aCAo$}g;O-X8CF5;30}G!eZYE<~BUe#(G=slHE(ErN!0r#dmh+$lXY}LB(1CIQ zWS~3rypERZOL9u*BgG6)yM{NM;szYQ{s7;p7ubysJ0Gzp^F~9_{Rmh`Z1nsvc=a{{mo3&Z@rL3J+vnLmu)YU`T%!VcX8Ygrt zw(8^Nukbm+sng!Q$K=2-F7{QtyXC&+e}m_;V(ySAc`;h$XRgH5n&7jEUh=tw+m{+g zE24J4^UO@Zw*q}LT<*0&!-RVn-o|nSeJKl-*aa$HOws7|cYa(Z$%3`DfGIR4Sx6 zX2*Y7mth$k_Zn!obSqy@(`x=C@7uRK_X7*Do;Lp*mz0qb^2+in;9o^&ns=(X`s=fj zCM$_UGGaX~R89Q!ICtQx28J01D2x$zDz9(-F{89mXhH;obRH1ku`A)#E2C@@H%vdx zZ(o$)8AW!M&djeX)D*900|%f#=3?B&je!Rb^B8^YX`4p_t=6ApQ6?>?BuP_8xQJVS zifmzs%95NmzxP5J*1YE8{rUONCc130F zDe+dO@NUCr;m(gI@p`A+puN8xy;o3deG7FN+uiqL9&^cUT#L~(>F)o9K*bsPGU-rp zhyTv3fcp7opYhd3f9(!JgAu{U|BjoycVApOZQB@J4A0g1hS=Etbpm)+7k-8;n=#@k zWW`pkyUJrX{aZ8y9!lck(bKJn*ezx6X`a-Afm>zM*PjbPD)le3g+k!)7JI=bxIjNdxRj zURFDrDQ{k;zACXE$m~k%?YE2}^souV!;t)gZ)8RA=D+oO10_W3yPC z`sJ1$nOCaLQJ`fn)^=D3q*UX5efIeazW)7$1I3FK3_L$C-ftRU&#wYi8)(u?6!2D9 zr;>&MmA6h2RwH4LJa($Sii8@`#6X_>rTvbr?}!!g6pASNM$??iU=?KwDoey=pR>nz zQBc6YmP36)iLMAQ9$}{7%vXN>(KeWeOqP2m84eJ z;)OUdNyF{VuXQ9+a3#;Zyo{SvU)W|-iFsezT|XB zcBU)YpJ?e;OCatqCJJ4HJ-ggr@+Z@(mzncA7ttbKSJm3JISzGCxp zs!1h77F3s5ufwEtQrK9go1lEOoll?4USC&!n+`$ipLK=*20>95fxI>KmxFN#!~uPfODvl=5JShfDSL4`6s_m!@|#p zdcAMOveQY!OT3Y>Jidec9YslOmLyfmSOa|b92tBZgSQAUe>T?#LL5!?)3ROMOA<@~ zae=m1c2}?>>c5HtK;L%d{wled+ge8<=g}SRP+XEbJ1q^0!e+9=LK!DV0JX^f@qoQY zH!-Vmd=<9@hfiv7Z@(wcQ=8Lzld>Dj6ovFANhS=)B}?CTohSJUFf6ckX+zqy{-sYh zPqdTGe&1C8cBjmjlf+s9UN(hlfKpuQf{R zIdoKUxQO8}*e|nOZ7pFWt~EcsHgM@K3=@IRer>INEPSRN3JolJ_rkSN;k6?OWZpU^+wz#`@X6rL18O0hn84w?w_@z1vl^_;=@Ggs*FsdYd~MK^lZjN+ zXAcVbHSu?Gr>D5%WfC47!ATLSPCJG36y2Gw&VN_J)NyeNHRc;yG^FOTNzH*WCk+`%H#k*>V ztWXE0-v0JjIW=^&nG#%0_+bBQUi6X@ry9f0WW9iQW_fVHk?^ZZt3YPhGUZ#B^Z2$L zqq*XBKbA!`IsNi(S-^wsSsANMt54a*os~5Vl!kugz)3PLdl9{YcA^|Mc$tK^zr^1L z(n#bFTN4A~Bml?-RX@zoSVp{L;M#>Sg_4@RN09E~+LOcA1IWE3a1|vCzfX-(F zRqSVBL-oWG+zdg8VK-s}Tdr64#f?J7^{|=}FE`IuK6UcmNt~1?zTCz7YXGpYg5bv& z%Vj`*TapVF%a93ip3XsgW&pNRoOp%}loQQTLLeGLBT?qb)1w;@&d6nK+*>gJnQJ){ zowSB}dXo^t%}pX-kBEMUc{LAI3qY5!_Mai(e+x!Y`?v4&H_*C?;OAjfo}zz&$P!=(LL_~4y69K=gCQmQw%bRBR1J1j=R<7-Z0=#eQNPuZ z{4_SiQZs{@H-ROgr_z$U`!7sKHWtR}^S;zaKpdm-L9M-~Rqet^ryd|R6{>U58Rc1o zDxmD{)O4+@c*O4;pU|RggBj@t1db=zvR1OB%MHJoG7E zdnRbj!;&M?ijN7WQ4OHxu3L!#QcYNI{*D-)>hGTV(d=;ga1J_wb>$gI%Jcx{7a})0 z7PK5ztxokuWo3!#@NbkYqUY_rx_j+b#7)BpD+_`}4Am3i4zJW*xRNPv^oo*b6sAt+ zi?WKH5MNnlxyd*CQRO{;qjg|I9n9nE*)s9DI?&yo8(yZsO7y$HQz%ZFBVUZ`meW|3latl=8ZQxd*6V6ivf21i*gpR{j{O>P&7B88+E z^cgZYZJXR{q+R;0bqPm)t~M22fZy}I9}Zpyhg!E~phQLEfcH~7Q1yD-fsrBy?0#r? z6+*pkLh`Ud+g`Kax$IQ}4~Uff1Kk^HT^7gOZ|3x-44cZ9Zfb|(VTV9@!EQOoIaVm8 z9{*#___Tx3d8?ar=R`P6Jr!%hiZI;XSr$gBr7Nr6pNum<)oE-A(yUTDlaL4%LWpk? zP*J0HCh`5R)*`%LoC80uuXpuy&EDQM==$@iZt8h_gF`={(Y^t7Z)$BD-MVJax{7Jv z&6>VYeSC_Pp0d8dy0rSVY2(`w8XP6z18hG}a>xNNVhEELX!+SFI?xd(L3F(~TGA9RO#y96Usf z0VR|Tkt_>+NW`miDaR7AS5}S`%b(?55Jyots-i0vL9u9!s2Rxvs(Xbx&>n2}yw(W2$6BQ;5t82f*Ku0a-wh zmZ#nUY4O@2WzS&x=rUdU@7^%!K}QDKR-k^fC8a`7d!-@C(g`xQ>Cq3)!}n()m6#kh z-Q&=Pv&{6jPM*|U-84mfjDo!Wyw2oR}=;1s>grz*5ykr*foFiSl%cBTuU~NneY@C zhL)O}U{o+&!hJRubmisDZG*8huFNIvcBLHIY}5xq$7-ayvp#aIlzdA&72+@TXlKJ` z{r)o`JAir*oe84rX4C$MWa$?-EOrPZfH3_S5oGkLtzgFHmtun=Vqq@6aCmQh0+gec zcF{iNZQJkEuM)3bVhr!a5@ zcOe=dfQvik;e?s0?xz_uoQRw$r5*O%D7%NkWEqrzM1%CHL_$%=*2k$RT|=$Dxl=yf ztI_EtAtH{DdLvNuy0wkj8tqN00|?bm1xBD%_`c3E&%$udyfvl8Gw2g`$m_?PG0Zc1 zyJpy2!^l1r6~i%wgLMxaaB05hG;|T9-DxCt9HCKJyI&U6QpFwLk~W(Edk-*u zX#=%(P<_5@^J43Y0^~)dWS*(H9L?wutUz)SuPeMI<E1#L|q>w&G&vzXbRz~eKqYHz4{u&J%Tt!x5%wBSbT|0w*oPN6V3s`WsG^1k3mg* zU0-haC$5c>>06L9s6IuW<~UjPW3v+y={ggqVk?59FTl$66ibBLQm2XcG$G|#a zTihgZa&d^o>Mc+>$wFSd3atn`(WefV)#KmSjq!&Fkxe9#5Ls+r^6UA!kV|I!WV1;l zh7bBy1;onuix1XY4o9mur7UkoLlP%P=OCiANMFmgRFeS?O)LE zhm-i>Bz`!FA5P+jllb8zemIFAPU44?_~9geIEf!l;)j#?;Us=IiT`sriE~{uB@<`C z1;O#`6b}@3lM|U?N5UawIe_*lC3&@;K=a#Xb9IN~+b1^xs|>~1BVCX?x^#2IN+xo>BsIT&Ly4K4s`J9Xsl z(%K}g4P#CC=yA&A{S3l+Y0Zu{H3MSV$bzyrWe%MD!xLMhfV)DLS?fdiOoMgexkbo5 z%Mm)UO_XG+5f;2{yq0ejZe3)!a^ce(xw6}ly8K4V7e+3Dv}YeHcwazAbGNp(iw*IMfGq$nCgIy} z^?ZrtY`9YVso(+T^niI`G!vrC-*cmWCc$@oC&SmQ=Ib%=FGhN~0gRq~KS2x*v!(sByYb;w2g~HliYdJ+7%30}czl%4P4bteA1#-O- z=WaLc6C1F(kg=mGEzLrf9=?P@&;KP9Yu49~J=K_ry3l51N8x1E{E^~(d8WVvX3UsZ zp*72qD>&}fvf1=TiuaeDgS?u~K^POAsp!GzC35a-6%`*qQEnOb4XE=SRbMs|`7Gi6 zS93#$eY}3hIU+YKQ`fI2G%sXvNjw7Qi!f{bV3RV0f)RL}(@owt5U^8OX2CzdoV>OW zie=uAJm1x*uVM(O{=nebg~d#YpjxQa07l)e+&vB}y*%Ga5T)@_$x z9^_8VMdv$Pl~td}Ok&o@9nET~4XhU1cnY~?t|aLU6@lSn8f8S7y7#2IlFvxF13UTX zQ=&nJNy1{(>0i6cT~Mu})+ZEPHckC&)XiD=S7=y|D2f-z1JbyQ0fjR8w{Dt21AB6Z z8)o~Li_27LZXddBz}(umV-B?bdVLtZ%Cmc_3=UVA0_j|>?0&DDW4+ph^p)e4DsuZ0?*L1?49iGZl@REhIML;A#UWoYK+i3tl;5XR)Ro^>=d=om0Dz4=9d42 zCQ@kGS(-&@kNj7HeCfU+AW_+c;Dj%p@Iu~x--ik@Tk)69LGGRKdV`$s+;E;(h*3Eb z3Gr)xC^xz+iVXyM*{`y+4(d9-3+bv`bje+Gf1BX!_dTWlHNWY}AnSbreHBo?-nJ+9 z7~C6BG`9yYf7e(xadHLkZd^Dv$g?x5n6M+k?8K{qYxvFGs>g?MQhan}AJC*ijJ#!( zr>+O@`?hdBF00xUq|*OX80h9v|H;B}#w&46YSfk=*lGHd1bR4S2-G8l;|sy3mTZ?; z(vL~~isIT7Tpwpbcq*T#b@23n%1c@er^&|zlJ7iSQ)1m4n(H}W>)}}PLO8ZiX5Ws1 zNfv-Mna_>Fg0mA+5&^OS>-C&AUX2Z61MY1cY{WCYV@Cgk{%m|PnGc=DBv0QXHNzg= zS5LH>#oDrtsc9p;61tcpXpcF0GjGn7%(8d7IIWib@wJ;^Qmtc;NMBO+w(LFfNw*K8 zj+BD}509jcR-5%(KO%_^&gfJtG{d?*nm#}ayd2T|PcbQ>*rI_jiLwbFK}i50cL>UC z=KS+j8@|oDW8j-MB}CFpWC&B|P23*+o;Zg-I^BccbgO=?g}VzP7N!qQ#(+09 z9v_F$lg7EE2j-M$kD3|Y@x?!Df=oG3bue&HU1N+zB?iNf)`2R>UV>{cfsLhiP9f_u zF(R`v(JM)KyF-Gpe#uKXDZr>H#1aMUlj+x&m?j>aP5e+3 zKh(qzHSt4D{7@7B=TQ>_BJcV@_NwVtUn6J_AET}~Nh%qx=GCt{?=r;~D} zN+$>t>Uni46aG}br%RazugtHX8HG=1Wc<|vR2xDO@69=cdG(0A-O^RCo2lm~=cO_P zQ2ll$fccN3*RS{EpC}Wu@JLA)1dP5X^&|>Y9#Y_==jkZc%?zTDco$1^K8Z_x5W-y^ zp{4gVXljK?$cl>snTFnctJt04MtXF;14DeEOxRyiLf9@ClC&?Z4*bt`JNRz&d2){Z z6QI}y?))Zq{A-jQ4)$sOELWhO-}{V6Szrx5gUxtIuc{$)}nXG{75{7E+m zB%wv&0U%y&mL*Q3%;zDdv{94=jN&6YdSN=R)7@nv#rrob$x{~(a_IYZX_1{BFSIX7 z_5p`=zA!a*@Lfv)MAiuY1cDMoPK}S|2RdzYN*n4Lk<4a!$<8`leb9_B*F@Iek|ox8 zW=|5_wpd}9m$EoZaVUf!zZ5ZlZOb!d6__5T!yE&380R|!^DQI;^dhNR$dXwtOSIU8 z`~vW01M~+ZOkd-~_e6gFC{m?!wzmXRH9p|hJ0!J$W#EYBhgfk4Y=Bm+w@?HG=L|`&Hc~g?>-r@c~KCLN0Njdx2;X)>P#Z= zu}Qs^B;URr*-k?3aej#0oT(FFeW#4}&g1eqqz9cg2nJW~0B4DMTjt&=qg9VC%pl3 zU*rSa>pD1%S&02(ZBYo_-$6P6dPNRusv)IINO`=@;+Eb%Cr#=&C5lI#ch5GDNF>!t z7G5?~+kwsDei4;UTD|-74)giRtTuWSOEYEYh17+u^GXMCETiR#=>L+_cvaA~tgM)2@_VEm5UCQR~htilDHMLDnZ zi75;->{+{IX@EJiJ^Hl7;WtwGtm=xNihOT@nChUiI0tl^hkZVaM_0Z-zmQfpuA{^U ztDsw*l;Cua1+?12Z~Kuan&aDAAvE$q#CUuUuSQ{Oyt~|J8Zu6Bd;Xp~W`M!VW9ZUc zL*ATEWFx1%7Q=7z32l6egEGg%GgGy@FIZA&q0HJ}eE)s=)V+6bYS4%LD;{oJXU24= zJ&ky>PW*M-M@&~o5yX~qs>WHb^LTlQZN>=(&eybK7A-pYcV0+HxU$esd?Q?6N0GE( zk}(p?D$2ogxs~G+$xsgiKE(g?_yo#X0rkdi#Hv z5jFk!k^l8(1l2z?Lh16a?f+*+pl|xi`z(yK;Uv`ZJqT z@NC89$>ZEefl)v#vCne0#n``5ZcI};uXg3gp{F+ccBiBe&ZVsZwSoU zHdSmFOlE#MZ(@hL7UxpH1bvt^Uz^6P2rfReydvS_S})4Cfh?izKwRekV@CX#5kF?c zj~Ve}M*Nr&KW4;_8S!IAi2s-oKW4;_8S!IA{Fo8{d&~%p_UM6GySb6Cg<oPV zi~as-vHzbN;p}@_5u3eyM52}*fmJJM5?}ySNll1dQ|bU;#F@IO-5UV21+19hmp8$~xHwR%ma*{}=R}pK&{B#3 z(ITpOc37Pbn5TI(juAP3x>OGuN=0FtN0`>{?tgehK^fa(ktlqO+PvPagXd0d-^8D@cB4Fzr)(6>K#;m2-&ds${)}`#NlbMJput3HD@+TmQ z^(BrI*4vw<(8n%OH)n@C7OG1ek5n}7*nN3);lCHgx@&lo^#TMVvPo>(d8G78NG8Z-`(r>YG@TP~W`aFj?eNdfT{l=ML7Aq5mBPLmy2HO|tuh-F&9c zF5gGUggQcfH_=vCpI*(7h1Vp%@oc=fXEm&W*-*vi5a)&#HpR-&Gux*>K*f5cRB|wG z+1s}^uMSIi%1XojYhX8nXcw-}oL9zqp9q&vTM|}3y!Uk}v5IXGBhjMGYCPNZ7VkeL zmUIpL8fNFc>@ed(&;kKRDgmZ6qK=_6HG-+~dnJ|`*VX?|u17(N4+0s#9Q1{-l5#HA z7c)GZkOz*ah)y*p_V{{2y5T#CalV`gc$7|7{9Vv7<9}=C zF4y8jeF2OChyGwwo1P=uF0KtR1Q%HYv<_@}_r~6C~ z7qQo$z1GWd{q)iotzj2W#@UGo^J|bKt@qUjMN=T`g;HqwJYAa=Og>YU+ z$dDsHO1N@%gD|^-KC(xY^b(7#vQGIjz~Jn^fsxo-)F&&oJLd&jjvU^Tp99_3uo){) zwhJrYF>U?Si5|7axwEaGp*y|^WeI<=+dVq;MCP4go9e3HN1nz9Bq`Buc&8g5PJB2D zwATIj(px8qjeTY4LbQVw_cMO-IY9K)s;1-RBLueEB@}cZ-VEG9B8<#u9f9+{q>>r5 z(jsH(Wu6LlqAF7gwtPG+i~#|ZBnHsjR6F7`|G!ftst+N)6MmjZ`bbIYyjNZ^Zbb1E9mL*-&aceVaf44uez4FOjMUdpM_j59G@tB}PF1Ga`5zA7 z@18qQ-qntUEuk{tTaW~NL+7-P?-RCO65S61c9Rz7qE*SsVH6c?JxF?oyk|F1WHm8k z_h>&%MkiJw`eS!n$%9OPwAVGa1}~4k;eC-tJ28K6h7XIpoM}{y%F+AK^jBCUTh)M) z(CIO-bry$8zrxv*sl>UhQM$XVRmPMRNE_(c%h2YE2il`W+jFuew=#6c0lyF?si8Y_ z|EP-cVFq=A(z5yQZlT|WnUsp^$ALd4gtDeQFnvfwe+XH;6Ud9(|s_*(kN>K8jD^k+oSGIc!-T7VlJR7f3v^KAJ1QG@{Dh z-dX)283kM|e>M+slg)NkfwMTvefce+BK@XGFuZ9JZ<@rLCh?|8ylE0|n#7wX@uo?< zX%cUm#G5AZrb)bM5^tKs|5KWTvAP{7zxEL)lG3MwME(IZ@A`tb%`u0N_ISR~5F%jJ zPb9s#NW`w7=E|(hPvp?|SYZT*ZiJisa5+-RU^8zr`@0zffOa5c_i0WXCB)N!X*oVr zABf9|#|5jI5j~Xlir+C>~D^y@_rT@z%>m<*2&ff+AKIi;TC?e^42EdExiLX2qkB znH~;N`)SIlj%b-6>e#r*bU~=B9H_`?ms6IZw%3N9eQiNtlXM-ql#Mwb3dR(;r3<$k zQ=M)A%Kj3((_%c^Ch1YQObM_6{rF|D;$Lv^b*>>zb!K$@RL=AZQ?mMu_QGyc7IXA- zH1G9(dNz1%o{sH8Ah9&1>)jX+<1F898>(LfZem`;_WUJK&E$N@ZJ;O}*4X=5SIZYo zP9(SN6MetZ55Erl8Wh#SZ$I`L#f0z;9|?+l_VPVJtsX4$F_I!Mge7O9nqLTdT~<;) zE()`AO)l%jDSUQ>v(p$*~>|l8JCApgEVrG=qZjeY=lncH2(*a52=ck0K zXnivYFj(R#Nu;gjA~MlG^u7*48=+K0K9aqb8FMXd9WUGn_8eU#^zqfBJxNY~#3&*x zy-EDq#lyfCOr9{dKcF2zk@Kr}p8-w@mPj6xT4;mv;bV6^as}y57K;taPx`b%+;c$InmE zo%@J|dd!#$8m$d{;s}-yN)Z32Y2BegW>iAAtLm`Ti$N6Z8phemAQtppaoQ|N;%+@qDwlTzb_n7S>FLb!JP9PMwnpGW$g0Bfk%=0!)^%H_g5tt&iQ1 zj)Y1dIP0n}qy^{CpcVX=G>M!;AshmP;MRi#d&>1QMg_T}nWS9m<6bv>PUaYx5;{D@ zQvk3z%o?$L?rRo-jWN934u(nr?Wh*2aD;N90)c5%q76|b6hKBGK-r+y`4okx-yx@) z40J||KByxKEWy6M5B076g+ZbcCV8fdy@h?vz99imEhljWK#(vH$+?|s^wwrEB zL!LDJ?q{}WRjxmlrfWEWXp4neKn6H@pS(6UtfMX%Beqny+Qxxoi^3^5t;kTUCKOr-vKKee=kV5hw)j=F zK{;nceb6}godx6grw|;~jt9jmZ^lYBdIT^FG3RiqC+&K_ii*YyAvc7TKfB_UKx_ji z(QWFhaGl)vaQ~Gp*~#!o>hBz2Rk)FV%_xJ6S762{c!CzTX9wva5wYi}u1isDzOd$)IiYJ;DfpmB!ZywYF&_hWkftnUNmWE(K`1dSLhE znXGmDK<{m%45eSJ>0MyTg^z2XSLjZk?O@D^p;gruBEB}{RJm&z2iGu{Zh{hST;=Cf zJBk=d8&Q1=D@N5_nH;k}(}Jx~>*R_D`I0<>9PExaib((cNQq4HUtzCaLwk7@=l0%$ z+|5~$W~QJ+qLqhQwFbRLYia*Kc>bO>6{+P)jg)vJCEiGhH&Wt_l=xpxO4wEhM3^v}mNKJR8UHZ&3Ov>z+|;H`&mjA& zvApTceFrO^pdN?A4+v&~DLY4+>-<|!H0w=pHDgz>0J8}kh$7gXv|xe^|Fg&rFhUdK z&$#qy#H>hv|4PKYD)>=#Kxi6G6@AhM%3dms8%6H9zw$_#jyE9#=Z|hHg-+SvDa$OP zya)N8G$qn#C&?&x)$yFC&J(W1P zrMa-&59N(d^VHZ+E}R6RX3AE6ynPfsgeW@5Q=e?`CUO{9>I%q&{n}pPD2)pJh{;Bd zNX4&Zgm~5Ex%97jJogMAdBP}C-6!>&OY#SGr5*h1Z(Y!@20wLKFT`G@tU()Y>C&;K zOtFh4s*VWSM!$^|XBC<72^j`dqLSccZM49AbubGa1_C7{yzMA^N?emN?$I=M6e+T( z6-BuRTHW;hp$Db|qU13Jj%OYY2iY-Ot}rteF-z|r2^J0eXpsW0Zi4>v&V=2wS+j9U z!LN}E3fts?`le(7V!boPzV09BmhapBvytW=3C5aXu83`F|E99Rd**df2To>3({bCL z5ON9ik?A456s2b9i^$W5!5tcJ_=NQT*~<|8!a@FQpHx%{w;5?70V>FEs1wroeXReq zC4xxj-lG&ZLv{!$dWmNg`(H*)M7c<~Tp_-YK2Ru=4WV^6V4mBaM4Syfr2{6Su=ve! z#%~40$8@JQ>{FRRJr`)6^#2$O{}a+g<9x41e69C_DZp)bV^hmCLDxGxWsx6!(b%JM z>077G#NEoKh$RTtFDPM8bj_D9y71VGF}Zfmy<4Kj$IB0nmgN6(6XPZ?kX>3L09B_s zKx)5InEV4J$C=wAI*%Ksv(XAF;vXCtO4Rb9YPrbTBh4~p=pS3S2QQT-X?KdM@)^|O zBuz1s)E0dP|10sk8J+)=Z9%Vota$moF0+owf2EwcPeXduz8T+Z|s8$ z&Fuix(Q7yYuj+-=p}v`n-HW5c_$G*S(7(Sp5hhxq-EzqJg3ou(D;A2Z0QtUQdL{mX z5C0fna>xz=P%Yl;t$#mpJr{hzH+qbZxxo+56)k}Q-4Luhhdp75<&fU%y@+k9Ojh=~ zp0GW)(cGqH&S_x#olp@3lmYu{3e^1{;~){@HwVI6*&yekHHp?k={n$)NN-$9bM+g!}t%My=SNA+tQ6lyJn&wcMmyLy(qHp7zXm!$fiUTdQPw zE7_pELO$mgAM4MWuFyXhT8cDyn?>e2$NO-Cq-7u{`C}RkPN#Qn97T&k_MzF9QLAJ^ zS3V7W#j%ocI-^v{JB_K9(4 zz#h$ZbWThQDt7W0v-nR>W-b_ zzI!&ii#)XTbG|@jYtv^zDRB4A7RECj-@X22?|7QC-B`w_>*@JZd-m9e@P`XDo>Vka z&t`j)OC;ksgcu`r!U-F&J)pUlq=cP!j5Mer{NTN5fJH4H7}a^3MqbUrBEd)41F&tW zk3GXl{^8dFcJDF0Z9vz1r4PNcfY$C@D$=g{C-s2tv(qa!Ho69+SKU!W^SDW-G!63a zN>7;5(?4{m9ShZMkgPdn@73_ar;u*;rrJMuH1e)WdT}g#-u4+nnVl7>Ys+KuTHo4X z)wDawy9oZEda?ix++V*>9Tz%8tH&gp$C@}l_}cb8PXEtfsd8YbO@o*&KB9GfNa(V~ zH2{<`84d@b*9Pvv#J2yORY_{m+6c38JxGaJ-%hD|@ zn+fz~J|u_ooCltR{4zmz?_8rT^&G<0J{4g^lj;PFe;FH$+S@pGFAR)zIrUAeGUV;6#&#(H3ZYje^l9g;CVl_%L84zW z8i0L^^zE9a4=yF>&kl97oO%Vx37uD6K3e;lidlsa0Rn{j&jR_<9F~n%U+LnIKgo8~ zw(nj#AIyUpw5sATeH3EzG*=`SYkVn52-w%UZgm+UHn3eD_fJI^{b5oh2BYm0vL(jG zWyE3*qX%Uuu?G3idQtfgyl+owDCB?pd{M&5-^_-jG{9S8jdtWZZc`^pR? z1EbC`-0mZbuD*nl&hgqY)lX1kY)t*&{;Wy`4jVf3@~{jr){mhovfWSTm?=*};^KL? zJ}}2>AvT>p=D5;fdvh0?H@HgD)MMnBDd{OMop+^61v8T;&-w)}SS_q>CLklRWW;{E zE60ByE7(d;26>2f?eT72`D3JhE)VKBWECIP`@n_nf;f_-u*nG?cf^;~Z2<7n+Ia*< zW^=WZQm6c(G?R47uiaQ));Dy{F;(BWeK#p3w$sYN((9s(E06ne!|$mqJ4CUTazo;# zyFFPez6&mX>gF1w?M@5C$%?~W(Yy+-c7j26v1q^FltrpG`(2P>h3RC}6xl=4B#-x^(6 zqR;hv8Nbm9e7mmj!Pe1CXI^cp21i4CNm1?_<1ju0V^{~VVJX0SrRxeRx&Dr+Xx0m? zy6wvA7QCNa$cET4AhhT97a5LMN}JEa1=?;5xGKJxi_mk6c}!8u=Inj9CuhUSiv@mz zz_D;YE)uoQI&@}pB2>e?HjAxyst4)oViB2=7W5NjKm)5K6QQ*PI@av?YnXW77MP>c zIpK;pj)dZ{25o!>Ol*HU?{7jkCR|8@O!Yexfhp0<*S#Y4{WW=mV$b_pI-h zs|A%@55$FiamMh7<4bk2#aL|jx^+S<<<#+nOdrgAtXz~DX70*gIF$zPsduR2#I}7{ zTt|b`?2sSZDQ+4V*uMBF?k>Ml`Hm!@Do`vNyxOV$^G^l+b3H*y*_K2pH(2pj20Atv zeU?1vXrIB4axxe3ZV63|j%8pMk&+%~V@{-K7j2KGnEOeq6ecd%VE^p7Qpg=w@>%D% z2#>)(Mc&gH3$A5U@x#bR)f&8k0wjXN`m$7 zt`a^6m_GQ|%30cb(Waq~{a6DrmbT!-R*Z%)QU+t3s#!`=U)p=~vz$)Cus=egy=Wcg&ZqUX#3iV=J7%S%C4*;C z9_oc&gTQeycD%?&B$19b8h_wdO)>rTP|Sao38vd_Q(U1wzB@?xUT|;#LHKAmo-3KO zUb@7~J;BR3hQfWcol$0jvhw#4Y_%lq&IY_z!lq3^COxLeDA%j7HAxo*+XCBSMD5eR zI&F9dpssoH&BLf}PJ z@^ZsmKD1F?aw>I(^=Tm#t=RRwy#NL~wHs#?h^sA?OuZ9pjKA2Z zLfEBNU8u2~2l*cNbM(k3*@_f5)m4X7+tYD;L{|htt8$foH3XAVGV0F12$W~oF0=En zd##0W33+R6rNhT}XPjC%gxF1HU%m9F7@XBC6dQ^ZXMDm#>b{FC%G@tpODzqAr)f5& z=Tvs-E|vcEZCz_=-()vQxjf~q!r2XnOAp$sK=Wk++U`@fj3i&luKPW+Rqa{D{*EG~ zEZyUKa3Y6d&)iHW)MfmV`b*gs!sS4mQIMFey4Et$ zS(zbO%=CWR$LXq`G3t}6E8++iaoMryU;J^_zdQLyehM%ld=N_auHSr8VDy+S17~Mu z66COj1Ac!xP}dH*KZWoP&RsG^SGYbNu-XS9fNlw%7Rp_c=pwMn&`1#k5$QH98c>`9 zl*|8&>Gq3gyosFY61&&jjl~M0d`~7%;UkySH6Xt*Sj1Li0e}1}lC`Q2@)|TQcCT0h zCk|uVQ=Cc`zE3;Wpi0oqsj8roWE?|<7$5U$H)vYu<6I&gbgq}=G*Ld8kSqcUTP#-0 zIKe1!g|4Jd+TNGi9zKlVd_@U6&1y{WjaaOo`07b ziLMr@hhMnre;WlTfHDaS4^XDNHh43s#6j(IKbPl5GRc>k$P+X+Tf08Mj;O++@v#Nn zBQnneh9pOjD$(S_Xp8(`3QNfN?nAnyIk z2O2uTp-{owh)vNKr>Cps%XQp-CnIiuw7j-;^Z+!`McJbOIMd)DI}!yMFuewd<%(T- zP`&MV0?FJp)Knfi6Y}i@G9lx=9|aEgFIHp`oE!EWN6q5Lq>KJI`Mena=(CIhw1Uhb yJ!`(~v+&IzZG)c;m{m>cqny7P3BRIier6weEsLC|} literal 0 HcmV?d00001 diff --git a/asset/icon/app_icon_round.png b/asset/icon/app_icon_round.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2845aa855f46dccf9d8826357a350f08183b80 GIT binary patch literal 37850 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 literal 0 HcmV?d00001 diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 00000000..132fd605 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,121 @@ +plugins { + `kotlin-dsl` +} + +kotlin { + explicitApi() + jvmToolchain(17) +} + +dependencies { + compileOnly(libs.gradle.kotlin) + compileOnly(libs.gradle.ksp) + compileOnly(libs.gradle.android) + compileOnly(libs.gradle.compose) + compileOnly(libs.gradle.compose.compiler) + compileOnly(libs.gradle.room) +} + +gradlePlugin { + plugins { + register("diary.kotlin.multiplatform") { + id = "diary.kotlin.multiplatform" + implementationClass = "plugin.kotlin.KotlinMultiplatformPlugin" + } + + register("diary.kotlin.multiplatform.common") { + id = "diary.kotlin.multiplatform.common" + implementationClass = "plugin.kotlin.KotlinMultiplatformCommonPlugin" + } + + register("diary.kotlin.multiplatform.all") { + id = "diary.kotlin.multiplatform.all" + implementationClass = "plugin.kotlin.KotlinMultiplatformAllPlugin" + } + + register("diary.kotlin.jvm") { + id = "diary.kotlin.jvm" + implementationClass = "plugin.kotlin.KotlinJvmPlugin" + } + + register("diary.kotlin.android") { + id = "diary.kotlin.android" + implementationClass = "plugin.kotlin.KotlinAndroidPlugin" + } + + register("diary.android.application") { + id = "diary.android.application" + implementationClass = "plugin.android.AndroidApplicationPlugin" + } + + register("diary.android.library") { + id = "diary.android.library" + implementationClass = "plugin.android.AndroidLibraryPlugin" + } + + register("diary.koin.common") { + id = "diary.koin.common" + implementationClass = "plugin.koin.KoinCommonPlugin" + } + + register("diary.koin.all") { + id = "diary.koin.all" + implementationClass = "plugin.koin.KoinAllPlugin" + } + + register("diary.koin.datastore") { + id = "diary.koin.datastore" + implementationClass = "plugin.koin.KoinDataStorePlugin" + } + + register("diary.koin.room") { + id = "diary.koin.room" + implementationClass = "plugin.koin.KoinRoomPlugin" + } + + register("diary.datastore") { + id = "diary.datastore" + implementationClass = "plugin.datastore.DataStorePlugin" + } + + register("diary.room") { + id = "diary.room" + implementationClass = "plugin.room.RoomPlugin" + } + + register("diary.compose") { + id = "diary.compose" + implementationClass = "plugin.compose.ComposePlugin" + } + + register("diary.app.data") { + id = "diary.app.data" + implementationClass = "plugin.convention.AppDataPlugin" + } + + register("diary.app.domain") { + id = "diary.app.domain" + implementationClass = "plugin.convention.AppDomainPlugin" + } + + register("diary.app.feature") { + id = "diary.app.feature" + implementationClass = "plugin.convention.AppFeaturePlugin" + } + + register("diary.server.domain") { + id = "diary.server.domain" + implementationClass = "plugin.convention.ServerDomainPlugin" + } + + register("diary.server.data") { + id = "diary.server.data" + implementationClass = "plugin.convention.ServerDataPlugin" + } + + register("diary.server.feature") { + id = "diary.server.feature" + implementationClass = "plugin.convention.ServerFeaturePlugin" + } + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 00000000..5d11ff59 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,21 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google { + content { + includeGroupByRegex("com.android.*") + includeGroupByRegex("com.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/Build.kt b/build-logic/src/main/kotlin/Build.kt new file mode 100644 index 00000000..e1a11628 --- /dev/null +++ b/build-logic/src/main/kotlin/Build.kt @@ -0,0 +1,9 @@ +public object Build { + internal const val JDK_VERSION = 17 + + internal const val ANDROID_COMPILE_SDK = 35 + internal const val ANDROID_MIN_SDK = 33 + internal const val ANDROID_TARGET_SDK = 35 + + public const val NAMESPACE: String = "io.github.taetae98coding.diary" +} diff --git a/build-logic/src/main/kotlin/ext/DependencyExt.kt b/build-logic/src/main/kotlin/ext/DependencyExt.kt new file mode 100644 index 00000000..9880477a --- /dev/null +++ b/build-logic/src/main/kotlin/ext/DependencyExt.kt @@ -0,0 +1,83 @@ +package ext + +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.provider.Provider + +internal fun DependencyHandler.implementation( + dependencyNotation: Provider, +) { + add("implementation", dependencyNotation) +} + +internal fun DependencyHandler.implementation( + dependencyNotation: ProjectDependency, +) { + add("implementation", dependencyNotation) +} + +internal fun DependencyHandler.ksp( + dependencyNotation: Provider, +) { + add("ksp", dependencyNotation) +} + +internal fun DependencyHandler.kspJvm( + dependencyNotation: Provider, +) { + add("kspJvm", dependencyNotation) +} + +internal fun DependencyHandler.kspWasmJs( + dependencyNotation: Provider, +) { + add("kspWasmJs", dependencyNotation) +} + +internal fun DependencyHandler.kspAndroid( + dependencyNotation: Provider, +) { + add("kspAndroid", dependencyNotation) +} + +internal fun DependencyHandler.kspIos( + dependencyNotation: Provider, +) { + add("kspIosX64", dependencyNotation) + add("kspIosArm64", dependencyNotation) + add("kspIosSimulatorArm64", dependencyNotation) +} + +public fun DependencyHandler.kspCommon( + dependencyNotation: Provider, +) { + kspJvm(dependencyNotation) + kspWasmJs(dependencyNotation) + kspIos(dependencyNotation) +} + +public fun DependencyHandler.kspAll( + dependencyNotation: Provider, +) { + kspJvm(dependencyNotation) + kspWasmJs(dependencyNotation) + kspAndroid(dependencyNotation) + kspIos(dependencyNotation) +} + +public fun DependencyHandler.kspDataStore( + dependencyNotation: Provider, +) { + kspJvm(dependencyNotation) + kspAndroid(dependencyNotation) + kspIos(dependencyNotation) +} + +public fun DependencyHandler.kspRoom( + dependencyNotation: Provider, +) { + kspJvm(dependencyNotation) + kspAndroid(dependencyNotation) + kspIos(dependencyNotation) +} diff --git a/build-logic/src/main/kotlin/ext/KotlinMultiplatformExt.kt b/build-logic/src/main/kotlin/ext/KotlinMultiplatformExt.kt new file mode 100644 index 00000000..bcfbbf98 --- /dev/null +++ b/build-logic/src/main/kotlin/ext/KotlinMultiplatformExt.kt @@ -0,0 +1,17 @@ +package ext + +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.plugins.ExtensionAware +import org.jetbrains.compose.ComposePlugin +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +internal fun KotlinMultiplatformExtension.sourceSets( + configure: Action>, +) { + (this as ExtensionAware).extensions.configure("sourceSets", configure) +} + +internal val KotlinMultiplatformExtension.compose: ComposePlugin.Dependencies + get() = (this as ExtensionAware).extensions.getByName("compose") as ComposePlugin.Dependencies diff --git a/build-logic/src/main/kotlin/ext/ProjectAndroidExt.kt b/build-logic/src/main/kotlin/ext/ProjectAndroidExt.kt new file mode 100644 index 00000000..1e2afbbd --- /dev/null +++ b/build-logic/src/main/kotlin/ext/ProjectAndroidExt.kt @@ -0,0 +1,29 @@ +package ext + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.getByType + +internal fun Project.withAndroid( + action: CommonExtension<*, *, *, *, *, *>.() -> Unit, +) { + val extension = extensions.findByType() ?: extensions.findByType() + if (extension == null) { + println("$displayName doesn't has android extension.") + return + } + + action(extension) +} + +internal fun Project.withAndroidApplication( + action: ApplicationExtension.() -> Unit, +) { + with( + receiver = extensions.getByType(), + block = action, + ) +} diff --git a/build-logic/src/main/kotlin/ext/ProjectComposeExt.kt b/build-logic/src/main/kotlin/ext/ProjectComposeExt.kt new file mode 100644 index 00000000..1f500c1e --- /dev/null +++ b/build-logic/src/main/kotlin/ext/ProjectComposeExt.kt @@ -0,0 +1,24 @@ +package ext + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.compose.ComposeExtension +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension + +internal fun Project.withCompose( + action: ComposeExtension.() -> Unit, +) { + with( + receiver = extensions.getByType(), + block = action, + ) +} + +internal fun Project.withComposeCompiler( + action: ComposeCompilerGradlePluginExtension.() -> Unit, +) { + with( + receiver = extensions.getByType(), + block = action, + ) +} diff --git a/build-logic/src/main/kotlin/ext/ProjectExt.kt b/build-logic/src/main/kotlin/ext/ProjectExt.kt new file mode 100644 index 00000000..6fce7ce7 --- /dev/null +++ b/build-logic/src/main/kotlin/ext/ProjectExt.kt @@ -0,0 +1,79 @@ +package ext + +import androidx.room.gradle.RoomExtension +import com.google.devtools.ksp.gradle.KspExtension +import java.util.Properties +import org.gradle.api.Project +import org.gradle.api.artifacts.dsl.DependencyHandler +import org.gradle.api.plugins.PluginContainer +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension + +internal fun Project.withPlugin( + action: PluginContainer.() -> Unit, +) { + with( + receiver = plugins, + block = action, + ) +} + +internal fun Project.withKotlin( + action: KotlinProjectExtension.() -> Unit, +) { + with( + receiver = extensions.getByType(), + block = action, + ) +} + +internal fun Project.withKotlinMultiplatform( + action: KotlinMultiplatformExtension.() -> Unit, +) { + with( + receiver = extensions.getByType(), + block = action, + ) +} + +internal fun Project.withDependency( + action: DependencyHandler.() -> Unit, +) { + with( + receiver = dependencies, + block = action, + ) +} + +internal fun Project.withKsp( + action: KspExtension.() -> Unit, +) { + with( + receiver = extensions.getByType(), + block = action, + ) +} + +internal fun Project.withRoom( + action: RoomExtension.() -> Unit, +) { + with( + receiver = extensions.getByType(), + block = action, + ) +} + +public fun Project.getLocalProperty(): Properties? { + val file = project.rootProject.file("local.properties") + + return if (file.exists()) { + Properties().apply { + file.inputStream() + .buffered() + .use { load(it) } + } + } else { + null + } +} diff --git a/build-logic/src/main/kotlin/ext/VersionCatalogExt.kt b/build-logic/src/main/kotlin/ext/VersionCatalogExt.kt new file mode 100644 index 00000000..ff565509 --- /dev/null +++ b/build-logic/src/main/kotlin/ext/VersionCatalogExt.kt @@ -0,0 +1,26 @@ +package ext + +import org.gradle.api.Project +import org.gradle.api.artifacts.ExternalModuleDependencyBundle +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.getByType + +internal val Project.libs: VersionCatalog + get() { + return extensions.getByType().named("libs") + } + +internal fun VersionCatalog.library( + alias: String, +): Provider { + return findLibrary(alias).get() +} + +internal fun VersionCatalog.bundle( + alias: String, +): Provider { + return findBundle(alias).get() +} diff --git a/build-logic/src/main/kotlin/plugin/android/AndroidApplicationPlugin.kt b/build-logic/src/main/kotlin/plugin/android/AndroidApplicationPlugin.kt new file mode 100644 index 00000000..bf2d9afc --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/android/AndroidApplicationPlugin.kt @@ -0,0 +1,25 @@ +package plugin.android + +import Build +import ext.withAndroidApplication +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class AndroidApplicationPlugin : Plugin { + private val androidPlugin = AndroidPlugin() + + override fun apply(target: Project) { + target.withPlugin { + apply("com.android.application") + } + + androidPlugin.apply(target) + + target.withAndroidApplication { + defaultConfig { + targetSdk = Build.ANDROID_TARGET_SDK + } + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/android/AndroidLibraryPlugin.kt b/build-logic/src/main/kotlin/plugin/android/AndroidLibraryPlugin.kt new file mode 100644 index 00000000..1ec6acef --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/android/AndroidLibraryPlugin.kt @@ -0,0 +1,17 @@ +package plugin.android + +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class AndroidLibraryPlugin : Plugin { + private val androidPlugin = AndroidPlugin() + + override fun apply(target: Project) { + target.withPlugin { + apply("com.android.library") + } + + androidPlugin.apply(target) + } +} diff --git a/build-logic/src/main/kotlin/plugin/android/AndroidPlugin.kt b/build-logic/src/main/kotlin/plugin/android/AndroidPlugin.kt new file mode 100644 index 00000000..16b0b979 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/android/AndroidPlugin.kt @@ -0,0 +1,17 @@ +package plugin.android + +import Build +import ext.withAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class AndroidPlugin : Plugin{ + override fun apply(target: Project) { + target.withAndroid { + defaultConfig { + compileSdk = Build.ANDROID_COMPILE_SDK + minSdk = Build.ANDROID_MIN_SDK + } + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/compose/ComposePlugin.kt b/build-logic/src/main/kotlin/plugin/compose/ComposePlugin.kt new file mode 100644 index 00000000..b2e1e846 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/compose/ComposePlugin.kt @@ -0,0 +1,34 @@ +package plugin.compose + +import ext.withAndroid +import ext.withComposeCompiler +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.assign +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag + +internal class ComposePlugin : Plugin { + override fun apply(target: Project) { + target.withPlugin { + apply("org.jetbrains.compose") + apply("org.jetbrains.kotlin.plugin.compose") + } + + target.withAndroid { + buildFeatures { + compose = true + } + } + + target.withComposeCompiler { + featureFlags.add(ComposeFeatureFlag.OptimizeNonSkippingGroups) +// featureFlags.add(ComposeFeatureFlag.PausableComposition) + +// stabilityConfigurationFiles.add(RegularFile { target.rootProject.file("compose-stability-configuration-file.txt") }) + + metricsDestination.assign(target.rootProject.file("build/compose/metrics")) + reportsDestination.assign(target.rootProject.file("build/compose/report")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/convention/AppDataPlugin.kt b/build-logic/src/main/kotlin/plugin/convention/AppDataPlugin.kt new file mode 100644 index 00000000..2f18e89f --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/convention/AppDataPlugin.kt @@ -0,0 +1,30 @@ +package plugin.convention + +import ext.libs +import ext.sourceSets +import ext.withKotlinMultiplatform +import org.gradle.api.Plugin +import org.gradle.api.Project +import plugin.koin.KoinCommonPlugin +import plugin.kotlin.KotlinMultiplatformCommonPlugin + +internal class AppDataPlugin : Plugin { + private val kotlinMultiplatformCommonPlugin = KotlinMultiplatformCommonPlugin() + private val koinCommonPlugin = KoinCommonPlugin() + + override fun apply(target: Project) { + kotlinMultiplatformCommonPlugin.apply(target) + koinCommonPlugin.apply(target) + + target.withKotlinMultiplatform { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:coroutines")) + implementation(project(":library:datetime")) + } + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/convention/AppDomainPlugin.kt b/build-logic/src/main/kotlin/plugin/convention/AppDomainPlugin.kt new file mode 100644 index 00000000..6806df88 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/convention/AppDomainPlugin.kt @@ -0,0 +1,51 @@ +package plugin.convention + +import ext.kspCommon +import ext.library +import ext.libs +import ext.sourceSets +import ext.withDependency +import ext.withKotlinMultiplatform +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project +import plugin.kotlin.KotlinMultiplatformCommonPlugin + +internal class AppDomainPlugin : Plugin { + private val kotlinMultiplatformCommonPlugin = KotlinMultiplatformCommonPlugin() + + override fun apply(target: Project) { + val libs = target.libs + + kotlinMultiplatformCommonPlugin.apply(target) + + target.withPlugin { + apply("com.google.devtools.ksp") + } + + target.withKotlinMultiplatform { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:coroutines")) + implementation(project(":library:datetime")) + implementation(project(":library:kotlin")) + + implementation(project.dependencies.platform(libs.library("koin-bom"))) + implementation(libs.library("koin-core")) + implementation(project.dependencies.platform(libs.library("koin-annotations-bom"))) + implementation(libs.library("koin-annotations")) + + api(project(":app:core:model")) + api(project(":common:exception")) + } + } + } + } + + target.withDependency { + kspCommon(platform(libs.library("koin-annotations-bom"))) + kspCommon(libs.library("koin-compiler")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt b/build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt new file mode 100644 index 00000000..6aaade2f --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/convention/AppFeaturePlugin.kt @@ -0,0 +1,86 @@ +package plugin.convention + +import ext.compose +import ext.library +import ext.libs +import ext.sourceSets +import ext.withCompose +import ext.withKotlinMultiplatform +import ext.withKsp +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.compose.resources.ResourcesExtension +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import plugin.android.AndroidLibraryPlugin +import plugin.compose.ComposePlugin +import plugin.koin.KoinAllPlugin +import plugin.kotlin.KotlinMultiplatformAllPlugin + +@OptIn(ExperimentalKotlinGradlePluginApi::class) +internal class AppFeaturePlugin : Plugin { + private val androidLibraryPlugin = AndroidLibraryPlugin() + private val kotlinMultiplatformAllPlugin = KotlinMultiplatformAllPlugin() + private val composePlugin = ComposePlugin() + private val koinAllPlugin = KoinAllPlugin() + + override fun apply(target: Project) { + val libs = target.libs + + androidLibraryPlugin.apply(target) + kotlinMultiplatformAllPlugin.apply(target) + composePlugin.apply(target) + koinAllPlugin.apply(target) + + target.withKotlinMultiplatform { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:design-system")) + implementation(project(":app:core:navigation")) + implementation(project(":app:core:resources")) + + implementation(project(":library:color")) + implementation(project(":library:kotlin")) + implementation(project(":library:navigation")) + implementation(project(":library:coroutines")) + implementation(project(":library:datetime")) + implementation(project(":library:shimmer-m3")) + + implementation(compose.material3) + implementation(compose.components.resources) + implementation(libs.library("compose-material3-adaptive-navigation")) + + implementation(libs.library("navigation-compose")) + + implementation(project.dependencies.platform(libs.library("koin-bom"))) + implementation(libs.library("koin-compose-viewmodel-navigation")) + } + } + + androidMain { + dependencies { + implementation(compose.preview) + } + } + + invokeWhenCreated("androidDebug") { + dependencies { + implementation(compose.uiTooling) + } + } + } + } + + target.withCompose { + with(extensions.getByType()) { + generateResClass = ResourcesExtension.ResourceClassGeneration.Never + } + } + + target.withKsp { + arg("KOIN_USE_COMPOSE_VIEWMODEL", "true") + arg("KOIN_DEFAULT_MODULE", "false") + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/convention/ServerDataPlugin.kt b/build-logic/src/main/kotlin/plugin/convention/ServerDataPlugin.kt new file mode 100644 index 00000000..a40ccc0f --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/convention/ServerDataPlugin.kt @@ -0,0 +1,37 @@ +package plugin.convention + +import ext.implementation +import ext.ksp +import ext.library +import ext.libs +import ext.withDependency +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.project +import plugin.kotlin.KotlinJvmPlugin + +internal class ServerDataPlugin : Plugin { + private val kotlinJvmPlugin = KotlinJvmPlugin() + + override fun apply(target: Project) { + val libs = target.libs + + kotlinJvmPlugin.apply(target) + + target.withPlugin { + apply("com.google.devtools.ksp") + } + + target.withDependency { + implementation(project(":server:core:model")) + + implementation(platform(libs.library("koin-bom"))) + implementation(libs.library("koin-core")) + implementation(platform(libs.library("koin-annotations-bom"))) + implementation(libs.library("koin-annotations")) + ksp(platform(libs.library("koin-annotations-bom"))) + ksp(libs.library("koin-compiler")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/convention/ServerDomainPlugin.kt b/build-logic/src/main/kotlin/plugin/convention/ServerDomainPlugin.kt new file mode 100644 index 00000000..909c809d --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/convention/ServerDomainPlugin.kt @@ -0,0 +1,40 @@ +package plugin.convention + +import ext.implementation +import ext.ksp +import ext.library +import ext.libs +import ext.withDependency +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.project +import plugin.kotlin.KotlinJvmPlugin + +internal class ServerDomainPlugin : Plugin { + private val kotlinJvmPlugin = KotlinJvmPlugin() + + override fun apply(target: Project) { + val libs = target.libs + + kotlinJvmPlugin.apply(target) + + target.withPlugin { + apply("com.google.devtools.ksp") + } + + target.withDependency { + implementation(project(":server:core:model")) + implementation(project(":common:exception")) + implementation(project(":library:kotlin")) + + implementation(libs.library("kotlinx-coroutines-core")) + implementation(platform(libs.library("koin-bom"))) + implementation(libs.library("koin-core")) + implementation(platform(libs.library("koin-annotations-bom"))) + implementation(libs.library("koin-annotations")) + ksp(platform(libs.library("koin-annotations-bom"))) + ksp(libs.library("koin-compiler")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/convention/ServerFeaturePlugin.kt b/build-logic/src/main/kotlin/plugin/convention/ServerFeaturePlugin.kt new file mode 100644 index 00000000..54c746bd --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/convention/ServerFeaturePlugin.kt @@ -0,0 +1,32 @@ +package plugin.convention + +import ext.implementation +import ext.library +import ext.libs +import ext.withDependency +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.project +import plugin.kotlin.KotlinJvmPlugin + +internal class ServerFeaturePlugin : Plugin { + private val kotlinJvmPlugin = KotlinJvmPlugin() + + override fun apply(target: Project) { + val libs = target.libs + + kotlinJvmPlugin.apply(target) + + target.withDependency { + implementation(project(":server:core:model")) + implementation(project(":common:model")) + implementation(project(":common:exception")) + + implementation(libs.library("ktor-server-core")) + implementation(libs.library("ktor-server-auth-jwt")) + + implementation(platform(libs.library("koin-bom"))) + implementation(libs.library("koin-ktor")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt b/build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt new file mode 100644 index 00000000..c6f34ab9 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/datastore/DataStorePlugin.kt @@ -0,0 +1,42 @@ +package plugin.datastore + +import ext.library +import ext.libs +import ext.sourceSets +import ext.withKotlinMultiplatform +import org.gradle.api.Plugin +import org.gradle.api.Project +import plugin.android.AndroidLibraryPlugin +import plugin.kotlin.KotlinMultiplatformPlugin + +internal class DataStorePlugin : Plugin { + private val androidLibraryPlugin = AndroidLibraryPlugin() + private val kotlinMultiplatformPlugin = KotlinMultiplatformPlugin() + + override fun apply(target: Project) { + val libs = target.libs + + androidLibraryPlugin.apply(target) + kotlinMultiplatformPlugin.apply(target) + + target.withKotlinMultiplatform { + jvm() + + androidTarget() + + iosX64() + iosArm64() + iosSimulatorArm64() + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain { + dependencies { + implementation(libs.library("datastore-preferences")) + } + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/koin/KoinAllPlugin.kt b/build-logic/src/main/kotlin/plugin/koin/KoinAllPlugin.kt new file mode 100644 index 00000000..5c1f86cc --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/koin/KoinAllPlugin.kt @@ -0,0 +1,39 @@ +package plugin.koin + +import ext.kspAll +import ext.library +import ext.libs +import ext.sourceSets +import ext.withDependency +import ext.withKotlinMultiplatform +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class KoinAllPlugin : Plugin{ + override fun apply(target: Project) { + val libs = target.libs + + target.withPlugin { + apply("com.google.devtools.ksp") + } + + target.withKotlinMultiplatform { + sourceSets { + commonMain { + dependencies { + implementation(project.dependencies.platform(libs.library("koin-bom"))) + implementation(libs.library("koin-core")) + implementation(project.dependencies.platform(libs.library("koin-annotations-bom"))) + implementation(libs.library("koin-annotations")) + } + } + } + } + + target.withDependency { + kspAll(platform(libs.library("koin-annotations-bom"))) + kspAll(libs.library("koin-compiler")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/koin/KoinCommonPlugin.kt b/build-logic/src/main/kotlin/plugin/koin/KoinCommonPlugin.kt new file mode 100644 index 00000000..d10cb2ec --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/koin/KoinCommonPlugin.kt @@ -0,0 +1,39 @@ +package plugin.koin + +import ext.kspCommon +import ext.library +import ext.libs +import ext.sourceSets +import ext.withDependency +import ext.withKotlinMultiplatform +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class KoinCommonPlugin : Plugin{ + override fun apply(target: Project) { + val libs = target.libs + + target.withPlugin { + apply("com.google.devtools.ksp") + } + + target.withKotlinMultiplatform { + sourceSets { + commonMain { + dependencies { + implementation(project.dependencies.platform(libs.library("koin-bom"))) + implementation(libs.library("koin-core")) + implementation(project.dependencies.platform(libs.library("koin-annotations-bom"))) + implementation(libs.library("koin-annotations")) + } + } + } + } + + target.withDependency { + kspCommon(platform(libs.library("koin-annotations-bom"))) + kspCommon(libs.library("koin-compiler")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/koin/KoinDataStorePlugin.kt b/build-logic/src/main/kotlin/plugin/koin/KoinDataStorePlugin.kt new file mode 100644 index 00000000..6a0a37ba --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/koin/KoinDataStorePlugin.kt @@ -0,0 +1,41 @@ +package plugin.koin + +import ext.kspDataStore +import ext.library +import ext.libs +import ext.sourceSets +import ext.withDependency +import ext.withKotlinMultiplatform +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class KoinDataStorePlugin : Plugin{ + override fun apply(target: Project) { + val libs = target.libs + + target.withPlugin { + apply("com.google.devtools.ksp") + } + + target.withKotlinMultiplatform { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:koin-datastore")) + + implementation(project.dependencies.platform(libs.library("koin-bom"))) + implementation(libs.library("koin-core")) + implementation(project.dependencies.platform(libs.library("koin-annotations-bom"))) + implementation(libs.library("koin-annotations")) + } + } + } + } + + target.withDependency { + kspDataStore(platform(libs.library("koin-annotations-bom"))) + kspDataStore(libs.library("koin-compiler")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/koin/KoinRoomPlugin.kt b/build-logic/src/main/kotlin/plugin/koin/KoinRoomPlugin.kt new file mode 100644 index 00000000..f7e57781 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/koin/KoinRoomPlugin.kt @@ -0,0 +1,47 @@ +package plugin.koin + +import ext.kspRoom +import ext.library +import ext.libs +import ext.sourceSets +import ext.withDependency +import ext.withKotlinMultiplatform +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +@OptIn(ExperimentalKotlinGradlePluginApi::class) +internal class KoinRoomPlugin : Plugin{ + override fun apply(target: Project) { + val libs = target.libs + + target.withPlugin { + apply("com.google.devtools.ksp") + } + + target.withKotlinMultiplatform { + sourceSets { + commonMain { + dependencies { + implementation(project(":library:koin-room")) + + implementation(project.dependencies.platform(libs.library("koin-bom"))) + implementation(libs.library("koin-core")) + implementation(project.dependencies.platform(libs.library("koin-annotations-bom"))) + implementation(libs.library("koin-annotations")) + } + } + } + + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + + target.withDependency { + kspRoom(platform(libs.library("koin-annotations-bom"))) + kspRoom(libs.library("koin-compiler")) + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinAndroidPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinAndroidPlugin.kt new file mode 100644 index 00000000..7704739e --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinAndroidPlugin.kt @@ -0,0 +1,17 @@ +package plugin.kotlin + +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class KotlinAndroidPlugin : Plugin { + private val kotlinPlugin = KotlinPlugin() + + override fun apply(target: Project) { + target.withPlugin { + apply("org.jetbrains.kotlin.android") + } + + kotlinPlugin.apply(target) + } +} diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinJvmPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinJvmPlugin.kt new file mode 100644 index 00000000..dec6f010 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinJvmPlugin.kt @@ -0,0 +1,17 @@ +package plugin.kotlin + +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class KotlinJvmPlugin : Plugin { + private val kotlinPlugin = KotlinPlugin() + + override fun apply(target: Project) { + target.withPlugin { + apply("org.jetbrains.kotlin.jvm") + } + + kotlinPlugin.apply(target) + } +} diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt new file mode 100644 index 00000000..8dc0ee86 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformAllPlugin.kt @@ -0,0 +1,31 @@ +package plugin.kotlin + +import ext.withKotlinMultiplatform +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +@OptIn(ExperimentalWasmDsl::class) +internal class KotlinMultiplatformAllPlugin : Plugin{ + private val kotlinMultiplatformPlugin = KotlinMultiplatformPlugin() + + override fun apply(target: Project) { + kotlinMultiplatformPlugin.apply(target) + + target.withKotlinMultiplatform { + jvm() + + wasmJs { + browser() + } + + androidTarget() + + iosX64() + iosArm64() + iosSimulatorArm64() + + applyDefaultHierarchyTemplate() + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt new file mode 100644 index 00000000..1bb4ffae --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformCommonPlugin.kt @@ -0,0 +1,29 @@ +package plugin.kotlin + +import ext.withKotlinMultiplatform +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +@OptIn(ExperimentalWasmDsl::class) +internal class KotlinMultiplatformCommonPlugin : Plugin{ + private val kotlinMultiplatformPlugin = KotlinMultiplatformPlugin() + + override fun apply(target: Project) { + kotlinMultiplatformPlugin.apply(target) + + target.withKotlinMultiplatform { + jvm() + + wasmJs { + browser() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + applyDefaultHierarchyTemplate() + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformPlugin.kt new file mode 100644 index 00000000..df034dbc --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinMultiplatformPlugin.kt @@ -0,0 +1,17 @@ +package plugin.kotlin + +import ext.withPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class KotlinMultiplatformPlugin : Plugin { + private val kotlinPlugin = KotlinPlugin() + + override fun apply(target: Project) { + target.withPlugin { + apply("org.jetbrains.kotlin.multiplatform") + } + + kotlinPlugin.apply(target) + } +} diff --git a/build-logic/src/main/kotlin/plugin/kotlin/KotlinPlugin.kt b/build-logic/src/main/kotlin/plugin/kotlin/KotlinPlugin.kt new file mode 100644 index 00000000..c187ed78 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/kotlin/KotlinPlugin.kt @@ -0,0 +1,15 @@ +package plugin.kotlin + +import Build +import ext.withKotlin +import org.gradle.api.Plugin +import org.gradle.api.Project + +internal class KotlinPlugin : Plugin { + override fun apply(target: Project) { + target.withKotlin { + jvmToolchain(Build.JDK_VERSION) + explicitApi() + } + } +} diff --git a/build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt b/build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt new file mode 100644 index 00000000..2eb04985 --- /dev/null +++ b/build-logic/src/main/kotlin/plugin/room/RoomPlugin.kt @@ -0,0 +1,60 @@ +package plugin.room + +import ext.bundle +import ext.kspRoom +import ext.library +import ext.libs +import ext.sourceSets +import ext.withDependency +import ext.withKotlinMultiplatform +import ext.withPlugin +import ext.withRoom +import org.gradle.api.Plugin +import org.gradle.api.Project +import plugin.android.AndroidLibraryPlugin +import plugin.kotlin.KotlinMultiplatformPlugin + +internal class RoomPlugin : Plugin { + private val androidLibraryPlugin = AndroidLibraryPlugin() + private val kotlinMultiplatformPlugin = KotlinMultiplatformPlugin() + + override fun apply(target: Project) { + val libs = target.libs + + androidLibraryPlugin.apply(target) + kotlinMultiplatformPlugin.apply(target) + + target.withPlugin { + apply("androidx.room") + apply("com.google.devtools.ksp") + } + + target.withKotlinMultiplatform { + jvm() + + androidTarget() + + iosX64() + iosArm64() + iosSimulatorArm64() + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain { + dependencies { + implementation(libs.bundle("room")) + } + } + } + } + + target.withRoom { + schemaDirectory("${target.projectDir}/schemas") + } + + target.withDependency { + kspRoom(libs.library("room-compiler")) + } + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..ac0f3a26 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform).apply(false) + alias(libs.plugins.kotlin.android).apply(false) + alias(libs.plugins.kotlin.jvm).apply(false) + + alias(libs.plugins.kotlin.serialization).apply(false) + alias(libs.plugins.ksp).apply(false) + + alias(libs.plugins.compose).apply(false) + alias(libs.plugins.compose.compiler).apply(false) + alias(libs.plugins.room).apply(false) + + alias(libs.plugins.android.application).apply(false) + alias(libs.plugins.android.library).apply(false) + + alias(libs.plugins.ktor.server).apply(false) + + alias(libs.plugins.dependency.guard).apply(false) + alias(libs.plugins.buildkonfig).apply(false) + alias(libs.plugins.spotless) + alias(libs.plugins.module.graph) +} + +subprojects { + afterEvaluate { + if (isKotlinProject()) { + plugins.apply("com.diffplug.spotless") + + spotless { + kotlin { + targetExclude("**/build/**") + + ktlint() + endWithNewline() + indentWithTabs() + trimTrailingWhitespace() + } + } + } + } +} + +subprojects { + afterEvaluate { + plugins.apply("com.jraska.module.graph.assertion") + + moduleGraphAssert { + configurations += setOf("commonMainImplementation", "commonMainApi", "implementation", "api") + } + } +} + +fun Project.isMultiplatformProject(): Boolean { + return plugins.findPlugin("org.jetbrains.kotlin.multiplatform") != null +} + +fun Project.isJvmProject(): Boolean { + return plugins.findPlugin("org.jetbrains.kotlin.jvm") != null +} + +fun Project.isAndroidProject(): Boolean { + return plugins.findPlugin("org.jetbrains.kotlin.android") != null +} + +fun Project.isKotlinProject(): Boolean { + return isMultiplatformProject() || isJvmProject() || isAndroidProject() +} diff --git a/common/exception/README.md b/common/exception/README.md new file mode 100644 index 00000000..c60f285d --- /dev/null +++ b/common/exception/README.md @@ -0,0 +1,3 @@ +# :common:exception module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_common_exception.svg) diff --git a/common/exception/build.gradle.kts b/common/exception/build.gradle.kts new file mode 100644 index 00000000..a26f4424 --- /dev/null +++ b/common/exception/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("diary.kotlin.multiplatform.common") +} diff --git a/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/ApiException.kt b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/ApiException.kt new file mode 100644 index 00000000..dc4153a5 --- /dev/null +++ b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/ApiException.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.common.exception + +public class ApiException( + override val message: String? = null, + override val cause: Throwable? = null, +) : Exception(message, cause) diff --git a/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/NetworkException.kt b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/NetworkException.kt new file mode 100644 index 00000000..babe26ac --- /dev/null +++ b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/NetworkException.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.common.exception + +public class NetworkException( + override val message: String? = null, + override val cause: Throwable? = null, +) : Exception(message, cause) diff --git a/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/AccountNotFoundException.kt b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/AccountNotFoundException.kt new file mode 100644 index 00000000..8c606391 --- /dev/null +++ b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/AccountNotFoundException.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.common.exception.account + +public class AccountNotFoundException( + override val message: String? = null, + override val cause: Throwable? = null, +) : Exception(message, cause) diff --git a/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/ExistEmailException.kt b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/ExistEmailException.kt new file mode 100644 index 00000000..dcb433d0 --- /dev/null +++ b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/ExistEmailException.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.common.exception.account + +public class ExistEmailException( + override val message: String? = null, + override val cause: Throwable? = null, +) : Exception(message, cause) diff --git a/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/InvalidEmailException.kt b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/InvalidEmailException.kt new file mode 100644 index 00000000..455b3710 --- /dev/null +++ b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/account/InvalidEmailException.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.common.exception.account + +public class InvalidEmailException( + override val message: String? = "", + override val cause: Throwable? = null, +) : Exception(message, cause) diff --git a/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/memo/MemoTitleBlankException.kt b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/memo/MemoTitleBlankException.kt new file mode 100644 index 00000000..55e1705d --- /dev/null +++ b/common/exception/src/commonMain/kotlin/io/github/taetae98coding/diary/common/exception/memo/MemoTitleBlankException.kt @@ -0,0 +1,6 @@ +package io.github.taetae98coding.diary.common.exception.memo + +public class MemoTitleBlankException( + override val message: String? = null, + override val cause: Throwable? = null, +) : Exception(message, cause) diff --git a/common/model/README.md b/common/model/README.md new file mode 100644 index 00000000..6acb8ffb --- /dev/null +++ b/common/model/README.md @@ -0,0 +1,3 @@ +# :common:model module +## Dependency graph +![Dependency graph](../../docs/images/graphs/dep_graph_common_model.svg) diff --git a/common/model/build.gradle.kts b/common/model/build.gradle.kts new file mode 100644 index 00000000..a8b74294 --- /dev/null +++ b/common/model/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("diary.kotlin.multiplatform.common") + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(libs.kotlinx.serialization.core) + api(libs.kotlinx.datetime) + } + } + } +} diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt new file mode 100644 index 00000000..57e18be3 --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/memo/MemoEntity.kt @@ -0,0 +1,30 @@ +package io.github.taetae98coding.diary.common.model.memo + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class MemoEntity( + @SerialName("id") + val id: String, + @SerialName("title") + val title: String, + @SerialName("description") + val description: String, + @SerialName("start") + val start: LocalDate?, + @SerialName("endInclusive") + val endInclusive: LocalDate?, + @SerialName("color") + val color: Int, + @SerialName("owner") + val owner: String, + @SerialName("isFinish") + val isFinish: Boolean, + @SerialName("isDelete") + val isDelete: Boolean, + @SerialName("updateAt") + val updateAt: Instant, +) diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/JoinRequest.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/JoinRequest.kt new file mode 100644 index 00000000..a230fd19 --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/JoinRequest.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.common.model.request.account + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class JoinRequest( + @SerialName("email") + val email: String, + @SerialName("password") + val password: String, +) diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/LoginRequest.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/LoginRequest.kt new file mode 100644 index 00000000..b723e36d --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/request/account/LoginRequest.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.common.model.request.account + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class LoginRequest( + @SerialName("email") + val email: String, + @SerialName("password") + val password: String, +) diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt new file mode 100644 index 00000000..be1e52cf --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/DiaryResponse.kt @@ -0,0 +1,28 @@ +package io.github.taetae98coding.diary.common.model.response + +import kotlinx.serialization.Serializable + +@Serializable +public data class DiaryResponse( + val code: Int = 0, + val message: String = "", + val body: T? = null, +) { + public companion object { + public val Success: DiaryResponse = DiaryResponse(200, "SUCCESS", Unit) + public val Created: DiaryResponse = DiaryResponse(201, "CREATED", Unit) + public val Unauthorized: DiaryResponse = DiaryResponse(401, "Unauthorized", Unit) + public val InternalServerError: DiaryResponse = DiaryResponse(500, "InternalServerError", Unit) + + public val AlreadyExistEmail: DiaryResponse = DiaryResponse(1000, "ALREADY_EXIST_EMAIL_EXCEPTION", Unit) + public val AccountNotFound: DiaryResponse = DiaryResponse(1001, "AccountNotFound", Unit) + + public fun success(body: T): DiaryResponse { + return DiaryResponse( + code = 200, + message = "SUCCESS", + body = body, + ) + } + } +} diff --git a/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/account/LoginResponse.kt b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/account/LoginResponse.kt new file mode 100644 index 00000000..49232d61 --- /dev/null +++ b/common/model/src/commonMain/kotlin/io/github/taetae98coding/diary/common/model/response/account/LoginResponse.kt @@ -0,0 +1,12 @@ +package io.github.taetae98coding.diary.common.model.response.account + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +public data class LoginResponse( + @SerialName("uid") + val uid: String, + @SerialName("token") + val token: String, +) diff --git a/compose-stability-configuration-file.txt b/compose-stability-configuration-file.txt new file mode 100644 index 00000000..3ac68085 --- /dev/null +++ b/compose-stability-configuration-file.txt @@ -0,0 +1,3 @@ +kotlin.collections.** + +kotlinx.datetime.** diff --git a/docs/images/graphs/dep_graph_app_core_account_preferences.svg b/docs/images/graphs/dep_graph_app_core_account_preferences.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg b/docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_account_preferences_memory.svg b/docs/images/graphs/dep_graph_app_core_account_preferences_memory.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_calendar_compose.svg b/docs/images/graphs/dep_graph_app_core_calendar_compose.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_coroutines.svg b/docs/images/graphs/dep_graph_app_core_coroutines.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_design_system.svg b/docs/images/graphs/dep_graph_app_core_design_system.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_diary_database.svg b/docs/images/graphs/dep_graph_app_core_diary_database.svg new file mode 100644 index 00000000..ef09f95c --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_diary_database.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:diary-database + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_diary_database_memory.svg b/docs/images/graphs/dep_graph_app_core_diary_database_memory.svg new file mode 100644 index 00000000..67d7a4dc --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_diary_database_memory.svg @@ -0,0 +1,41 @@ + + + + + + :app:core:diary-database-memory + + + + :app:core:diary-database + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_diary_database_room.svg b/docs/images/graphs/dep_graph_app_core_diary_database_room.svg new file mode 100644 index 00000000..47e4357e --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_diary_database_room.svg @@ -0,0 +1,57 @@ + + + + + + :app:core:diary-database-room + + + + :library:koin-room + + + + + + + + :app:core:diary-database + + + + + + + + :library:coroutines + + + + + + + + :library:room + + + + + + + + :app:core:model + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_diary_service.svg b/docs/images/graphs/dep_graph_app_core_diary_service.svg new file mode 100644 index 00000000..829d47e3 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_diary_service.svg @@ -0,0 +1,41 @@ + + + + + + :app:core:diary-service + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_database.svg b/docs/images/graphs/dep_graph_app_core_holiday_database.svg new file mode 100644 index 00000000..9109e744 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_holiday_database.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:holiday-database + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_database_memory.svg b/docs/images/graphs/dep_graph_app_core_holiday_database_memory.svg new file mode 100644 index 00000000..d92fe9e0 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_holiday_database_memory.svg @@ -0,0 +1,25 @@ + + + + + + :app:core:holiday-database-memory + + + + :app:core:holiday-database + + + + + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_database_room.svg b/docs/images/graphs/dep_graph_app_core_holiday_database_room.svg new file mode 100644 index 00000000..056af84b --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_holiday_database_room.svg @@ -0,0 +1,57 @@ + + + + + + :app:core:holiday-database-room + + + + :library:koin-room + + + + + + + + :app:core:holiday-database + + + + + + + + :library:room + + + + + + + + :library:coroutines + + + + + + + + :app:core:model + + + + + + + + :library:datetime + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_holiday_preferences.svg b/docs/images/graphs/dep_graph_app_core_holiday_preferences.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg b/docs/images/graphs/dep_graph_app_core_holiday_preferences_datastore.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_holiday_preferences_memory.svg b/docs/images/graphs/dep_graph_app_core_holiday_preferences_memory.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_holiday_service.svg b/docs/images/graphs/dep_graph_app_core_holiday_service.svg new file mode 100644 index 00000000..4c38263a --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_holiday_service.svg @@ -0,0 +1,17 @@ + + + + + + :app:core:holiday-service + + + + :app:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_core_model.svg b/docs/images/graphs/dep_graph_app_core_model.svg new file mode 100644 index 00000000..9a93db0f --- /dev/null +++ b/docs/images/graphs/dep_graph_app_core_model.svg @@ -0,0 +1,9 @@ + + + + + + :app:core:model + + + diff --git a/docs/images/graphs/dep_graph_app_core_navigation.svg b/docs/images/graphs/dep_graph_app_core_navigation.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_core_resources.svg b/docs/images/graphs/dep_graph_app_core_resources.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_app_data_account.svg b/docs/images/graphs/dep_graph_app_data_account.svg new file mode 100644 index 00000000..47240bdd --- /dev/null +++ b/docs/images/graphs/dep_graph_app_data_account.svg @@ -0,0 +1,101 @@ + + + + + + :app:data:account + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:account-preferences + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:account + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_backup.svg b/docs/images/graphs/dep_graph_app_data_backup.svg new file mode 100644 index 00000000..0f8d0f10 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_data_backup.svg @@ -0,0 +1,137 @@ + + + + + + :app:data:backup + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-database + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:backup + + + + + + + + :app:core:model + + + + + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_fetch.svg b/docs/images/graphs/dep_graph_app_data_fetch.svg new file mode 100644 index 00000000..0ebf197d --- /dev/null +++ b/docs/images/graphs/dep_graph_app_data_fetch.svg @@ -0,0 +1,137 @@ + + + + + + :app:data:fetch + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-database + + + + + + + + :app:core:diary-service + + + + + + + + :app:domain:fetch + + + + + + + + :app:core:model + + + + + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_holiday.svg b/docs/images/graphs/dep_graph_app_data_holiday.svg new file mode 100644 index 00000000..fd7f47c1 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_data_holiday.svg @@ -0,0 +1,97 @@ + + + + + + :app:data:holiday + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:holiday-preferences + + + + + + + + :app:core:holiday-database + + + + + + + + :app:core:holiday-service + + + + + + + + :app:domain:holiday + + + + + + + + :app:core:model + + + + + + + + + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_data_memo.svg b/docs/images/graphs/dep_graph_app_data_memo.svg new file mode 100644 index 00000000..ea7b43c2 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_data_memo.svg @@ -0,0 +1,105 @@ + + + + + + :app:data:memo + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :app:core:diary-database + + + + + + + + :app:domain:memo + + + + + + + + :app:core:model + + + + + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_account.svg b/docs/images/graphs/dep_graph_app_domain_account.svg new file mode 100644 index 00000000..f80a0c6d --- /dev/null +++ b/docs/images/graphs/dep_graph_app_domain_account.svg @@ -0,0 +1,49 @@ + + + + + + :app:domain:account + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_backup.svg b/docs/images/graphs/dep_graph_app_domain_backup.svg new file mode 100644 index 00000000..5ba19d9b --- /dev/null +++ b/docs/images/graphs/dep_graph_app_domain_backup.svg @@ -0,0 +1,77 @@ + + + + + + :app:domain:backup + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_fetch.svg b/docs/images/graphs/dep_graph_app_domain_fetch.svg new file mode 100644 index 00000000..eb1cc794 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_domain_fetch.svg @@ -0,0 +1,77 @@ + + + + + + :app:domain:fetch + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_holiday.svg b/docs/images/graphs/dep_graph_app_domain_holiday.svg new file mode 100644 index 00000000..dec9f8bb --- /dev/null +++ b/docs/images/graphs/dep_graph_app_domain_holiday.svg @@ -0,0 +1,49 @@ + + + + + + :app:domain:holiday + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_domain_memo.svg b/docs/images/graphs/dep_graph_app_domain_memo.svg new file mode 100644 index 00000000..8a6a5f87 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_domain_memo.svg @@ -0,0 +1,77 @@ + + + + + + :app:domain:memo + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:kotlin + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_feature_account.svg b/docs/images/graphs/dep_graph_app_feature_account.svg new file mode 100644 index 00000000..868ada75 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_feature_account.svg @@ -0,0 +1,141 @@ + + + + + + :app:feature:account + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_feature_calendar.svg b/docs/images/graphs/dep_graph_app_feature_calendar.svg new file mode 100644 index 00000000..8b658069 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_feature_calendar.svg @@ -0,0 +1,221 @@ + + + + + + :app:feature:calendar + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:core:calendar-compose + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:holiday + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_feature_memo.svg b/docs/images/graphs/dep_graph_app_feature_memo.svg new file mode 100644 index 00000000..ac3ada79 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_feature_memo.svg @@ -0,0 +1,169 @@ + + + + + + :app:feature:memo + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:domain:memo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_feature_more.svg b/docs/images/graphs/dep_graph_app_feature_more.svg new file mode 100644 index 00000000..a44a8adf --- /dev/null +++ b/docs/images/graphs/dep_graph_app_feature_more.svg @@ -0,0 +1,141 @@ + + + + + + :app:feature:more + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:domain:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_platform_android.svg b/docs/images/graphs/dep_graph_app_platform_android.svg new file mode 100644 index 00000000..facaec76 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_platform_android.svg @@ -0,0 +1,813 @@ + + + + + + :app:platform:android + + + + :app:platform:common + + + + + + + + :app:core:diary-database-room + + + + + + + + :app:core:diary-service + + + + + + + + :app:core:account-preferences-datastore + + + + + + + + :app:core:holiday-preferences-datastore + + + + + + + + :app:core:holiday-database-room + + + + + + + + :app:core:holiday-service + + + + + + + + + + + + + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:data:memo + + + + + + + + :app:data:account + + + + + + + + :app:data:holiday + + + + + + + + :app:data:backup + + + + + + + + :app:data:fetch + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:account + + + + + + + + :app:domain:holiday + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:fetch + + + + + + + + :app:core:coroutines + + + + + + + + :app:feature:memo + + + + + + + + :app:feature:calendar + + + + + + + + :app:feature:more + + + + + + + + :app:feature:account + + + + + + + + + + + + :library:koin-room + + + + + + + + :app:core:diary-database + + + + + + + + :library:room + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + :library:koin-datastore + + + + + + + + + + + + + + + + :app:core:holiday-preferences + + + + + + + + + + + + + + + + + + + + :app:core:holiday-database + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:calendar-compose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_platform_common.svg b/docs/images/graphs/dep_graph_app_platform_common.svg new file mode 100644 index 00000000..8237d640 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_platform_common.svg @@ -0,0 +1,697 @@ + + + + + + :app:platform:common + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:data:memo + + + + + + + + :app:data:account + + + + + + + + :app:data:holiday + + + + + + + + :app:data:backup + + + + + + + + :app:data:fetch + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:account + + + + + + + + :app:domain:holiday + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:fetch + + + + + + + + :app:core:coroutines + + + + + + + + :app:core:diary-service + + + + + + + + :app:core:holiday-service + + + + + + + + :app:feature:memo + + + + + + + + :app:feature:calendar + + + + + + + + :app:feature:more + + + + + + + + :app:feature:account + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:diary-database + + + + + + + + + + + + + + + + + + + + + + + + :app:core:account-preferences + + + + + + + + + + + + + + + + + + + + + + + + :app:core:holiday-preferences + + + + + + + + :app:core:holiday-database + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:calendar-compose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_platform_ios.svg b/docs/images/graphs/dep_graph_app_platform_ios.svg new file mode 100644 index 00000000..62f86817 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_platform_ios.svg @@ -0,0 +1,817 @@ + + + + + + :app:platform:ios + + + + :app:platform:common + + + + + + + + :app:core:coroutines + + + + + + + + :app:core:diary-database-room + + + + + + + + :app:core:diary-service + + + + + + + + :app:core:account-preferences-datastore + + + + + + + + :app:core:holiday-preferences-datastore + + + + + + + + :app:core:holiday-database-room + + + + + + + + :app:core:holiday-service + + + + + + + + + + + + + + + + + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:data:memo + + + + + + + + :app:data:account + + + + + + + + :app:data:holiday + + + + + + + + :app:data:backup + + + + + + + + :app:data:fetch + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:account + + + + + + + + :app:domain:holiday + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:fetch + + + + + + + + :app:feature:memo + + + + + + + + :app:feature:calendar + + + + + + + + :app:feature:more + + + + + + + + :app:feature:account + + + + + + + + + + + + :library:koin-room + + + + + + + + :app:core:diary-database + + + + + + + + :library:room + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + :library:koin-datastore + + + + + + + + + + + + + + + + :app:core:holiday-preferences + + + + + + + + + + + + + + + + + + + + :app:core:holiday-database + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:calendar-compose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_platform_jvm.svg b/docs/images/graphs/dep_graph_app_platform_jvm.svg new file mode 100644 index 00000000..9ea0b875 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_platform_jvm.svg @@ -0,0 +1,825 @@ + + + + + + :app:platform:jvm + + + + :app:platform:common + + + + + + + + :app:core:coroutines + + + + + + + + :app:core:diary-database-room + + + + + + + + :app:core:diary-service + + + + + + + + :app:core:account-preferences-datastore + + + + + + + + :app:core:holiday-preferences-datastore + + + + + + + + :app:core:holiday-database-room + + + + + + + + :app:core:holiday-service + + + + + + + + :library:koin-room + + + + + + + + :library:koin-datastore + + + + + + + + + + + + + + + + + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:data:memo + + + + + + + + :app:data:account + + + + + + + + :app:data:holiday + + + + + + + + :app:data:backup + + + + + + + + :app:data:fetch + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:account + + + + + + + + :app:domain:holiday + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:fetch + + + + + + + + :app:feature:memo + + + + + + + + :app:feature:calendar + + + + + + + + :app:feature:more + + + + + + + + :app:feature:account + + + + + + + + + + + + + + + + :app:core:diary-database + + + + + + + + :library:room + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + + + + + + + + + + + + + :app:core:holiday-preferences + + + + + + + + + + + + + + + + + + + + :app:core:holiday-database + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:calendar-compose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_app_platform_wasm.svg b/docs/images/graphs/dep_graph_app_platform_wasm.svg new file mode 100644 index 00000000..bf3df001 --- /dev/null +++ b/docs/images/graphs/dep_graph_app_platform_wasm.svg @@ -0,0 +1,773 @@ + + + + + + :app:platform:wasm + + + + :app:platform:common + + + + + + + + :app:core:coroutines + + + + + + + + :app:core:diary-database-memory + + + + + + + + :app:core:diary-service + + + + + + + + :app:core:account-preferences-memory + + + + + + + + :app:core:holiday-preferences-memory + + + + + + + + :app:core:holiday-database-memory + + + + + + + + :app:core:holiday-service + + + + + + + + + + + + + + + + + + + + :app:core:design-system + + + + + + + + :app:core:navigation + + + + + + + + :app:core:resources + + + + + + + + :library:color + + + + + + + + :library:kotlin + + + + + + + + :library:navigation + + + + + + + + :library:coroutines + + + + + + + + :library:datetime + + + + + + + + :library:shimmer-m3 + + + + + + + + :app:data:memo + + + + + + + + :app:data:account + + + + + + + + :app:data:holiday + + + + + + + + :app:data:backup + + + + + + + + :app:data:fetch + + + + + + + + :app:domain:memo + + + + + + + + :app:domain:account + + + + + + + + :app:domain:holiday + + + + + + + + :app:domain:backup + + + + + + + + :app:domain:fetch + + + + + + + + :app:feature:memo + + + + + + + + :app:feature:calendar + + + + + + + + :app:feature:more + + + + + + + + :app:feature:account + + + + + + + + + + + + + + + + :app:core:diary-database + + + + + + + + :app:core:model + + + + + + + + :common:exception + + + + + + + + :app:core:account-preferences + + + + + + + + :common:model + + + + + + + + + + + + :app:core:holiday-preferences + + + + + + + + :app:core:holiday-database + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :app:core:calendar-compose + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_common_exception.svg b/docs/images/graphs/dep_graph_common_exception.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_common_model.svg b/docs/images/graphs/dep_graph_common_model.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_color.svg b/docs/images/graphs/dep_graph_library_color.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_coroutines.svg b/docs/images/graphs/dep_graph_library_coroutines.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_datetime.svg b/docs/images/graphs/dep_graph_library_datetime.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_koin_datastore.svg b/docs/images/graphs/dep_graph_library_koin_datastore.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_koin_room.svg b/docs/images/graphs/dep_graph_library_koin_room.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_kotlin.svg b/docs/images/graphs/dep_graph_library_kotlin.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_navigation.svg b/docs/images/graphs/dep_graph_library_navigation.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_room.svg b/docs/images/graphs/dep_graph_library_room.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_library_shimmer_m3.svg b/docs/images/graphs/dep_graph_library_shimmer_m3.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_server_app.svg b/docs/images/graphs/dep_graph_server_app.svg new file mode 100644 index 00000000..c4cd11f8 --- /dev/null +++ b/docs/images/graphs/dep_graph_server_app.svg @@ -0,0 +1,189 @@ + + + + + + :server:app + + + + :server:core:database + + + + + + + + :server:data:account + + + + + + + + :server:data:memo + + + + + + + + :server:domain:account + + + + + + + + :server:domain:memo + + + + + + + + :server:feature:home + + + + + + + + :server:feature:account + + + + + + + + :server:feature:memo + + + + + + + + :common:model + + + + + + + + :server:core:model + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_core_database.svg b/docs/images/graphs/dep_graph_server_core_database.svg new file mode 100644 index 00000000..dfbf2f18 --- /dev/null +++ b/docs/images/graphs/dep_graph_server_core_database.svg @@ -0,0 +1,17 @@ + + + + + + :server:core:database + + + + :server:core:model + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_core_model.svg b/docs/images/graphs/dep_graph_server_core_model.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_server_data_account.svg b/docs/images/graphs/dep_graph_server_data_account.svg new file mode 100644 index 00000000..ae0ff61b --- /dev/null +++ b/docs/images/graphs/dep_graph_server_data_account.svg @@ -0,0 +1,57 @@ + + + + + + :server:data:account + + + + :server:core:model + + + + + + + + :server:core:database + + + + + + + + :server:domain:account + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_data_memo.svg b/docs/images/graphs/dep_graph_server_data_memo.svg new file mode 100644 index 00000000..2d17dc80 --- /dev/null +++ b/docs/images/graphs/dep_graph_server_data_memo.svg @@ -0,0 +1,57 @@ + + + + + + :server:data:memo + + + + :server:core:model + + + + + + + + :server:core:database + + + + + + + + :server:domain:memo + + + + + + + + + + + + + + + + :common:exception + + + + + + + + :library:kotlin + + + + + + + diff --git a/docs/images/graphs/dep_graph_server_domain_account.svg b/docs/images/graphs/dep_graph_server_domain_account.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_server_domain_memo.svg b/docs/images/graphs/dep_graph_server_domain_memo.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_server_feature_account.svg b/docs/images/graphs/dep_graph_server_feature_account.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_server_feature_home.svg b/docs/images/graphs/dep_graph_server_feature_home.svg new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/graphs/dep_graph_server_feature_memo.svg b/docs/images/graphs/dep_graph_server_feature_memo.svg new file mode 100644 index 00000000..e69de29b diff --git a/generateModuleGraph.sh b/generateModuleGraph.sh new file mode 100644 index 00000000..9828daa8 --- /dev/null +++ b/generateModuleGraph.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +if ! command -v dot &> /dev/null +then + echo "The 'dot' command is not found. This is required to generate SVGs from the Graphviz files." + echo "Installation instructions:" + echo " - On macOS: You can install Graphviz using Homebrew with the command: 'brew install graphviz'" + echo " - On Ubuntu: You can install Graphviz using APT with the command: 'sudo apt-get install graphviz'" + exit 1 +fi + +if ! command -v svgo &> /dev/null +then + echo "The 'svgo' command is not found. This is required to cleanup and compress SVGs." + echo "Installation instructions available at https://github.com/svg/svgo." + exit 1 +fi + +if grep -P "" /dev/null > /dev/null 2>&1; then + GREP_COMMAND=grep +elif command -v ggrep &> /dev/null; then + GREP_COMMAND=ggrep +else + echo "You don't have a version of 'grep' installed which supports Perl regular expressions." + echo "On MacOS you can install one using Homebrew with the command: 'brew install grep'" + exit 1 +fi + +module_paths=$(${GREP_COMMAND} -oP 'include\("\K[^"]+' settings.gradle.kts) + +echo "$module_paths" | while read -r module_path; +do + echo "run $module_path" + + file_name="dep_graph${module_path//:/_}" + file_name="${file_name//-/_}" + echo "file $file_name" + + path="${module_path:1}" + path=${path//:/\/} + readme_path="${path}/README.md" + echo "readme $readme_path" + + relative_image_path="../" + depth=$(awk -F: '{print NF-1}' <<< "${module_path}") + for ((i=1; i<$depth; i++)); do + relative_image_path+="../" + done + relative_image_path+="docs/images/graphs/${file_name}.svg" + echo "image path $relative_image_path" + + echo "# ${module_path} module" > "$readme_path" + echo "## Dependency graph" >> "$readme_path" + echo "![Dependency graph](${relative_image_path})" >> "$readme_path" + + ./gradlew "$module_path:generateModulesGraphvizText" -Pmodules.graph.output.gv="build/${file_name}.gv" --no-build-cache --no-configuration-cache + + dot -Tsvg "build/${file_name}.gv" | + svgo --multipass --pretty --output="docs/images/graphs/${file_name}.svg" - + +# rm "${file_name}.gv" +done \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..be568185 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +org.gradle.caching=true +org.gradle.configuration-cache=true +org.gradle.jvmargs=-Xms8G -Xmx8G -Dfile.encoding=UTF-8 +org.gradle.parallel=true + +kotlin.code.style=official +kotlin.native.ignoreDisabledTargets=true + +android.useAndroidX=true +android.nonTransitiveRClass=true + +buildkonfig.flavor=dev \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..7c8223c6 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,170 @@ +[versions] +### core +kotlin = "2.0.21" # https://github.com/jetbrains/kotlin/releases +agp = "8.6.1" # https://developer.android.com/build/releases/gradle-plugin?hl=en +ktor = "3.0.1" # https://github.com/ktorio/ktor/releases +ksp = "2.0.21-1.0.26" # https://github.com/google/ksp/releases + +kotlinx-serialization = "1.7.3" # https://github.com/Kotlin/kotlinx.serialization/releases +kotlinx-coroutines = "1.9.0" # https://github.com/Kotlin/kotlinx.coroutines/releases +kotlinx-datetime = "0.6.1" # https://github.com/Kotlin/kotlinx-datetime/releases + +### multiplatform +compose = "1.7.0" # https://github.com/JetBrains/compose-multiplatform/releases +compose-material3-adaptive = "1.0.0" +navigation = "2.8.0-alpha10" +lifecycle = "2.8.3" +androidx-lifecycle = "2.8.5" + +compose-markdown = "0.27.0" # https://github.com/mikepenz/multiplatform-markdown-renderer/releases + +koin = "4.0.0" # https://github.com/InsertKoinIO/koin/releases +koin-annotations = "2.0.0-Beta1" # https://github.com/InsertKoinIO/koin-annotations/releases +datastore = "1.1.1" # https://developer.android.com/jetpack/androidx/releases/datastore?hl=en +room = "2.7.0-alpha11" # https://developer.android.com/jetpack/androidx/releases/room?hl=en +sqlite = "2.5.0-alpha11" # https://developer.android.com/jetpack/androidx/releases/sqlite?hl=en + +### android +android-material = "1.12.0" # https://github.com/material-components/material-components-android/releases +androidx-activity = "1.9.3" # https://developer.android.com/jetpack/androidx/releases/activity?hl=en +androidx-startup = "1.2.0" # https://developer.android.com/jetpack/androidx/releases/startup?hl=en + +leakcanary = "2.14" # https://github.com/square/leakcanary/releases + +### server +exposed = "0.56.0" # https://github.com/JetBrains/Exposed/releases +mysql = "8.0.33" + +logback = "1.5.12" # https://github.com/qos-ch/logback/tags + +### plugin +dependency-guard = "0.5.0" # https://github.com/dropbox/dependency-guard/releases +spotless = "7.0.0.BETA4" # https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md +module-graph = "2.7.1" # https://github.com/jraska/modules-graph-assert/releases +buildkonfig = "0.15.2" # https://github.com/yshrsmz/BuildKonfig/releases + +[libraries] +### gradle +gradle-kotlin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } +gradle-ksp = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } +gradle-android = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +gradle-compose = { group = "org.jetbrains.compose", name = "compose-gradle-plugin", version.ref = "compose" } +gradle-compose-compiler = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" } +gradle-room = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" } + +### core +kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinx-serialization" } + +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } + +kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } + +### multiplatform +compose-material3-adaptive = { group = "org.jetbrains.compose.material3.adaptive", name = "adaptive", version.ref = "compose-material3-adaptive" } +compose-material3-adaptive-navigation = { group = "org.jetbrains.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "compose-material3-adaptive" } +compose-markdown = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-m3", version.ref = "compose-markdown" } + +navigation-common = { group = "org.jetbrains.androidx.navigation", name = "navigation-common", version.ref = "navigation" } +navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "navigation" } + +lifecycle-common = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-common", version.ref = "lifecycle" } +lifecycle-runtime = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycle" } +lifecycle-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } + +datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } + +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite" } + +koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin" } +koin-core = { group = "io.insert-koin", name = "koin-core" } +koin-android = { group = "io.insert-koin", name = "koin-android" } +koin-ktor = { group = "io.insert-koin", name = "koin-ktor" } +koin-compose-viewmodel-navigation = { group = "io.insert-koin", name = "koin-compose-viewmodel-navigation" } + +koin-annotations-bom = { group = "io.insert-koin", name = "koin-annotations-bom", version.ref = "koin-annotations" } +koin-annotations = { group = "io.insert-koin", name = "koin-annotations" } +koin-compiler = { group = "io.insert-koin", name = "koin-ksp-compiler" } + +### android +android-material = { group = "com.google.android.material", name = "material", version.ref = "android-material" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" } +androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx-startup" } +androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidx-lifecycle" } + +leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } + +### ktor +ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-client-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } +ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktor" } +ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktor" } +ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } +ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" } +ktor-server-config-yaml = { group = "io.ktor", name = "ktor-server-config-yaml", version.ref = "ktor" } +ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" } +ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref = "ktor" } +ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = "ktor" } +ktor-server-auth-jwt = { group = "io.ktor", name = "ktor-server-auth-jwt", version.ref = "ktor" } +ktor-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } + +### server +exposed-bom = { group = "org.jetbrains.exposed", name = "exposed-bom", version.ref = "exposed" } +exposed-core = { group = "org.jetbrains.exposed", name = "exposed-core" } +exposed-datetime = { group = "org.jetbrains.exposed", name = "exposed-kotlin-datetime" } +exposed-jdbc = { group = "org.jetbrains.exposed", name = "exposed-jdbc" } +mysql-connector = { group = "mysql", name = "mysql-connector-java", version.ref = "mysql" } + +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } + +[plugins] +### core +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } + +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } + +### multiplatform +compose = { id = "org.jetbrains.compose", version.ref = "compose" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + +room = { id = "androidx.room", version.ref = "room" } + +### android +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } + +### server +ktor-server = { id = "io.ktor.plugin", version.ref = "ktor" } + +### plugin +dependency-guard = { id = "com.dropbox.dependency-guard", version.ref = "dependency-guard" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "module-graph" } +buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" } + +[bundles] +ktor-client = [ + "ktor-client-core", + "ktor-client-negotiation", + "ktor-kotlinx-json" +] + +room = [ + "room-runtime", + "sqlite-bundled" +] + +ktor-server = [ + "ktor-server-netty", + "ktor-server-config-yaml", + "ktor-server-content-negotiation", + "ktor-server-cors", + "ktor-kotlinx-json", + "ktor-server-auth-jwt" +] \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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