├── .github └── workflows │ └── static.yml ├── .gitignore ├── .swiftformat ├── App ├── TaskTree.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── ryu.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── xcschemes │ │ └── TaskTree.xcscheme └── iOS │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon.png │ │ └── Contents.json │ └── Contents.json │ ├── Info.plist │ ├── InfoPlist.xcstrings │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── TaskTree.entitlements │ └── TaskTreeApp.swift ├── LICENSE ├── Makefile ├── Mintfile ├── Packages └── TaskTree │ ├── .gitignore │ ├── Package.resolved │ ├── Package.swift │ ├── PackageDependencies.md │ ├── Sources │ ├── AppFeature │ │ └── AppView.swift │ ├── SettingFeature │ │ ├── LicensesView.swift │ │ ├── Resources │ │ │ └── Localizable.xcstrings │ │ └── Setting.swift │ ├── SwiftDataModel │ │ └── Todo.swift │ ├── SwiftDataUtils │ │ └── ModelContext+init.swift │ ├── TaskTreeFeature │ │ ├── Resources │ │ │ └── Localizable.xcstrings │ │ └── TaskTree.swift │ ├── TodoClient │ │ ├── Resources │ │ │ └── Localizable.xcstrings │ │ └── TodoClient.swift │ └── Utils │ │ ├── AlertState+error.swift │ │ ├── NavigationLink+empty.swift │ │ ├── Resources │ │ └── Localizable.xcstrings │ │ └── View+alert.swift │ └── Tests │ └── TaskTreeTests │ └── TaskTreeTests.swift ├── README.md ├── TaskTree.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved └── docs └── privacy-policy └── index.html /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v4 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | .DS_Store 3 | App/TaskTree.xcodeproj/xcuserdata/ 4 | TaskTree.xcworkspace/xcuserdata/ 5 | App/.DS_Store 6 | Packages/TaskTree/Sources/Generated/Licenses.swift 7 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.9 2 | 3 | --commas inline 4 | --disable redundantVoidReturnType,blankLinesAroundMark 5 | --disable redundantLet 6 | --disable preferForLoop 7 | -------------------------------------------------------------------------------- /App/TaskTree.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 429D47422B52E1F4009A257B /* SettingFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 429D47412B52E1F4009A257B /* SettingFeature */; }; 11 | 42DBDFBB2B52337C002F555D /* TaskTreeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DBDFB32B52337C002F555D /* TaskTreeApp.swift */; }; 12 | 42DBDFBD2B52337C002F555D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42DBDFB52B52337C002F555D /* Assets.xcassets */; }; 13 | 42DBDFBE2B52337C002F555D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42DBDFB72B52337C002F555D /* Preview Assets.xcassets */; }; 14 | 42DBDFC32B524F38002F555D /* AppFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 42DBDFC22B524F38002F555D /* AppFeature */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 42DBDF9D2B52314B002F555D /* TaskTree.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TaskTree.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 42DBDFB32B52337C002F555D /* TaskTreeApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskTreeApp.swift; sourceTree = ""; }; 20 | 42DBDFB52B52337C002F555D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 21 | 42DBDFB72B52337C002F555D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 22 | 42DBDFB92B52337C002F555D /* TaskTree.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = TaskTree.entitlements; sourceTree = ""; }; 23 | 42DBDFBA2B52337C002F555D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | 42DBDFC42B52A73A002F555D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 42DBDF9A2B52314B002F555D /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | 429D47422B52E1F4009A257B /* SettingFeature in Frameworks */, 33 | 42DBDFC32B524F38002F555D /* AppFeature in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 42DBDF942B52314B002F555D = { 41 | isa = PBXGroup; 42 | children = ( 43 | 42DBDFB22B52337C002F555D /* iOS */, 44 | 42DBDF9E2B52314B002F555D /* Products */, 45 | 42DBDFC12B524F38002F555D /* Frameworks */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 42DBDF9E2B52314B002F555D /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 42DBDF9D2B52314B002F555D /* TaskTree.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 42DBDFB22B52337C002F555D /* iOS */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 42DBDFB32B52337C002F555D /* TaskTreeApp.swift */, 61 | 42DBDFB52B52337C002F555D /* Assets.xcassets */, 62 | 42DBDFB62B52337C002F555D /* Preview Content */, 63 | 42DBDFB92B52337C002F555D /* TaskTree.entitlements */, 64 | 42DBDFBA2B52337C002F555D /* Info.plist */, 65 | 42DBDFC42B52A73A002F555D /* InfoPlist.xcstrings */, 66 | ); 67 | path = iOS; 68 | sourceTree = ""; 69 | }; 70 | 42DBDFB62B52337C002F555D /* Preview Content */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 42DBDFB72B52337C002F555D /* Preview Assets.xcassets */, 74 | ); 75 | path = "Preview Content"; 76 | sourceTree = ""; 77 | }; 78 | 42DBDFC12B524F38002F555D /* Frameworks */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | ); 82 | name = Frameworks; 83 | sourceTree = ""; 84 | }; 85 | /* End PBXGroup section */ 86 | 87 | /* Begin PBXNativeTarget section */ 88 | 42DBDF9C2B52314B002F555D /* TaskTree */ = { 89 | isa = PBXNativeTarget; 90 | buildConfigurationList = 42DBDFAF2B52314D002F555D /* Build configuration list for PBXNativeTarget "TaskTree" */; 91 | buildPhases = ( 92 | 42DBDF992B52314B002F555D /* Sources */, 93 | 42DBDF9A2B52314B002F555D /* Frameworks */, 94 | 42DBDF9B2B52314B002F555D /* Resources */, 95 | ); 96 | buildRules = ( 97 | ); 98 | dependencies = ( 99 | ); 100 | name = TaskTree; 101 | packageProductDependencies = ( 102 | 42DBDFC22B524F38002F555D /* AppFeature */, 103 | 429D47412B52E1F4009A257B /* SettingFeature */, 104 | ); 105 | productName = TaskTree; 106 | productReference = 42DBDF9D2B52314B002F555D /* TaskTree.app */; 107 | productType = "com.apple.product-type.application"; 108 | }; 109 | /* End PBXNativeTarget section */ 110 | 111 | /* Begin PBXProject section */ 112 | 42DBDF952B52314B002F555D /* Project object */ = { 113 | isa = PBXProject; 114 | attributes = { 115 | BuildIndependentTargetsInParallel = 1; 116 | LastSwiftUpdateCheck = 1500; 117 | LastUpgradeCheck = 1500; 118 | TargetAttributes = { 119 | 42DBDF9C2B52314B002F555D = { 120 | CreatedOnToolsVersion = 15.0; 121 | }; 122 | }; 123 | }; 124 | buildConfigurationList = 42DBDF982B52314B002F555D /* Build configuration list for PBXProject "TaskTree" */; 125 | compatibilityVersion = "Xcode 14.0"; 126 | developmentRegion = ja; 127 | hasScannedForEncodings = 0; 128 | knownRegions = ( 129 | en, 130 | Base, 131 | ja, 132 | ); 133 | mainGroup = 42DBDF942B52314B002F555D; 134 | productRefGroup = 42DBDF9E2B52314B002F555D /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 42DBDF9C2B52314B002F555D /* TaskTree */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | 42DBDF9B2B52314B002F555D /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 42DBDFBE2B52337C002F555D /* Preview Assets.xcassets in Resources */, 149 | 42DBDFBD2B52337C002F555D /* Assets.xcassets in Resources */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXResourcesBuildPhase section */ 154 | 155 | /* Begin PBXSourcesBuildPhase section */ 156 | 42DBDF992B52314B002F555D /* Sources */ = { 157 | isa = PBXSourcesBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | 42DBDFBB2B52337C002F555D /* TaskTreeApp.swift in Sources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXSourcesBuildPhase section */ 165 | 166 | /* Begin XCBuildConfiguration section */ 167 | 42DBDFAD2B52314D002F555D /* Debug */ = { 168 | isa = XCBuildConfiguration; 169 | buildSettings = { 170 | ALWAYS_SEARCH_USER_PATHS = NO; 171 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 172 | CLANG_ANALYZER_NONNULL = YES; 173 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 174 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 175 | CLANG_ENABLE_MODULES = YES; 176 | CLANG_ENABLE_OBJC_ARC = YES; 177 | CLANG_ENABLE_OBJC_WEAK = YES; 178 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 179 | CLANG_WARN_BOOL_CONVERSION = YES; 180 | CLANG_WARN_COMMA = YES; 181 | CLANG_WARN_CONSTANT_CONVERSION = YES; 182 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 183 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 184 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 185 | CLANG_WARN_EMPTY_BODY = YES; 186 | CLANG_WARN_ENUM_CONVERSION = YES; 187 | CLANG_WARN_INFINITE_RECURSION = YES; 188 | CLANG_WARN_INT_CONVERSION = YES; 189 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 190 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 191 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 193 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 194 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 195 | CLANG_WARN_STRICT_PROTOTYPES = YES; 196 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 197 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 198 | CLANG_WARN_UNREACHABLE_CODE = YES; 199 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 200 | COPY_PHASE_STRIP = NO; 201 | DEBUG_INFORMATION_FORMAT = dwarf; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | ENABLE_TESTABILITY = YES; 204 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 205 | GCC_C_LANGUAGE_STANDARD = gnu17; 206 | GCC_DYNAMIC_NO_PIC = NO; 207 | GCC_NO_COMMON_BLOCKS = YES; 208 | GCC_OPTIMIZATION_LEVEL = 0; 209 | GCC_PREPROCESSOR_DEFINITIONS = ( 210 | "DEBUG=1", 211 | "$(inherited)", 212 | ); 213 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 214 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 215 | GCC_WARN_UNDECLARED_SELECTOR = YES; 216 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 217 | GCC_WARN_UNUSED_FUNCTION = YES; 218 | GCC_WARN_UNUSED_VARIABLE = YES; 219 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 220 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 221 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 222 | MTL_FAST_MATH = YES; 223 | ONLY_ACTIVE_ARCH = YES; 224 | SDKROOT = iphoneos; 225 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 226 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 227 | }; 228 | name = Debug; 229 | }; 230 | 42DBDFAE2B52314D002F555D /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 235 | CLANG_ANALYZER_NONNULL = YES; 236 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu17; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 277 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 278 | MTL_ENABLE_DEBUG_INFO = NO; 279 | MTL_FAST_MATH = YES; 280 | SDKROOT = iphoneos; 281 | SWIFT_COMPILATION_MODE = wholemodule; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Release; 285 | }; 286 | 42DBDFB02B52314D002F555D /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 291 | CODE_SIGN_ENTITLEMENTS = iOS/TaskTree.entitlements; 292 | CODE_SIGN_STYLE = Automatic; 293 | CURRENT_PROJECT_VERSION = 1; 294 | DEVELOPMENT_ASSET_PATHS = "\"iOS/Preview Content\""; 295 | DEVELOPMENT_TEAM = G8RH83B4LT; 296 | ENABLE_PREVIEWS = YES; 297 | GENERATE_INFOPLIST_FILE = YES; 298 | INFOPLIST_FILE = iOS/Info.plist; 299 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 300 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 301 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 304 | LD_RUNPATH_SEARCH_PATHS = ( 305 | "$(inherited)", 306 | "@executable_path/Frameworks", 307 | ); 308 | MARKETING_VERSION = 1.0; 309 | PRODUCT_BUNDLE_IDENTIFIER = com.ryu.TaskTree; 310 | PRODUCT_NAME = "$(TARGET_NAME)"; 311 | SWIFT_EMIT_LOC_STRINGS = YES; 312 | SWIFT_VERSION = 5.0; 313 | TARGETED_DEVICE_FAMILY = "1,2"; 314 | }; 315 | name = Debug; 316 | }; 317 | 42DBDFB12B52314D002F555D /* Release */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 321 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 322 | CODE_SIGN_ENTITLEMENTS = iOS/TaskTree.entitlements; 323 | CODE_SIGN_STYLE = Automatic; 324 | CURRENT_PROJECT_VERSION = 1; 325 | DEVELOPMENT_ASSET_PATHS = "\"iOS/Preview Content\""; 326 | DEVELOPMENT_TEAM = G8RH83B4LT; 327 | ENABLE_PREVIEWS = YES; 328 | GENERATE_INFOPLIST_FILE = YES; 329 | INFOPLIST_FILE = iOS/Info.plist; 330 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 331 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 332 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 333 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 334 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 335 | LD_RUNPATH_SEARCH_PATHS = ( 336 | "$(inherited)", 337 | "@executable_path/Frameworks", 338 | ); 339 | MARKETING_VERSION = 1.0; 340 | PRODUCT_BUNDLE_IDENTIFIER = com.ryu.TaskTree; 341 | PRODUCT_NAME = "$(TARGET_NAME)"; 342 | SWIFT_EMIT_LOC_STRINGS = YES; 343 | SWIFT_VERSION = 5.0; 344 | TARGETED_DEVICE_FAMILY = "1,2"; 345 | }; 346 | name = Release; 347 | }; 348 | /* End XCBuildConfiguration section */ 349 | 350 | /* Begin XCConfigurationList section */ 351 | 42DBDF982B52314B002F555D /* Build configuration list for PBXProject "TaskTree" */ = { 352 | isa = XCConfigurationList; 353 | buildConfigurations = ( 354 | 42DBDFAD2B52314D002F555D /* Debug */, 355 | 42DBDFAE2B52314D002F555D /* Release */, 356 | ); 357 | defaultConfigurationIsVisible = 0; 358 | defaultConfigurationName = Release; 359 | }; 360 | 42DBDFAF2B52314D002F555D /* Build configuration list for PBXNativeTarget "TaskTree" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | 42DBDFB02B52314D002F555D /* Debug */, 364 | 42DBDFB12B52314D002F555D /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | /* End XCConfigurationList section */ 370 | 371 | /* Begin XCSwiftPackageProductDependency section */ 372 | 429D47412B52E1F4009A257B /* SettingFeature */ = { 373 | isa = XCSwiftPackageProductDependency; 374 | productName = SettingFeature; 375 | }; 376 | 42DBDFC22B524F38002F555D /* AppFeature */ = { 377 | isa = XCSwiftPackageProductDependency; 378 | productName = AppFeature; 379 | }; 380 | /* End XCSwiftPackageProductDependency section */ 381 | }; 382 | rootObject = 42DBDF952B52314B002F555D /* Project object */; 383 | } 384 | -------------------------------------------------------------------------------- /App/TaskTree.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /App/TaskTree.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /App/TaskTree.xcodeproj/project.xcworkspace/xcuserdata/ryu.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryu0118/TaskTree/90aa38e1c92c7ba0b2a4731a8570c995b1b617d7/App/TaskTree.xcodeproj/project.xcworkspace/xcuserdata/ryu.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /App/TaskTree.xcodeproj/xcshareddata/xcschemes/TaskTree.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryu0118/TaskTree/90aa38e1c92c7ba0b2a4731a8570c995b1b617d7/App/iOS/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIBackgroundModes 6 | 7 | remote-notification 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /App/iOS/InfoPlist.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "ja", 3 | "strings" : { 4 | "CFBundleName" : { 5 | "comment" : "Bundle name", 6 | "extractionState" : "extracted_with_value", 7 | "localizations" : { 8 | "en" : { 9 | "stringUnit" : { 10 | "state" : "translated", 11 | "value" : "TaskTree" 12 | } 13 | }, 14 | "ja" : { 15 | "stringUnit" : { 16 | "state" : "new", 17 | "value" : "TaskTree" 18 | } 19 | } 20 | } 21 | } 22 | }, 23 | "version" : "1.0" 24 | } -------------------------------------------------------------------------------- /App/iOS/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/iOS/TaskTree.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /App/iOS/TaskTreeApp.swift: -------------------------------------------------------------------------------- 1 | import AppFeature 2 | import SwiftUI 3 | 4 | @main 5 | struct TaskTreeApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | AppView() 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ryu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MINTRUN = mint run 2 | SWIFTFORMAT = $(MINTRUN) swiftformat 3 | DGRAPH = $(MINTRUN) swift-dependencies-graph dgraph 4 | LICENSE = $(MINTRUN) licensecli 5 | 6 | .PHONY: setup 7 | setup: 8 | brew install mint 9 | mint bootstrap 10 | 11 | .PHONY: format 12 | format: 13 | $(SWIFTFORMAT) . 14 | 15 | .PHONY: dgraph 16 | dgraph: 17 | $(DGRAPH) ./Packages/TaskTree 18 | 19 | .PHONY: license 20 | license: 21 | $(LICENSE) ./Packages/TaskTree/ ./Packages/TaskTree/Sources/Generated/ 22 | 23 | -------------------------------------------------------------------------------- /Mintfile: -------------------------------------------------------------------------------- 1 | SwiftGen/SwiftGen@6.6.2 2 | Ryu0118/swift-dependencies-graph@0.1.0 3 | Ryu0118/LicenseCLI@0.1.0 4 | -------------------------------------------------------------------------------- /Packages/TaskTree/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/TaskTree/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "combine-schedulers", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/combine-schedulers", 7 | "state" : { 8 | "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-case-paths", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-case-paths", 16 | "state" : { 17 | "revision" : "76d7791b5bda47df7e3d4690c4c3aaf089730707", 18 | "version" : "1.2.1" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-clocks", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-clocks", 25 | "state" : { 26 | "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", 27 | "version" : "1.0.2" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-concurrency-extras", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras", 34 | "state" : { 35 | "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", 36 | "version" : "1.1.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-custom-dump", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 43 | "state" : { 44 | "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", 45 | "version" : "1.1.2" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-dependencies", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/pointfreeco/swift-dependencies", 52 | "state" : { 53 | "revision" : "c31b1445c4fae49e6fdb75496b895a3653f6aefc", 54 | "version" : "1.1.5" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-syntax", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-syntax", 61 | "state" : { 62 | "revision" : "43c802fb7f96e090dde015344a94b5e85779eff1", 63 | "version" : "509.1.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swiftui-navigation", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/pointfreeco/swiftui-navigation", 70 | "state" : { 71 | "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", 72 | "version" : "1.2.0" 73 | } 74 | }, 75 | { 76 | "identity" : "xctest-dynamic-overlay", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 79 | "state" : { 80 | "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", 81 | "version" : "1.0.2" 82 | } 83 | } 84 | ], 85 | "version" : 2 86 | } 87 | -------------------------------------------------------------------------------- /Packages/TaskTree/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TaskTree", 8 | platforms: [ 9 | .iOS(.v17) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, making them visible to other packages. 13 | .library( 14 | name: "AppFeature", 15 | targets: ["AppFeature"] 16 | ), 17 | .library( 18 | name: "SettingFeature", 19 | targets: [ 20 | "SettingFeature" 21 | ] 22 | ) 23 | ], 24 | dependencies: [ 25 | .package(url: "https://github.com/pointfreeco/swift-dependencies", exact: "1.1.5"), 26 | .package(url: "https://github.com/pointfreeco/swiftui-navigation", exact: "1.2.0") 27 | ], 28 | targets: [ 29 | // Targets are the basic building blocks of a package, defining a module or a test suite. 30 | // Targets can depend on other targets in this package and products from dependencies. 31 | .target( 32 | name: "AppFeature", 33 | dependencies: [ 34 | "TaskTreeFeature" 35 | ] 36 | ), 37 | .target( 38 | name: "SwiftDataModel" 39 | ), 40 | .target( 41 | name: "SwiftDataUtils" 42 | ), 43 | .target( 44 | name: "TodoClient", 45 | dependencies: [ 46 | "SwiftDataModel", 47 | "SwiftDataUtils", 48 | .product(name: "Dependencies", package: "swift-dependencies"), 49 | .product(name: "DependenciesMacros", package: "swift-dependencies") 50 | ], 51 | resources: [ 52 | .process("Resources") 53 | ] 54 | ), 55 | .target( 56 | name: "Utils", 57 | dependencies: [ 58 | .product(name: "SwiftUINavigation", package: "swiftui-navigation") 59 | ], 60 | resources: [ 61 | .process("Resources") 62 | ] 63 | ), 64 | .target( 65 | name: "Generated" 66 | ), 67 | .target( 68 | name: "TaskTreeFeature", 69 | dependencies: [ 70 | "TodoClient", 71 | "Utils", 72 | "SettingFeature", 73 | .product(name: "Dependencies", package: "swift-dependencies"), 74 | .product(name: "SwiftUINavigation", package: "swiftui-navigation") 75 | ], 76 | resources: [ 77 | .process("Resources") 78 | ] 79 | ), 80 | .target( 81 | name: "SettingFeature", 82 | dependencies: [ 83 | "Generated" 84 | ], 85 | resources: [ 86 | .process("Resources") 87 | ] 88 | ), 89 | 90 | .testTarget( 91 | name: "TaskTreeTests", 92 | dependencies: [ 93 | "AppFeature", 94 | .product(name: "Dependencies", package: "swift-dependencies") 95 | ] 96 | ) 97 | ] 98 | ) 99 | -------------------------------------------------------------------------------- /Packages/TaskTree/PackageDependencies.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | graph TD; 3 | AppFeature-->TaskTreeFeature; 4 | TodoClient-->SwiftDataModel; 5 | TodoClient-->SwiftDataUtils; 6 | TaskTreeFeature-->TodoClient; 7 | TaskTreeFeature-->Utils; 8 | TaskTreeFeature-->SettingFeature; 9 | SettingFeature-->Generated; 10 | TaskTreeTests-->AppFeature; 11 | ``` -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/AppFeature/AppView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import TaskTreeFeature 3 | 4 | public struct AppView: View { 5 | public init() {} 6 | 7 | public var body: some View { 8 | NavigationStack { 9 | TaskTreeView() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/SettingFeature/LicensesView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Generated 3 | 4 | public struct LicensesView: View { 5 | public var body: some View { 6 | List { 7 | ForEach(Licenses.all) { license in 8 | NavigationLink { 9 | ScrollView { 10 | Text(license.license) 11 | } 12 | } label: { 13 | Text(license.id) 14 | } 15 | } 16 | } 17 | .navigationTitle(String(localized: "License", bundle: .module)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/SettingFeature/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "License" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "ja" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "ライセンス" 11 | } 12 | } 13 | } 14 | }, 15 | "Source Code" : { 16 | "extractionState" : "manual", 17 | "localizations" : { 18 | "ja" : { 19 | "stringUnit" : { 20 | "state" : "translated", 21 | "value" : "ソースコード" 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | "version" : "1.0" 28 | } -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/SettingFeature/Setting.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Observation 3 | import SwiftUI 4 | 5 | @Observable 6 | public final class SettingModel { 7 | public init() {} 8 | } 9 | 10 | public struct SettingView: View { 11 | let modal: SettingModel 12 | 13 | public init(modal: SettingModel) { 14 | self.modal = modal 15 | } 16 | 17 | public var body: some View { 18 | List { 19 | NavigationLink { 20 | LicensesView() 21 | } label: { 22 | Text("License", bundle: .module) 23 | } 24 | Link( 25 | String(localized: "Source Code", bundle: .module), 26 | destination: URL(string: "https://github.com/Ryu0118/TaskTree")! 27 | ) 28 | .buttonStyle(.borderless) 29 | } 30 | } 31 | } 32 | 33 | #Preview { 34 | SettingView(modal: .init()) 35 | } 36 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/SwiftDataModel/Todo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftData 3 | 4 | @Model 5 | public final class Todo { 6 | @Attribute(.unique) 7 | public var id: UUID 8 | 9 | @Relationship(deleteRule: .cascade, inverse: \Todo.parent) 10 | public var children: [Todo] = [] 11 | public var parent: Todo? 12 | public var title: String 13 | public var createdAt: Date 14 | 15 | @Transient 16 | public var isCompleted: Bool { 17 | get { 18 | if children.isEmpty { 19 | _isCompleted ?? false 20 | } else { 21 | children.allSatisfy(\.isCompleted) 22 | } 23 | } 24 | set { 25 | if children.isEmpty { 26 | _isCompleted = newValue 27 | } 28 | } 29 | } 30 | 31 | @Transient 32 | public var childrenCount: Int { 33 | children.count + children.reduce(0) { partialResult, todo in 34 | partialResult + todo.childrenCount 35 | } 36 | } 37 | 38 | public var _isCompleted: Bool? 39 | 40 | public init( 41 | id: UUID, 42 | children: [Todo], 43 | title: String, 44 | createdAt: Date 45 | ) { 46 | self.id = id 47 | self.children = children 48 | self.title = title 49 | self.createdAt = createdAt 50 | } 51 | } 52 | 53 | public let rootTodo = Todo( 54 | id: UUID(uuidString: "4C0D55DC-95BD-405C-BC78-F09331422E57")!, 55 | children: [], 56 | title: "Root", 57 | createdAt: Date() 58 | ) 59 | 60 | public extension Todo { 61 | var isRoot: Bool { 62 | rootTodo.id == id 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/SwiftDataUtils/ModelContext+init.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftData 3 | 4 | // ref: https://github.com/yusuga/swiftdata-101/blob/main/SwiftData101/Extension/ModelContext%2BSwiftData101.swift 5 | public extension ModelContext { 6 | enum StorageType { 7 | case inMemory 8 | case file 9 | } 10 | 11 | convenience init( 12 | for types: any PersistentModel.Type..., 13 | storageType: StorageType, 14 | shouldDeleteOldFile: Bool, 15 | fileName: String = #function 16 | ) throws { 17 | let schema = Schema(types) 18 | 19 | let sqliteURL = URL.documentsDirectory 20 | .appending(component: fileName) 21 | .appendingPathExtension("sqlite") 22 | 23 | if shouldDeleteOldFile { 24 | let fileManager = FileManager.default 25 | 26 | if fileManager.fileExists(atPath: sqliteURL.path) { 27 | try fileManager.removeItem(at: sqliteURL) 28 | } 29 | } 30 | 31 | let modelConfiguration = switch storageType { 32 | case .inMemory: 33 | ModelConfiguration( 34 | schema: schema, 35 | isStoredInMemoryOnly: true 36 | ) 37 | case .file: 38 | ModelConfiguration( 39 | schema: schema, 40 | url: sqliteURL, 41 | cloudKitDatabase: .automatic 42 | ) 43 | } 44 | 45 | let modelContainer = try ModelContainer( 46 | for: schema, 47 | configurations: [modelConfiguration] 48 | ) 49 | 50 | self.init(modelContainer) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/TaskTreeFeature/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "Add" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "ja" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "追加" 11 | } 12 | } 13 | } 14 | }, 15 | "Add a task" : { 16 | "extractionState" : "manual", 17 | "localizations" : { 18 | "ja" : { 19 | "stringUnit" : { 20 | "state" : "translated", 21 | "value" : "タスクを追加" 22 | } 23 | } 24 | } 25 | }, 26 | "Cancel" : { 27 | "extractionState" : "manual", 28 | "localizations" : { 29 | "ja" : { 30 | "stringUnit" : { 31 | "state" : "translated", 32 | "value" : "キャンセル" 33 | } 34 | } 35 | } 36 | }, 37 | "Enter a task name" : { 38 | "extractionState" : "manual", 39 | "localizations" : { 40 | "en" : { 41 | "stringUnit" : { 42 | "state" : "translated", 43 | "value" : "タスクを入力" 44 | } 45 | }, 46 | "ja" : { 47 | "stringUnit" : { 48 | "state" : "translated", 49 | "value" : "" 50 | } 51 | } 52 | } 53 | }, 54 | "Let's add a task!" : { 55 | "extractionState" : "manual", 56 | "localizations" : { 57 | "ja" : { 58 | "stringUnit" : { 59 | "state" : "translated", 60 | "value" : "タスクを追加しましょう!" 61 | } 62 | } 63 | } 64 | }, 65 | "No Tasks" : { 66 | "extractionState" : "manual", 67 | "localizations" : { 68 | "ja" : { 69 | "stringUnit" : { 70 | "state" : "translated", 71 | "value" : "タスクがありません" 72 | } 73 | } 74 | } 75 | }, 76 | "Task" : { 77 | "extractionState" : "manual", 78 | "localizations" : { 79 | "ja" : { 80 | "stringUnit" : { 81 | "state" : "translated", 82 | "value" : "タスク" 83 | } 84 | } 85 | } 86 | } 87 | }, 88 | "version" : "1.0" 89 | } -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/TaskTreeFeature/TaskTree.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | import Foundation 3 | import Observation 4 | import SwiftDataModel 5 | import SwiftUI 6 | import SwiftUINavigation 7 | import TodoClient 8 | import Utils 9 | import SettingFeature 10 | 11 | @MainActor 12 | @Observable 13 | final class TaskTreeModel { 14 | enum AlertAction: Equatable { 15 | case addTask 16 | } 17 | 18 | var setting: SettingModel? 19 | var selectedChildModel: TaskTreeModel? 20 | var parentTodo: Todo 21 | var children: [TaskTreeModel] { 22 | parentTodo.children 23 | .map { TaskTreeModel(parentTodo: $0) } 24 | .sorted { $0.parentTodo.createdAt > $1.parentTodo.createdAt } 25 | } 26 | 27 | var alert: AlertState? 28 | var addTaskAlert: AlertState? 29 | var taskTitle: String = "" 30 | 31 | init(parentTodo: Todo?) { 32 | if let parentTodo { 33 | self.parentTodo = parentTodo 34 | } else { 35 | @Dependency(\.todoClient) var todoClient 36 | do { 37 | self.parentTodo = try todoClient.fetchRootTodo() 38 | } catch { 39 | fatalError("Failed to retrieve root todo") 40 | } 41 | } 42 | } 43 | 44 | @ObservationIgnored 45 | @Dependency(\.todoClient) private var todoClient 46 | 47 | func delete(_ indexSet: IndexSet) { 48 | do { 49 | let todos = indexSet.map { children[$0].parentTodo } 50 | try todoClient.remove(parentTodo: parentTodo, todos: todos) 51 | } catch { 52 | alert = .error(error) 53 | } 54 | } 55 | 56 | func selectChildModel(_ child: TaskTreeModel) { 57 | selectedChildModel = child 58 | } 59 | 60 | func addTask(_ todo: Todo) { 61 | do { 62 | try todoClient.appendTodos(todo: todo, parentTodo: parentTodo) 63 | taskTitle = "" 64 | } catch { 65 | alert = .error(error) 66 | } 67 | } 68 | 69 | func toggleIsCompleted(_ todo: Todo) { 70 | do { 71 | try todoClient.toggleIsComplete(todo: todo) 72 | } catch { 73 | alert = .error(error) 74 | } 75 | } 76 | 77 | func presentAddTaskAlert() { 78 | addTaskAlert = AlertState { 79 | TextState("Add a task", bundle: .module) 80 | } actions: { 81 | ButtonState(action: .addTask) { 82 | TextState("Add", bundle: .module) 83 | } 84 | 85 | ButtonState(role: .cancel) { 86 | TextState("Cancel", bundle: .module) 87 | } 88 | } 89 | } 90 | 91 | func presentSetting() { 92 | setting = .init() 93 | } 94 | } 95 | 96 | @MainActor 97 | public struct TaskTreeView: View { 98 | @Bindable var model: TaskTreeModel 99 | 100 | public init() { 101 | model = TaskTreeModel(parentTodo: nil) 102 | } 103 | 104 | init(model: TaskTreeModel) { 105 | self.model = model 106 | } 107 | 108 | public var body: some View { 109 | List { 110 | ForEach(model.children, id: \.parentTodo) { childModel in 111 | HStack { 112 | Image(systemName: childModel.parentTodo.isCompleted ? "checkmark.circle" : "circle") 113 | .resizable() 114 | .foregroundStyle(.primary) 115 | .frame(width: 18, height: 18) 116 | .onTapGesture { 117 | model.toggleIsCompleted(childModel.parentTodo) 118 | } 119 | HStack { 120 | Text(childModel.parentTodo.title) 121 | .overlay { 122 | if childModel.parentTodo.isCompleted { 123 | Rectangle() 124 | .fill(.primary) 125 | .frame(height: 1) 126 | } 127 | } 128 | Spacer() 129 | HStack(spacing: 0) { 130 | Text("\(childModel.parentTodo.childrenCount)") 131 | .font(.subheadline) 132 | .foregroundStyle(.secondary) 133 | NavigationLink.empty 134 | .frame(width: 10) 135 | } 136 | } 137 | .contentShape(Rectangle()) 138 | .onTapGesture { 139 | model.selectChildModel(childModel) 140 | } 141 | } 142 | } 143 | .onDelete { indexSet in 144 | model.delete(indexSet) 145 | } 146 | } 147 | .navigationTitle(model.parentTodo.isRoot ? String(localized: "Task", bundle: .module) : model.parentTodo.title) 148 | .overlay { 149 | if model.children.isEmpty { 150 | ContentUnavailableView( 151 | String(localized: "No Tasks", bundle: .module), 152 | systemImage: "list.bullet.clipboard", 153 | description: Text("Let's add a task!", bundle: .module) 154 | ) 155 | } 156 | } 157 | .alert($model.alert) 158 | .alert($model.addTaskAlert) { action in 159 | switch action { 160 | case .addTask: 161 | model.addTask( 162 | Todo( 163 | id: UUID(), 164 | children: [], 165 | title: model.taskTitle, 166 | createdAt: .now 167 | ) 168 | ) 169 | case .none: 170 | break 171 | } 172 | } content: { 173 | TextField(String(localized: "Enter a task name", bundle: .module), text: $model.taskTitle) 174 | } 175 | .navigationDestination(unwrapping: $model.selectedChildModel) { $childModel in 176 | TaskTreeView(model: $childModel.wrappedValue) 177 | } 178 | .sheet(unwrapping: $model.setting) { $setting in 179 | NavigationStack { 180 | SettingView(modal: $setting.wrappedValue) 181 | } 182 | } 183 | .toolbar { 184 | ToolbarItem(placement: .topBarTrailing) { 185 | Button { 186 | model.presentSetting() 187 | } label: { 188 | Image(systemName: "gear") 189 | } 190 | } 191 | ToolbarItem(placement: .topBarTrailing) { 192 | Button { 193 | model.presentAddTaskAlert() 194 | } label: { 195 | Image(systemName: "plus") 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | #Preview { 203 | NavigationStack { 204 | TaskTreeView( 205 | model: withDependencies { 206 | $0.todoClient = .previewValue 207 | } operation: { 208 | TaskTreeModel(parentTodo: nil) 209 | } 210 | ) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/TodoClient/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "task-cannot-be-completed" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "en" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "The task cannot be completed if the child task is not finished" 11 | } 12 | }, 13 | "ja" : { 14 | "stringUnit" : { 15 | "state" : "translated", 16 | "value" : "子のタスクが終わっていないとタスクが完了できません" 17 | } 18 | } 19 | } 20 | }, 21 | "task-cannot-be-found" : { 22 | "extractionState" : "manual", 23 | "localizations" : { 24 | "en" : { 25 | "stringUnit" : { 26 | "state" : "translated", 27 | "value" : "A task cannot be found" 28 | } 29 | }, 30 | "ja" : { 31 | "stringUnit" : { 32 | "state" : "translated", 33 | "value" : "タスクが見つかりませんでした" 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "version" : "1.0" 40 | } -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/TodoClient/TodoClient.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | import DependenciesMacros 3 | import Foundation 4 | import SwiftData 5 | import SwiftDataModel 6 | import SwiftDataUtils 7 | 8 | @DependencyClient 9 | public struct TodoClient { 10 | public var fetchRootTodo: @Sendable () throws -> Todo 11 | public var appendTodos: @Sendable (_ todo: Todo, _ parentTodo: Todo) throws -> Void 12 | public var deleteTodo: @Sendable (_ todo: Todo) throws -> Void 13 | public var fetchTodos: @Sendable (_ parentID: UUID) throws -> [Todo] 14 | public var toggleIsComplete: @Sendable (_ todo: Todo) throws -> Void 15 | public var fetchTodo: @Sendable (_ todoID: UUID) throws -> Todo 16 | public var remove: @Sendable (_ parentTodo: Todo, _ todos: [Todo]) throws -> Void 17 | } 18 | 19 | extension TodoClient: DependencyKey { 20 | public static let liveValue: TodoClient = .live() 21 | 22 | static func live( 23 | storageType: ModelContext.StorageType = .file, 24 | shouldDeleteOldFile: Bool = false 25 | ) -> Self { 26 | @Sendable func context() throws -> ModelContext { 27 | try ModelContext( 28 | for: SwiftDataModel.Todo.self, 29 | storageType: storageType, 30 | shouldDeleteOldFile: shouldDeleteOldFile 31 | ) 32 | } 33 | 34 | @Sendable func fetchTodo(id: UUID) throws -> SwiftDataModel.Todo { 35 | let context = try context() 36 | guard let todo = try context.fetch( 37 | FetchDescriptor( 38 | predicate: #Predicate { 39 | $0.id == id 40 | } 41 | ) 42 | ).first 43 | else { 44 | throw TodoClientError.taskCannotBeFound 45 | } 46 | return todo 47 | } 48 | 49 | return Self( 50 | fetchRootTodo: { 51 | let context = try context() 52 | 53 | do { 54 | return try fetchTodo(id: rootTodo.id) 55 | } catch let error as TodoClientError where error == .taskCannotBeFound { 56 | context.insert(rootTodo) 57 | try context.save() 58 | return rootTodo 59 | } catch { 60 | throw error 61 | } 62 | }, 63 | appendTodos: { todo, parentTodo in 64 | let context = try context() 65 | parentTodo.children.append(todo) 66 | try context.save() 67 | }, 68 | deleteTodo: { todo in 69 | let context = try context() 70 | let id = todo.id 71 | try context.delete( 72 | model: SwiftDataModel.Todo.self, 73 | where: #Predicate { 74 | $0.id == id 75 | } 76 | ) 77 | }, 78 | fetchTodos: { id in 79 | let context = try context() 80 | return try context.fetch( 81 | FetchDescriptor( 82 | predicate: #Predicate { 83 | $0.parent?.id == id 84 | } 85 | ) 86 | ) 87 | }, 88 | toggleIsComplete: { todo in 89 | let context = try context() 90 | if todo.children.isEmpty { 91 | if todo._isCompleted == nil { 92 | todo._isCompleted = true 93 | } else { 94 | todo._isCompleted?.toggle() 95 | } 96 | try context.save() 97 | } else { 98 | throw TodoClientError.taskCannotBeCompleted 99 | } 100 | }, 101 | fetchTodo: { id in 102 | try fetchTodo(id: id) 103 | }, 104 | remove: { parentTodo, todos in 105 | let context = try context() 106 | let todoIDs = todos.map(\.id) 107 | parentTodo.children.removeAll { todoIDs.contains($0.id) } 108 | try context.save() 109 | } 110 | ) 111 | } 112 | } 113 | 114 | public enum TodoClientError: LocalizedError { 115 | case taskCannotBeCompleted 116 | case taskCannotBeFound 117 | 118 | public var errorDescription: String? { 119 | switch self { 120 | case .taskCannotBeCompleted: 121 | String(localized: "task-cannot-be-completed", bundle: .module) 122 | 123 | case .taskCannotBeFound: 124 | String(localized: "task-cannot-be-found", bundle: .module) 125 | } 126 | } 127 | } 128 | 129 | extension TodoClient: TestDependencyKey { 130 | public static let testValue = Self() 131 | public static let previewValue: Self = .live(storageType: .inMemory, shouldDeleteOldFile: true) 132 | } 133 | 134 | public extension DependencyValues { 135 | var todoClient: TodoClient { 136 | get { self[TodoClient.self] } 137 | set { self[TodoClient.self] = newValue } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/Utils/AlertState+error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUINavigation 3 | 4 | public extension AlertState { 5 | static func error(_ error: some Error) -> Self { 6 | AlertState { 7 | TextState("An error has occurred", bundle: .module) 8 | } actions: { 9 | ButtonState(role: .cancel) { 10 | TextState("OK") 11 | } 12 | } message: { 13 | TextState(error.localizedDescription) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/Utils/NavigationLink+empty.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension NavigationLink where Label == EmptyView, Destination == EmptyView { 4 | /// Useful in cases where a `NavigationLink` is needed but there should not be 5 | /// a destination. e.g. for programmatic navigation. 6 | static var empty: NavigationLink { 7 | self.init(destination: EmptyView(), label: { EmptyView() }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/Utils/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "An error has occurred" : { 5 | "extractionState" : "manual", 6 | "localizations" : { 7 | "ja" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "エラーが発生しました" 11 | } 12 | } 13 | } 14 | } 15 | }, 16 | "version" : "1.0" 17 | } -------------------------------------------------------------------------------- /Packages/TaskTree/Sources/Utils/View+alert.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftUINavigation 3 | 4 | public extension View { 5 | func alert( 6 | _ state: Binding?>, 7 | action handler: @escaping (Value?) async -> Void = { (_: Never?) async in }, 8 | content: () -> some View 9 | ) -> some View { 10 | alert( 11 | (state.wrappedValue?.title).map(Text.init) ?? Text(verbatim: ""), 12 | isPresented: state.isPresent(), 13 | presenting: state.wrappedValue, 14 | actions: { 15 | content() 16 | ForEach($0.buttons) { 17 | Button($0, action: handler) 18 | } 19 | }, 20 | message: { $0.message.map { Text($0) } } 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Packages/TaskTree/Tests/TaskTreeTests/TaskTreeTests.swift: -------------------------------------------------------------------------------- 1 | @testable import TaskTree 2 | import XCTest 3 | 4 | final class TaskTreeTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TaskTree -------------------------------------------------------------------------------- /TaskTree.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TaskTree.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TaskTree.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "combine-schedulers", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/combine-schedulers", 7 | "state" : { 8 | "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-case-paths", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-case-paths", 16 | "state" : { 17 | "revision" : "76d7791b5bda47df7e3d4690c4c3aaf089730707", 18 | "version" : "1.2.1" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-clocks", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-clocks", 25 | "state" : { 26 | "revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33", 27 | "version" : "1.0.2" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-concurrency-extras", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras", 34 | "state" : { 35 | "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", 36 | "version" : "1.1.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-custom-dump", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 43 | "state" : { 44 | "revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605", 45 | "version" : "1.1.2" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-dependencies", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/pointfreeco/swift-dependencies", 52 | "state" : { 53 | "revision" : "c31b1445c4fae49e6fdb75496b895a3653f6aefc", 54 | "version" : "1.1.5" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-syntax", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-syntax", 61 | "state" : { 62 | "revision" : "43c802fb7f96e090dde015344a94b5e85779eff1", 63 | "version" : "509.1.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swiftui-navigation", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/pointfreeco/swiftui-navigation", 70 | "state" : { 71 | "revision" : "78f9d72cf667adb47e2040aa373185c88c63f0dc", 72 | "version" : "1.2.0" 73 | } 74 | }, 75 | { 76 | "identity" : "xctest-dynamic-overlay", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 79 | "state" : { 80 | "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", 81 | "version" : "1.0.2" 82 | } 83 | } 84 | ], 85 | "version" : 2 86 | } 87 | -------------------------------------------------------------------------------- /docs/privacy-policy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Privacy Policy 7 | 8 | 9 | 10 | Privacy Policy

11 | built the TaskTree app as 12 | an Open Source app. This SERVICE is provided by 13 | at no cost and is intended for use as 14 | is. 15 |

16 | This page is used to inform visitors regarding my 17 | policies with the collection, use, and disclosure of Personal 18 | Information if anyone decided to use my Service. 19 |

20 | If you choose to use my Service, then you agree to 21 | the collection and use of information in relation to this 22 | policy. The Personal Information that I collect is 23 | used for providing and improving the Service. I will not use or share your information with 24 | anyone except as described in this Privacy Policy. 25 |

26 | The terms used in this Privacy Policy have the same meanings 27 | as in our Terms and Conditions, which are accessible at 28 | TaskTree unless otherwise defined in this Privacy Policy. 29 |

Information Collection and Use

30 | For a better experience, while using our Service, I 31 | may require you to provide us with certain personally 32 | identifiable information. The information that 33 | I request will be retained on your device and is not collected by me in any way. 34 |

Log Data

35 | I want to inform you that whenever you 36 | use my Service, in a case of an error in the app 37 | I collect data and information (through third-party 38 | products) on your phone called Log Data. This Log Data may 39 | include information such as your device Internet Protocol 40 | (“IP”) address, device name, operating system version, the 41 | configuration of the app when utilizing my Service, 42 | the time and date of your use of the Service, and other 43 | statistics. 44 |

Cookies

45 | Cookies are files with a small amount of data that are 46 | commonly used as anonymous unique identifiers. These are sent 47 | to your browser from the websites that you visit and are 48 | stored on your device's internal memory. 49 |

50 | This Service does not use these “cookies” explicitly. However, 51 | the app may use third-party code and libraries that use 52 | “cookies” to collect information and improve their services. 53 | You have the option to either accept or refuse these cookies 54 | and know when a cookie is being sent to your device. If you 55 | choose to refuse our cookies, you may not be able to use some 56 | portions of this Service. 57 |

Service Providers

58 | I may employ third-party companies and 59 | individuals due to the following reasons: 60 |

  • To facilitate our Service;
  • To provide the Service on our behalf;
  • To perform Service-related services; or
  • To assist us in analyzing how our Service is used.

61 | I want to inform users of this Service 62 | that these third parties have access to their Personal 63 | Information. The reason is to perform the tasks assigned to 64 | them on our behalf. However, they are obligated not to 65 | disclose or use the information for any other purpose. 66 |

Security

67 | I value your trust in providing us your 68 | Personal Information, thus we are striving to use commercially 69 | acceptable means of protecting it. But remember that no method 70 | of transmission over the internet, or method of electronic 71 | storage is 100% secure and reliable, and I cannot 72 | guarantee its absolute security. 73 |

Links to Other Sites

74 | This Service may contain links to other sites. If you click on 75 | a third-party link, you will be directed to that site. Note 76 | that these external sites are not operated by me. 77 | Therefore, I strongly advise you to review the 78 | Privacy Policy of these websites. I have 79 | no control over and assume no responsibility for the content, 80 | privacy policies, or practices of any third-party sites or 81 | services. 82 |

Children’s Privacy

83 | I do not knowingly collect personally 84 | identifiable information from children. I 85 | encourage all children to never submit any personally 86 | identifiable information through 87 | the Application and/or Services. 88 | I encourage parents and legal guardians to monitor 89 | their children's Internet usage and to help enforce this Policy by instructing 90 | their children never to provide personally identifiable information through the Application and/or Services without their permission. If you have reason to believe that a child 91 | has provided personally identifiable information to us through the Application and/or Services, 92 | please contact us. You must also be at least 16 years of age to consent to the processing 93 | of your personally identifiable information in your country (in some countries we may allow your parent 94 | or guardian to do so on your behalf). 95 |

Changes to This Privacy Policy

96 | I may update our Privacy Policy from 97 | time to time. Thus, you are advised to review this page 98 | periodically for any changes. I will 99 | notify you of any changes by posting the new Privacy Policy on 100 | this page. 101 |

This policy is effective as of 2024-01-14

Contact Us

102 | If you have any questions or suggestions about my 103 | Privacy Policy, do not hesitate to contact me at ryu.apps.info@gmail.com. 104 | 105 | 106 | 107 | --------------------------------------------------------------------------------