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..b6d08273 --- /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 00000000..0f5b1440 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/1024.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..d9f4666f Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/114.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..dd11857d Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/114.png differ 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 00000000..d75dee65 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/120.png differ 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 00000000..21e955a4 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/144.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 00000000..78a67ea6 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/152.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 00000000..771922cd Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/152.png differ 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 00000000..b6137a26 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/16.png differ 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 00000000..b611623f Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/180.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..141487a4 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/180.png differ 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 00000000..a19d2838 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/256.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 00000000..69a26cf1 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/256.png differ 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 00000000..54354239 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/29.png differ 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 00000000..5ea27971 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/40.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..f7e7e000 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/40.png differ 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 00000000..7e8c3a1c Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/512.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 00000000..882ab296 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/512.png differ 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 00000000..c9d62a12 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/58.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..f3270c75 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/58.png differ 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 00000000..43dfc8c8 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/64.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 00000000..4ffbfcf4 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/64.png differ 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 00000000..2ab33e56 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/76.png b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 00000000..fda9ccdf Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/76.png differ 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 00000000..c4572eb6 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/80.png differ 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 00000000..0f6478a7 Binary files /dev/null and b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Diary/Diary/Assets.xcassets/AppIcon.appiconset/Contents.json b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..5c82812f --- /dev/null +++ b/Diary/Diary/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/Diary/Diary/Assets.xcassets/Contents.json b/Diary/Diary/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Diary/Diary/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Diary/Diary/ContentView.swift b/Diary/Diary/ContentView.swift new file mode 100644 index 00000000..04ff190d --- /dev/null +++ b/Diary/Diary/ContentView.swift @@ -0,0 +1,12 @@ +import SwiftUI +import Kotlin + +struct ContentView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> 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 00000000..d9f4666f Binary files /dev/null and b/Diary/Diary/appstore.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..d6e4248e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Diary Kotlin Multiplatform \ No newline at end of file diff --git a/app/core/account-preferences-datastore/README.md b/app/core/account-preferences-datastore/README.md new file mode 100644 index 00000000..7d34e177 --- /dev/null +++ b/app/core/account-preferences-datastore/README.md @@ -0,0 +1,3 @@ +# :app:core:account-preferences-datastore module +## Dependency graph +![Dependency graph](../../../docs/images/graphs/dep_graph_app_core_account_preferences_datastore.svg) diff --git a/app/core/account-preferences-datastore/build.gradle.kts b/app/core/account-preferences-datastore/build.gradle.kts new file mode 100644 index 00000000..f9e90ace --- /dev/null +++ b/app/core/account-preferences-datastore/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("diary.datastore") + id("diary.koin.datastore") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":app:core:account-preferences")) + } + } + } +} + +android { + namespace = "${Build.NAMESPACE}.core.account.preferences.datastore" +} diff --git a/app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferences.kt b/app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferences.kt new file mode 100644 index 00000000..fb14f062 --- /dev/null +++ b/app/core/account-preferences-datastore/src/commonMain/kotlin/io/github/taetae98coding/diary/core/account/preferences/datastore/AccountDataStorePreferences.kt @@ -0,0 +1,47 @@ +package io.github.taetae98coding.diary.core.account.preferences.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import io.github.taetae98coding.diary.core.account.preferences.AccountPreferences +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest + +@OptIn(ExperimentalCoroutinesApi::class) +internal class AccountDataStorePreferences(private val dataStore: DataStore, ) : 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 00000000..2cd6779e Binary files /dev/null and b/app/platform/android/keystore/dev.jks differ diff --git a/app/platform/android/keystore/real.jks b/app/platform/android/keystore/real.jks new file mode 100644 index 00000000..2cd6779e Binary files /dev/null and b/app/platform/android/keystore/real.jks differ diff --git a/app/platform/android/src/main/AndroidManifest.xml b/app/platform/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b76fae4c --- /dev/null +++ b/app/platform/android/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + \ 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 00000000..06287b97 Binary files /dev/null and b/app/platform/android/src/main/ic_launcher-playstore.png differ diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt new file mode 100644 index 00000000..06f2728f --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryActivity.kt @@ -0,0 +1,17 @@ +package io.github.taetae98coding.diary + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import io.github.taetae98coding.diary.app.App + +public class DiaryActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + App() + } + } +} diff --git a/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt new file mode 100644 index 00000000..2d2af98d --- /dev/null +++ b/app/platform/android/src/main/kotlin/io/github/taetae98coding/diary/DiaryApplication.kt @@ -0,0 +1,29 @@ +package io.github.taetae98coding.diary + +import android.app.Application +import androidx.lifecycle.LifecycleOwner +import io.github.taetae98coding.diary.app.manager.BackupManager +import io.github.taetae98coding.diary.app.manager.FetchManager +import org.koin.android.ext.android.get + +public class DiaryApplication : Application() { + override fun onCreate() { + super.onCreate() + initBackupManager() + initFetchManager() + } + + private fun initBackupManager() { + val appLifecycleOwner = get() + 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 00000000..2cccc8f3 Binary files /dev/null and b/app/platform/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ 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 00000000..f95c40b6 Binary files /dev/null and b/app/platform/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/platform/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/platform/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..a42f3a77 Binary files /dev/null and b/app/platform/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/platform/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/platform/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..c82923d4 Binary files /dev/null and b/app/platform/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ 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 00000000..384ed3ba Binary files /dev/null and b/app/platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ 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 00000000..8464aaec Binary files /dev/null and b/asset/icon/app_icon_mac.icns differ diff --git a/asset/icon/app_icon_round.png b/asset/icon/app_icon_round.png new file mode 100644 index 00000000..0b2845aa Binary files /dev/null and b/asset/icon/app_icon_round.png differ diff --git a/asset/icon/app_icon_square.png b/asset/icon/app_icon_square.png new file mode 100644 index 00000000..b3ab7c7f Binary files /dev/null and b/asset/icon/app_icon_square.png differ 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 00000000..e708b1c0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ 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")