From 261b2f58cc718ce3c52ed932a7a7fa699d2c1591 Mon Sep 17 00:00:00 2001 From: szbk Date: Wed, 11 Feb 2026 18:06:35 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20ios=20mobil=20aray=C3=BCz=20tasar=C4=B1?= =?UTF-8?q?m=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Bookibra.xcodeproj/project.pbxproj | 481 ++++++++++++++++++ .../UserInterfaceState.xcuserstate | Bin 0 -> 20636 bytes .../xcschemes/xcschememanagement.plist | 14 + ios/Bookibra/App/AppRouter.swift | 53 ++ ios/Bookibra/App/BookibraApp.swift | 72 +++ .../Components/BlurFogOverlay.swift | 17 + .../Components/BookCoverCard.swift | 57 +++ .../Components/NetworkErrorView.swift | 21 + .../Components/PrimaryPillButton.swift | 21 + .../DesignSystem/Components/ScrewView.swift | 14 + .../Components/ShelfSectionView.swift | 85 ++++ ios/Bookibra/DesignSystem/Theme.swift | 29 ++ ios/Bookibra/Models/BookRemote.swift | 135 +++++ ios/Bookibra/Models/LibraryBook.swift | 63 +++ ios/Bookibra/Models/UserProfile.swift | 10 + .../AccentColor.colorset/Contents.json | 20 + .../AppIcon.appiconset/Contents.json | 53 ++ .../Resources/Assets.xcassets/Contents.json | 6 + ios/Bookibra/Resources/Debug.xcconfig | 1 + ios/Bookibra/Resources/Info.plist | 46 ++ ios/Bookibra/Resources/Release.xcconfig | 1 + .../Resources/en.lproj/Localizable.strings | 18 + ios/Bookibra/Resources/mock_book_remote.json | 21 + .../Resources/tr.lproj/Localizable.strings | 18 + ios/Bookibra/Services/APIClient.swift | 129 +++++ ios/Bookibra/Services/AuthService.swift | 42 ++ ios/Bookibra/Services/BooksService.swift | 52 ++ ios/Bookibra/Services/ImageCache.swift | 34 ++ ios/Bookibra/Services/KeychainStore.swift | 47 ++ .../ViewModels/AddBooksViewModel.swift | 88 ++++ ios/Bookibra/ViewModels/AuthViewModel.swift | 53 ++ .../ViewModels/BookDetailViewModel.swift | 55 ++ .../ViewModels/CategoryViewModel.swift | 39 ++ ios/Bookibra/ViewModels/HomeViewModel.swift | 61 +++ .../Views/AddBooks/AddBooksView.swift | 115 +++++ .../Views/AddBooks/BarcodeScannerView.swift | 154 ++++++ ios/Bookibra/Views/Auth/AuthView.swift | 71 +++ .../Views/Category/CategoryListView.swift | 71 +++ .../Views/Detail/BookDetailView.swift | 58 +++ ios/Bookibra/Views/Home/HomeView.swift | 73 +++ ios/README.md | 23 + ios/create_xcodeproj.rb | 80 +++ 42 files changed, 2501 insertions(+) create mode 100644 ios/Bookibra.xcodeproj/project.pbxproj create mode 100644 ios/Bookibra.xcodeproj/project.xcworkspace/xcuserdata/wisecolt.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 ios/Bookibra.xcodeproj/xcuserdata/wisecolt.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 ios/Bookibra/App/AppRouter.swift create mode 100644 ios/Bookibra/App/BookibraApp.swift create mode 100644 ios/Bookibra/DesignSystem/Components/BlurFogOverlay.swift create mode 100644 ios/Bookibra/DesignSystem/Components/BookCoverCard.swift create mode 100644 ios/Bookibra/DesignSystem/Components/NetworkErrorView.swift create mode 100644 ios/Bookibra/DesignSystem/Components/PrimaryPillButton.swift create mode 100644 ios/Bookibra/DesignSystem/Components/ScrewView.swift create mode 100644 ios/Bookibra/DesignSystem/Components/ShelfSectionView.swift create mode 100644 ios/Bookibra/DesignSystem/Theme.swift create mode 100644 ios/Bookibra/Models/BookRemote.swift create mode 100644 ios/Bookibra/Models/LibraryBook.swift create mode 100644 ios/Bookibra/Models/UserProfile.swift create mode 100644 ios/Bookibra/Resources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ios/Bookibra/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Bookibra/Resources/Assets.xcassets/Contents.json create mode 100644 ios/Bookibra/Resources/Debug.xcconfig create mode 100644 ios/Bookibra/Resources/Info.plist create mode 100644 ios/Bookibra/Resources/Release.xcconfig create mode 100644 ios/Bookibra/Resources/en.lproj/Localizable.strings create mode 100644 ios/Bookibra/Resources/mock_book_remote.json create mode 100644 ios/Bookibra/Resources/tr.lproj/Localizable.strings create mode 100644 ios/Bookibra/Services/APIClient.swift create mode 100644 ios/Bookibra/Services/AuthService.swift create mode 100644 ios/Bookibra/Services/BooksService.swift create mode 100644 ios/Bookibra/Services/ImageCache.swift create mode 100644 ios/Bookibra/Services/KeychainStore.swift create mode 100644 ios/Bookibra/ViewModels/AddBooksViewModel.swift create mode 100644 ios/Bookibra/ViewModels/AuthViewModel.swift create mode 100644 ios/Bookibra/ViewModels/BookDetailViewModel.swift create mode 100644 ios/Bookibra/ViewModels/CategoryViewModel.swift create mode 100644 ios/Bookibra/ViewModels/HomeViewModel.swift create mode 100644 ios/Bookibra/Views/AddBooks/AddBooksView.swift create mode 100644 ios/Bookibra/Views/AddBooks/BarcodeScannerView.swift create mode 100644 ios/Bookibra/Views/Auth/AuthView.swift create mode 100644 ios/Bookibra/Views/Category/CategoryListView.swift create mode 100644 ios/Bookibra/Views/Detail/BookDetailView.swift create mode 100644 ios/Bookibra/Views/Home/HomeView.swift create mode 100644 ios/README.md create mode 100644 ios/create_xcodeproj.rb diff --git a/ios/Bookibra.xcodeproj/project.pbxproj b/ios/Bookibra.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3454e5d --- /dev/null +++ b/ios/Bookibra.xcodeproj/project.pbxproj @@ -0,0 +1,481 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0987C082DE634D36D5BF03DE /* ShelfSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C361617CEBCE704307DA0A9 /* ShelfSectionView.swift */; }; + 1DCD42AC02DDABACC54958C5 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923CE0DF37333FFAF75668D5 /* UserProfile.swift */; }; + 245681EBBC7EB40F2733DD6B /* NetworkErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DED90139ADCDD834AE33CF /* NetworkErrorView.swift */; }; + 2C2B30975D5DC09342690C43 /* BlurFogOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6F6AF589E6DD8E8EEF14CF /* BlurFogOverlay.swift */; }; + 31B3807F97E8E1512FB5DEEA /* CategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15179D9ED03860747BBC180 /* CategoryViewModel.swift */; }; + 438812F1044DE6EBEA5B42E6 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72FE6CAAF6A00908AC19835F /* AVFoundation.framework */; }; + 44B8242D7211EDD650FCA488 /* BarcodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471C2927142735752EA378A9 /* BarcodeScannerView.swift */; }; + 4EE9C0CB6C3006780E8CDFA4 /* BookCoverCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C2A1F39419A03C31114C3 /* BookCoverCard.swift */; }; + 5B73E67A2A3873F06000D8AF /* VisionKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1744A67EB267139F1EBDAE55 /* VisionKit.framework */; }; + 5DD75F144A0C411F2D7EF9C8 /* LibraryBook.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93B9F6AC3711B6E32030129 /* LibraryBook.swift */; }; + 6998E51506433C1B0A647330 /* BookibraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8155E61788F47524B11449 /* BookibraApp.swift */; }; + 6B60107AEE8C2489D19B1D9D /* mock_book_remote.json in Resources */ = {isa = PBXBuildFile; fileRef = 95A0AD7A2591540C4ED3252F /* mock_book_remote.json */; }; + 6E9DFC74E4EA2AC64A343E4C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0417E5217F2A37B2065F6DC9 /* Assets.xcassets */; }; + 7C130ABD8F4627EA3C0FB239 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2448E99A3D8CDBD5334901C7 /* ImageCache.swift */; }; + 7C17970A1EC6CE66AB2B6962 /* BookDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C4EA4FE11115BA86AB2802 /* BookDetailViewModel.swift */; }; + 7C5391EE19CD4370B0871A6F /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18379D6C3B16C83CCBCF22 /* HomeView.swift */; }; + 89E2012E58DEB2A6CA3191F9 /* AddBooksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB571FD5EA0CB9C15DBD1DB /* AddBooksViewModel.swift */; }; + 90E97C917EEA4B7B1D09F8FB /* BookRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB98ADB94703A1CE74B79CD /* BookRemote.swift */; }; + 9DF5677130BA3D0F14C04B4B /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB5F3DC625CEAE1BEC3911F /* HomeViewModel.swift */; }; + A08E9B72A9FAA96CC58C82B1 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 329388F7DE8EB288AEE98A23 /* AuthView.swift */; }; + A5BC1762D555E6DC13C8664E /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5F3C9BA827C89A343166A4 /* AuthViewModel.swift */; }; + B404D223478123428719790C /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4DF6CD123627625C1967D42 /* AppRouter.swift */; }; + B66DAE4BF97BCAC84740B541 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 20947201FBE7D30CD6F69E38 /* Localizable.strings */; }; + BDE45343461F02318DD86FDB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1B0A599E6FA80C6DBB852EB5 /* Localizable.strings */; }; + C9656D40284BF3D44322AE99 /* BookDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A39116C7E45F12244CA1DC23 /* BookDetailView.swift */; }; + CB31E95DBEF85410677B11E3 /* PrimaryPillButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE7B76B449982D19D91A67F3 /* PrimaryPillButton.swift */; }; + CD404352E3F12BCAE64E4BAC /* ScrewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DD9EB12817D19C6BC65306 /* ScrewView.swift */; }; + D146299A54D7A660951C3075 /* AddBooksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA01B4D0A49FA8E5D1E4F65 /* AddBooksView.swift */; }; + D2B224D07CFBA048947BCB22 /* SwiftData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 562B9464344AA711558F2AD0 /* SwiftData.framework */; }; + D6D4F249AA8A85A041C7D112 /* CategoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D117FECFDDAC2EDDC191CEC9 /* CategoryListView.swift */; }; + E1E4040DBBACA7268F84998B /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806F17D3E801BC96C6B5ED6D /* KeychainStore.swift */; }; + E7D483E62D94D20A511C6967 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B618484EE9FC15B0DCA5E055 /* APIClient.swift */; }; + EAA4D823C890C98F37F64E1E /* BooksService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77D7EF1C78724F79956D5B1 /* BooksService.swift */; }; + EC793F00D7851722C0DD1633 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B226CA006E6EEA7DB04F100 /* Theme.swift */; }; + F3452503ADD5962494ABB38D /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF98B022FA5BAA8F627DAAD /* AuthService.swift */; }; + F971998248197B0A0848FC88 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 192DFF277BADE93F9813BFFA /* Foundation.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0417E5217F2A37B2065F6DC9 /* Assets.xcassets */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Resources/Assets.xcassets; sourceTree = ""; }; + 1744A67EB267139F1EBDAE55 /* VisionKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VisionKit.framework; path = System/Library/Frameworks/VisionKit.framework; sourceTree = ""; }; + 192DFF277BADE93F9813BFFA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 1B0A599E6FA80C6DBB852EB5 /* Localizable.strings */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = Localizable.strings; path = Resources/tr.lproj/Localizable.strings; sourceTree = ""; }; + 1EB98ADB94703A1CE74B79CD /* BookRemote.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookRemote.swift; path = Models/BookRemote.swift; sourceTree = ""; }; + 20947201FBE7D30CD6F69E38 /* Localizable.strings */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = Localizable.strings; path = Resources/en.lproj/Localizable.strings; sourceTree = ""; }; + 2448E99A3D8CDBD5334901C7 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Services/ImageCache.swift; sourceTree = ""; }; + 2F8155E61788F47524B11449 /* BookibraApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookibraApp.swift; path = App/BookibraApp.swift; sourceTree = ""; }; + 329388F7DE8EB288AEE98A23 /* AuthView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthView.swift; path = Views/Auth/AuthView.swift; sourceTree = ""; }; + 349C2A1F39419A03C31114C3 /* BookCoverCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookCoverCard.swift; path = DesignSystem/Components/BookCoverCard.swift; sourceTree = ""; }; + 3B226CA006E6EEA7DB04F100 /* Theme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = DesignSystem/Theme.swift; sourceTree = ""; }; + 3BB571FD5EA0CB9C15DBD1DB /* AddBooksViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AddBooksViewModel.swift; path = ViewModels/AddBooksViewModel.swift; sourceTree = ""; }; + 471C2927142735752EA378A9 /* BarcodeScannerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarcodeScannerView.swift; path = Views/AddBooks/BarcodeScannerView.swift; sourceTree = ""; }; + 4C361617CEBCE704307DA0A9 /* ShelfSectionView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShelfSectionView.swift; path = DesignSystem/Components/ShelfSectionView.swift; sourceTree = ""; }; + 4DA01B4D0A49FA8E5D1E4F65 /* AddBooksView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AddBooksView.swift; path = Views/AddBooks/AddBooksView.swift; sourceTree = ""; }; + 562B9464344AA711558F2AD0 /* SwiftData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftData.framework; path = System/Library/Frameworks/SwiftData.framework; sourceTree = ""; }; + 72DED90139ADCDD834AE33CF /* NetworkErrorView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkErrorView.swift; path = DesignSystem/Components/NetworkErrorView.swift; sourceTree = ""; }; + 72FE6CAAF6A00908AC19835F /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = ""; }; + 7B6C1F4EB35DF0216BC86061 /* Bookibra.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bookibra.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 806F17D3E801BC96C6B5ED6D /* KeychainStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KeychainStore.swift; path = Services/KeychainStore.swift; sourceTree = ""; }; + 923CE0DF37333FFAF75668D5 /* UserProfile.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UserProfile.swift; path = Models/UserProfile.swift; sourceTree = ""; }; + 95A0AD7A2591540C4ED3252F /* mock_book_remote.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = mock_book_remote.json; path = Resources/mock_book_remote.json; sourceTree = ""; }; + 96C4EA4FE11115BA86AB2802 /* BookDetailViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookDetailViewModel.swift; path = ViewModels/BookDetailViewModel.swift; sourceTree = ""; }; + 98AAF8E8F7F53CA9CE81186B /* Release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Bookibra/Resources/Release.xcconfig; sourceTree = ""; }; + A39116C7E45F12244CA1DC23 /* BookDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookDetailView.swift; path = Views/Detail/BookDetailView.swift; sourceTree = ""; }; + A77D7EF1C78724F79956D5B1 /* BooksService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BooksService.swift; path = Services/BooksService.swift; sourceTree = ""; }; + AF5F3C9BA827C89A343166A4 /* AuthViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthViewModel.swift; path = ViewModels/AuthViewModel.swift; sourceTree = ""; }; + AFD5A6DD3D2BFC014B9AB859 /* Debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Bookibra/Resources/Debug.xcconfig; sourceTree = ""; }; + B618484EE9FC15B0DCA5E055 /* APIClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = APIClient.swift; path = Services/APIClient.swift; sourceTree = ""; }; + B93B9F6AC3711B6E32030129 /* LibraryBook.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LibraryBook.swift; path = Models/LibraryBook.swift; sourceTree = ""; }; + BEB5F3DC625CEAE1BEC3911F /* HomeViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeViewModel.swift; path = ViewModels/HomeViewModel.swift; sourceTree = ""; }; + BF18379D6C3B16C83CCBCF22 /* HomeView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeView.swift; path = Views/Home/HomeView.swift; sourceTree = ""; }; + C15179D9ED03860747BBC180 /* CategoryViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CategoryViewModel.swift; path = ViewModels/CategoryViewModel.swift; sourceTree = ""; }; + C8DD9EB12817D19C6BC65306 /* ScrewView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScrewView.swift; path = DesignSystem/Components/ScrewView.swift; sourceTree = ""; }; + CAF98B022FA5BAA8F627DAAD /* AuthService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthService.swift; path = Services/AuthService.swift; sourceTree = ""; }; + D117FECFDDAC2EDDC191CEC9 /* CategoryListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CategoryListView.swift; path = Views/Category/CategoryListView.swift; sourceTree = ""; }; + DC6F6AF589E6DD8E8EEF14CF /* BlurFogOverlay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BlurFogOverlay.swift; path = DesignSystem/Components/BlurFogOverlay.swift; sourceTree = ""; }; + E4DF6CD123627625C1967D42 /* AppRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRouter.swift; path = App/AppRouter.swift; sourceTree = ""; }; + FE7B76B449982D19D91A67F3 /* PrimaryPillButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PrimaryPillButton.swift; path = DesignSystem/Components/PrimaryPillButton.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 96BCE7EBCFE3A1B03D95C0A5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F971998248197B0A0848FC88 /* Foundation.framework in Frameworks */, + 438812F1044DE6EBEA5B42E6 /* AVFoundation.framework in Frameworks */, + 5B73E67A2A3873F06000D8AF /* VisionKit.framework in Frameworks */, + D2B224D07CFBA048947BCB22 /* SwiftData.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 11824F4150C100F76F38DBD8 = { + isa = PBXGroup; + children = ( + 94F7452CA97CED98C86AF84E /* Products */, + 8EB4884498D4ACBA5BB04312 /* Frameworks */, + AFD5A6DD3D2BFC014B9AB859 /* Debug.xcconfig */, + 98AAF8E8F7F53CA9CE81186B /* Release.xcconfig */, + F2E80CFCFD2FE3BF793C4147 /* Bookibra */, + ); + sourceTree = ""; + }; + 8EB4884498D4ACBA5BB04312 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E592B0F1D9DDB4B2033A4A3D /* iOS */, + 72FE6CAAF6A00908AC19835F /* AVFoundation.framework */, + 1744A67EB267139F1EBDAE55 /* VisionKit.framework */, + 562B9464344AA711558F2AD0 /* SwiftData.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 94F7452CA97CED98C86AF84E /* Products */ = { + isa = PBXGroup; + children = ( + 7B6C1F4EB35DF0216BC86061 /* Bookibra.app */, + ); + name = Products; + sourceTree = ""; + }; + E592B0F1D9DDB4B2033A4A3D /* iOS */ = { + isa = PBXGroup; + children = ( + 192DFF277BADE93F9813BFFA /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + F2E80CFCFD2FE3BF793C4147 /* Bookibra */ = { + isa = PBXGroup; + children = ( + E4DF6CD123627625C1967D42 /* AppRouter.swift */, + 2F8155E61788F47524B11449 /* BookibraApp.swift */, + DC6F6AF589E6DD8E8EEF14CF /* BlurFogOverlay.swift */, + 349C2A1F39419A03C31114C3 /* BookCoverCard.swift */, + 72DED90139ADCDD834AE33CF /* NetworkErrorView.swift */, + FE7B76B449982D19D91A67F3 /* PrimaryPillButton.swift */, + C8DD9EB12817D19C6BC65306 /* ScrewView.swift */, + 4C361617CEBCE704307DA0A9 /* ShelfSectionView.swift */, + 3B226CA006E6EEA7DB04F100 /* Theme.swift */, + 1EB98ADB94703A1CE74B79CD /* BookRemote.swift */, + B93B9F6AC3711B6E32030129 /* LibraryBook.swift */, + 923CE0DF37333FFAF75668D5 /* UserProfile.swift */, + B618484EE9FC15B0DCA5E055 /* APIClient.swift */, + CAF98B022FA5BAA8F627DAAD /* AuthService.swift */, + A77D7EF1C78724F79956D5B1 /* BooksService.swift */, + 2448E99A3D8CDBD5334901C7 /* ImageCache.swift */, + 806F17D3E801BC96C6B5ED6D /* KeychainStore.swift */, + 3BB571FD5EA0CB9C15DBD1DB /* AddBooksViewModel.swift */, + AF5F3C9BA827C89A343166A4 /* AuthViewModel.swift */, + 96C4EA4FE11115BA86AB2802 /* BookDetailViewModel.swift */, + C15179D9ED03860747BBC180 /* CategoryViewModel.swift */, + BEB5F3DC625CEAE1BEC3911F /* HomeViewModel.swift */, + 4DA01B4D0A49FA8E5D1E4F65 /* AddBooksView.swift */, + 471C2927142735752EA378A9 /* BarcodeScannerView.swift */, + 329388F7DE8EB288AEE98A23 /* AuthView.swift */, + D117FECFDDAC2EDDC191CEC9 /* CategoryListView.swift */, + A39116C7E45F12244CA1DC23 /* BookDetailView.swift */, + BF18379D6C3B16C83CCBCF22 /* HomeView.swift */, + 0417E5217F2A37B2065F6DC9 /* Assets.xcassets */, + 20947201FBE7D30CD6F69E38 /* Localizable.strings */, + 1B0A599E6FA80C6DBB852EB5 /* Localizable.strings */, + 95A0AD7A2591540C4ED3252F /* mock_book_remote.json */, + ); + path = Bookibra; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A0CA764B5F4498B2368FB4C7 /* Bookibra */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0D8C0A6CBE24CFFE953CC286 /* Build configuration list for PBXNativeTarget "Bookibra" */; + buildPhases = ( + 4B0B88378B618BDE3340051C /* Sources */, + 96BCE7EBCFE3A1B03D95C0A5 /* Frameworks */, + AD49F7B39587DC1F8AC10D9B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Bookibra; + productName = Bookibra; + productReference = 7B6C1F4EB35DF0216BC86061 /* Bookibra.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1229A2BA11F02DDB82E39253 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + A0CA764B5F4498B2368FB4C7 = { + DevelopmentTeam = S34SFUY9SC; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = AAD88BDD3442AC750E1C238E /* Build configuration list for PBXProject "Bookibra" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 11824F4150C100F76F38DBD8; + productRefGroup = 94F7452CA97CED98C86AF84E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A0CA764B5F4498B2368FB4C7 /* Bookibra */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD49F7B39587DC1F8AC10D9B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E9DFC74E4EA2AC64A343E4C /* Assets.xcassets in Resources */, + B66DAE4BF97BCAC84740B541 /* Localizable.strings in Resources */, + BDE45343461F02318DD86FDB /* Localizable.strings in Resources */, + 6B60107AEE8C2489D19B1D9D /* mock_book_remote.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4B0B88378B618BDE3340051C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B404D223478123428719790C /* AppRouter.swift in Sources */, + 6998E51506433C1B0A647330 /* BookibraApp.swift in Sources */, + 2C2B30975D5DC09342690C43 /* BlurFogOverlay.swift in Sources */, + 4EE9C0CB6C3006780E8CDFA4 /* BookCoverCard.swift in Sources */, + 245681EBBC7EB40F2733DD6B /* NetworkErrorView.swift in Sources */, + CB31E95DBEF85410677B11E3 /* PrimaryPillButton.swift in Sources */, + CD404352E3F12BCAE64E4BAC /* ScrewView.swift in Sources */, + 0987C082DE634D36D5BF03DE /* ShelfSectionView.swift in Sources */, + EC793F00D7851722C0DD1633 /* Theme.swift in Sources */, + 90E97C917EEA4B7B1D09F8FB /* BookRemote.swift in Sources */, + 5DD75F144A0C411F2D7EF9C8 /* LibraryBook.swift in Sources */, + 1DCD42AC02DDABACC54958C5 /* UserProfile.swift in Sources */, + E7D483E62D94D20A511C6967 /* APIClient.swift in Sources */, + F3452503ADD5962494ABB38D /* AuthService.swift in Sources */, + EAA4D823C890C98F37F64E1E /* BooksService.swift in Sources */, + 7C130ABD8F4627EA3C0FB239 /* ImageCache.swift in Sources */, + E1E4040DBBACA7268F84998B /* KeychainStore.swift in Sources */, + 89E2012E58DEB2A6CA3191F9 /* AddBooksViewModel.swift in Sources */, + A5BC1762D555E6DC13C8664E /* AuthViewModel.swift in Sources */, + 7C17970A1EC6CE66AB2B6962 /* BookDetailViewModel.swift in Sources */, + 31B3807F97E8E1512FB5DEEA /* CategoryViewModel.swift in Sources */, + 9DF5677130BA3D0F14C04B4B /* HomeViewModel.swift in Sources */, + D146299A54D7A660951C3075 /* AddBooksView.swift in Sources */, + 44B8242D7211EDD650FCA488 /* BarcodeScannerView.swift in Sources */, + A08E9B72A9FAA96CC58C82B1 /* AuthView.swift in Sources */, + D6D4F249AA8A85A041C7D112 /* CategoryListView.swift in Sources */, + C9656D40284BF3D44322AE99 /* BookDetailView.swift in Sources */, + 7C5391EE19CD4370B0871A6F /* HomeView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 180A253868383C3109315D0F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AFD5A6DD3D2BFC014B9AB859 /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = NO; + DEVELOPMENT_TEAM = S34SFUY9SC; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Bookibra/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.bookibra.ios; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2D538BCF427EE4B9D90BA1E5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AFD5A6DD3D2BFC014B9AB859 /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 17.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.bookibra.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4BC6D305A4C5E17883964793 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 98AAF8E8F7F53CA9CE81186B /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 17.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.bookibra.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + EE4DDF7AD06BABD0D78E43A0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 98AAF8E8F7F53CA9CE81186B /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = NO; + DEVELOPMENT_TEAM = S34SFUY9SC; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Bookibra/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.bookibra.ios; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0D8C0A6CBE24CFFE953CC286 /* Build configuration list for PBXNativeTarget "Bookibra" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE4DDF7AD06BABD0D78E43A0 /* Release */, + 180A253868383C3109315D0F /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AAD88BDD3442AC750E1C238E /* Build configuration list for PBXProject "Bookibra" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2D538BCF427EE4B9D90BA1E5 /* Debug */, + 4BC6D305A4C5E17883964793 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 1229A2BA11F02DDB82E39253 /* Project object */; +} diff --git a/ios/Bookibra.xcodeproj/project.xcworkspace/xcuserdata/wisecolt.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Bookibra.xcodeproj/project.xcworkspace/xcuserdata/wisecolt.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..891df9fb20045d1eb04e31128bdb110d1895c21e GIT binary patch literal 20636 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 IqkPN%1Ae#|;s5{u literal 0 HcmV?d00001 diff --git a/ios/Bookibra.xcodeproj/xcuserdata/wisecolt.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/Bookibra.xcodeproj/xcuserdata/wisecolt.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..0437220 --- /dev/null +++ b/ios/Bookibra.xcodeproj/xcuserdata/wisecolt.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Bookibra.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/ios/Bookibra/App/AppRouter.swift b/ios/Bookibra/App/AppRouter.swift new file mode 100644 index 0000000..d5cbb7a --- /dev/null +++ b/ios/Bookibra/App/AppRouter.swift @@ -0,0 +1,53 @@ +import Foundation +import SwiftUI + +@MainActor +final class AppRouter: ObservableObject { + enum Route: Hashable { + case addBooks + case category(name: String) + case detail(BookRemote) + } + + @Published var isAuthenticated = false + @Published var path: [Route] = [] + + func resetToHome() { + path.removeAll() + } +} + +struct AppDependencies { + let apiClient: APIClientProtocol + let authService: AuthServiceProtocol + let booksService: BooksServiceProtocol + let keychain: KeychainStoreProtocol + let imageCache: ImageCacheProtocol + + static func live() -> AppDependencies { + let config = URLSessionConfiguration.default + config.waitsForConnectivity = false + config.timeoutIntervalForRequest = 8 + config.timeoutIntervalForResource = 15 + let session = URLSession(configuration: config) + let client = APIClient(baseURL: Bundle.main.apiBaseURL, session: session) + return AppDependencies( + apiClient: client, + authService: AuthService(client: client), + booksService: BooksService(client: client), + keychain: KeychainStore(), + imageCache: ImageCache.shared + ) + } +} + +private struct DependenciesKey: EnvironmentKey { + static let defaultValue = AppDependencies.live() +} + +extension EnvironmentValues { + var dependencies: AppDependencies { + get { self[DependenciesKey.self] } + set { self[DependenciesKey.self] = newValue } + } +} diff --git a/ios/Bookibra/App/BookibraApp.swift b/ios/Bookibra/App/BookibraApp.swift new file mode 100644 index 0000000..7b21a34 --- /dev/null +++ b/ios/Bookibra/App/BookibraApp.swift @@ -0,0 +1,72 @@ +import SwiftUI +import SwiftData + +@main +struct BookibraApp: App { + @StateObject private var router = AppRouter() + private let dependencies = AppDependencies.live() + private let container: ModelContainer + + init() { + do { + container = try ModelContainer(for: LibraryBook.self) + } catch { + fatalError("SwiftData container oluşturulamadı: \(error)") + } + } + + var body: some Scene { + WindowGroup { + RootView() + .environmentObject(router) + .environment(\.dependencies, dependencies) + .modelContainer(container) + } + } +} + +private struct RootView: View { + @EnvironmentObject private var router: AppRouter + @Environment(\.dependencies) private var dependencies + + var body: some View { + NavigationStack(path: $router.path) { + Group { + if router.isAuthenticated { + HomeView(viewModel: HomeViewModel()) + } else { + AuthView(viewModel: AuthViewModel(authService: dependencies.authService, keychain: dependencies.keychain)) + } + } + .navigationDestination(for: AppRouter.Route.self) { route in + switch route { + case .addBooks: + AddBooksView(viewModel: AddBooksViewModel(booksService: dependencies.booksService)) + case .category(let name): + CategoryListView(viewModel: CategoryViewModel(categoryName: name)) + case .detail(let book): + BookDetailView(viewModel: BookDetailViewModel(book: book)) + } + } + } + .task { + await bootstrapSession() + } + } + + @MainActor + private func bootstrapSession() async { + let token = dependencies.keychain.read(for: AuthViewModel.tokenKey) + guard let token, !token.isEmpty else { + router.isAuthenticated = false + return + } + + do { + _ = try await dependencies.authService.profile(token: token) + router.isAuthenticated = true + } catch { + router.isAuthenticated = false + } + } +} diff --git a/ios/Bookibra/DesignSystem/Components/BlurFogOverlay.swift b/ios/Bookibra/DesignSystem/Components/BlurFogOverlay.swift new file mode 100644 index 0000000..06ae14f --- /dev/null +++ b/ios/Bookibra/DesignSystem/Components/BlurFogOverlay.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct BlurFogOverlay: View { + var body: some View { + Rectangle() + .fill( + LinearGradient( + colors: [Color.clear, Theme.background.opacity(0.82), Theme.background], + startPoint: .top, + endPoint: .bottom + ) + ) + .background(.ultraThinMaterial) + .ignoresSafeArea(edges: .bottom) + .allowsHitTesting(false) + } +} diff --git a/ios/Bookibra/DesignSystem/Components/BookCoverCard.swift b/ios/Bookibra/DesignSystem/Components/BookCoverCard.swift new file mode 100644 index 0000000..2697ace --- /dev/null +++ b/ios/Bookibra/DesignSystem/Components/BookCoverCard.swift @@ -0,0 +1,57 @@ +import SwiftUI + +struct BookCoverCard: View { + let book: BookRemote + let imageCache: ImageCacheProtocol + + var body: some View { + VStack(spacing: 6) { + RemoteImageView(url: book.coverImageUrl, imageCache: imageCache) + .frame(width: 98, height: 145) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 10) + .stroke(Color.black.opacity(0.08), lineWidth: 1) + } + .shadow(color: .black.opacity(0.16), radius: 6, y: 4) + + Text(book.title) + .font(.caption) + .lineLimit(1) + .foregroundStyle(.primary) + } + .accessibilityElement(children: .combine) + .accessibilityLabel("\(book.title), \(book.authors.joined(separator: ", "))") + } +} + +private struct RemoteImageView: View { + let url: URL? + let imageCache: ImageCacheProtocol + + @State private var image: UIImage? + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(LinearGradient(colors: [Color.gray.opacity(0.22), Color.gray.opacity(0.35)], startPoint: .top, endPoint: .bottom)) + if let image { + Image(uiImage: image) + .resizable() + .scaledToFill() + } else { + Image(systemName: "book.closed") + .font(.title3) + .foregroundStyle(Color.black.opacity(0.35)) + } + } + .task(id: url) { + guard let url else { return } + do { + image = try await imageCache.image(for: url) + } catch { + image = nil + } + } + } +} diff --git a/ios/Bookibra/DesignSystem/Components/NetworkErrorView.swift b/ios/Bookibra/DesignSystem/Components/NetworkErrorView.swift new file mode 100644 index 0000000..b496cbb --- /dev/null +++ b/ios/Bookibra/DesignSystem/Components/NetworkErrorView.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct NetworkErrorView: View { + let message: String + let retryAction: () -> Void + + var body: some View { + VStack(spacing: 12) { + Image(systemName: "wifi.exclamationmark") + .font(.title2) + Text(message) + .font(.subheadline) + .multilineTextAlignment(.center) + Button(String(localized: "common.retry"), action: retryAction) + .buttonStyle(.borderedProminent) + } + .padding() + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) + .padding(.horizontal) + } +} diff --git a/ios/Bookibra/DesignSystem/Components/PrimaryPillButton.swift b/ios/Bookibra/DesignSystem/Components/PrimaryPillButton.swift new file mode 100644 index 0000000..354a1c5 --- /dev/null +++ b/ios/Bookibra/DesignSystem/Components/PrimaryPillButton.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct PrimaryPillButton: View { + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.headline) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .contentShape(Rectangle()) + } + .background(Color.black) + .clipShape(Capsule()) + .shadow(color: .black.opacity(0.28), radius: 18, y: 8) + .accessibilityLabel(title) + } +} diff --git a/ios/Bookibra/DesignSystem/Components/ScrewView.swift b/ios/Bookibra/DesignSystem/Components/ScrewView.swift new file mode 100644 index 0000000..2563054 --- /dev/null +++ b/ios/Bookibra/DesignSystem/Components/ScrewView.swift @@ -0,0 +1,14 @@ +import SwiftUI + +struct ScrewView: View { + var body: some View { + ZStack { + Circle() + .fill(Color.black.opacity(0.25)) + Circle() + .stroke(Color.white.opacity(0.5), lineWidth: 1) + .padding(1) + } + .frame(width: 9, height: 9) + } +} diff --git a/ios/Bookibra/DesignSystem/Components/ShelfSectionView.swift b/ios/Bookibra/DesignSystem/Components/ShelfSectionView.swift new file mode 100644 index 0000000..15d23bd --- /dev/null +++ b/ios/Bookibra/DesignSystem/Components/ShelfSectionView.swift @@ -0,0 +1,85 @@ +import SwiftUI + +struct ShelfSectionView: View { + let title: String + let books: [BookRemote] + let gradient: LinearGradient + let imageCache: ImageCacheProtocol + let onTapCategory: () -> Void + let onTapBook: (BookRemote) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Button(action: onTapCategory) { + Text(title) + .font(.title3.weight(.bold)) + .foregroundStyle(Theme.textPrimary) + } + Spacer() + Text("\(books.count) books") + .font(.caption) + .foregroundStyle(Theme.textSecondary) + Image(systemName: "chevron.left") + .font(.caption2) + .foregroundStyle(.gray) + Image(systemName: "chevron.right") + .font(.caption2) + .foregroundStyle(.gray) + } + + ZStack(alignment: .bottom) { + RoundedRectangle(cornerRadius: 16) + .fill(gradient) + .frame(height: 74) + .shadow(color: .black.opacity(0.12), radius: 10, y: 8) + .overlay { + HStack { + ScrewView() + Spacer() + ScrewView() + } + .padding(.horizontal, 12) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + if books.isEmpty { + ghostCovers + } else { + ForEach(books) { book in + Button { + onTapBook(book) + } label: { + BookCoverCard(book: book, imageCache: imageCache) + } + } + } + } + .padding(.horizontal, 16) + .padding(.bottom, 18) + } + .frame(height: 190) + } + } + .padding(.horizontal, 20) + } + + private var ghostCovers: some View { + HStack(spacing: 10) { + ForEach(0..<3, id: \.self) { _ in + RoundedRectangle(cornerRadius: 10) + .fill(Color.white.opacity(0.5)) + .frame(width: 92, height: 140) + .overlay { + Image(systemName: "book") + .foregroundStyle(.white.opacity(0.8)) + } + } + Text(String(localized: "home.noBooks")) + .font(.caption) + .foregroundStyle(.secondary) + .frame(width: 130, alignment: .leading) + } + } +} diff --git a/ios/Bookibra/DesignSystem/Theme.swift b/ios/Bookibra/DesignSystem/Theme.swift new file mode 100644 index 0000000..8c2c741 --- /dev/null +++ b/ios/Bookibra/DesignSystem/Theme.swift @@ -0,0 +1,29 @@ +import SwiftUI + +enum Theme { + static let background = Color(red: 0.97, green: 0.96, blue: 0.94) + static let textPrimary = Color.black + static let textSecondary = Color.gray + + static let designShelf = LinearGradient( + colors: [Color(red: 0.87, green: 0.66, blue: 0.45), Color(red: 0.79, green: 0.55, blue: 0.34)], + startPoint: .top, + endPoint: .bottom + ) + + static let psychologyShelf = LinearGradient( + colors: [Color(red: 0.58, green: 0.67, blue: 0.78), Color(red: 0.45, green: 0.55, blue: 0.66)], + startPoint: .top, + endPoint: .bottom + ) + + static let novelsShelf = LinearGradient( + colors: [Color.white, Color(red: 0.9, green: 0.9, blue: 0.9)], + startPoint: .top, + endPoint: .bottom + ) + + static func headerSerif(size: CGFloat) -> Font { + .custom("NewYork-Regular", size: size, relativeTo: .largeTitle) + } +} diff --git a/ios/Bookibra/Models/BookRemote.swift b/ios/Bookibra/Models/BookRemote.swift new file mode 100644 index 0000000..432a991 --- /dev/null +++ b/ios/Bookibra/Models/BookRemote.swift @@ -0,0 +1,135 @@ +import Foundation + +struct BookRemote: Codable, Identifiable, Hashable { + var id: String { isbn13 ?? isbn10 ?? title + (authors.first ?? "") } + + let remoteId: String? + let title: String + let authors: [String] + let publishedYear: Int? + let isbn10: String? + let isbn13: String? + let coverImageUrl: URL? + let language: String? + let description: String? + let pageCount: Int? + let categories: [String] + let publisher: String? + let sourceLocale: String? + + init( + remoteId: String? = nil, + title: String, + authors: [String] = [], + publishedYear: Int? = nil, + isbn10: String? = nil, + isbn13: String? = nil, + coverImageUrl: URL? = nil, + language: String? = nil, + description: String? = nil, + pageCount: Int? = nil, + categories: [String] = [], + publisher: String? = nil, + sourceLocale: String? = nil + ) { + self.remoteId = remoteId + self.title = title + self.authors = authors + self.publishedYear = publishedYear + self.isbn10 = isbn10 + self.isbn13 = isbn13 + self.coverImageUrl = coverImageUrl + self.language = language + self.description = description + self.pageCount = pageCount + self.categories = categories + self.publisher = publisher + self.sourceLocale = sourceLocale + } +} + +struct BookSearchResponse: Decodable { + let items: [BookRemote] + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Endpoint 1: /isbn returns { data: { tr: {...}, en: {...} } } + if let localeMap = try? container.decode([String: RawBook].self, forKey: .data) { + self.items = localeMap.map { locale, raw in raw.toBook(locale: locale) } + return + } + + // Endpoint 2/3: /title and /filter returns { data: [{ locale, items: [...] }] } + if let groups = try? container.decode([LocaleGroup].self, forKey: .data) { + self.items = groups.flatMap { group in + group.items.map { $0.toBook(locale: group.locale) } + } + return + } + + self.items = [] + } + + private enum CodingKeys: String, CodingKey { + case data + } +} + +private struct LocaleGroup: Decodable { + let locale: String + let items: [RawBook] +} + +private struct RawBook: Decodable { + let asin: String? + let title: String? + let authorName: String? + let author: String? + let isbn: String? + let thumbImage: String? + let image: String? + let date: String? + let publisher: String? + let page: Int? + let description: String? + let categories: [String]? + let locale: String? + + func toBook(locale: String?) -> BookRemote { + let coverRaw = thumbImage ?? image + let authorsText = authorName ?? author ?? "" + let authors = authorsText + .replacingOccurrences(of: "[Yazar]", with: "") + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + + let isbnClean = isbn?.filter { $0.isNumber || $0.uppercased() == "X" } + let isbn10 = isbnClean?.count == 10 ? isbnClean : nil + let isbn13 = isbnClean?.count == 13 ? isbnClean : nil + + return BookRemote( + remoteId: asin, + title: title ?? "Untitled", + authors: authors, + publishedYear: Self.extractYear(from: date), + isbn10: isbn10, + isbn13: isbn13, + coverImageUrl: coverRaw.flatMap(URL.init(string:)), + language: locale, + description: description, + pageCount: page, + categories: categories ?? [], + publisher: publisher, + sourceLocale: locale + ) + } + + private static func extractYear(from value: String?) -> Int? { + guard let value else { return nil } + let digits = value.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + guard digits.count >= 4 else { return nil } + return Int(String(digits.prefix(4))) + } +} diff --git a/ios/Bookibra/Models/LibraryBook.swift b/ios/Bookibra/Models/LibraryBook.swift new file mode 100644 index 0000000..4fb325a --- /dev/null +++ b/ios/Bookibra/Models/LibraryBook.swift @@ -0,0 +1,63 @@ +import Foundation +import SwiftData + +@Model +final class LibraryBook { + @Attribute(.unique) var localId: UUID + var title: String + var authorsString: String + var coverUrlString: String? + var isbn10: String? + var isbn13: String? + var publishedYear: Int? + var categoriesString: String + var summary: String? + var dateAdded: Date + var language: String? + var sourceLocale: String? + var remotePayloadJson: String? + + init( + localId: UUID = UUID(), + title: String, + authorsString: String, + coverUrlString: String? = nil, + isbn10: String? = nil, + isbn13: String? = nil, + publishedYear: Int? = nil, + categoriesString: String = "", + summary: String? = nil, + dateAdded: Date = .now, + language: String? = nil, + sourceLocale: String? = nil, + remotePayloadJson: String? = nil + ) { + self.localId = localId + self.title = title + self.authorsString = authorsString + self.coverUrlString = coverUrlString + self.isbn10 = isbn10 + self.isbn13 = isbn13 + self.publishedYear = publishedYear + self.categoriesString = categoriesString + self.summary = summary + self.dateAdded = dateAdded + self.language = language + self.sourceLocale = sourceLocale + self.remotePayloadJson = remotePayloadJson + } + + var authors: [String] { + authorsString + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + } + + var categories: [String] { + categoriesString + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + } +} diff --git a/ios/Bookibra/Models/UserProfile.swift b/ios/Bookibra/Models/UserProfile.swift new file mode 100644 index 0000000..3e2d72b --- /dev/null +++ b/ios/Bookibra/Models/UserProfile.swift @@ -0,0 +1,10 @@ +import Foundation + +struct UserProfile: Codable, Equatable { + struct User: Codable, Equatable { + let id: String + let email: String + } + + let user: User +} diff --git a/ios/Bookibra/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/Bookibra/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..6d4c82f --- /dev/null +++ b/ios/Bookibra/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.580", + "green" : "0.424", + "red" : "0.294" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Bookibra/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Bookibra/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..8121323 --- /dev/null +++ b/ios/Bookibra/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Bookibra/Resources/Assets.xcassets/Contents.json b/ios/Bookibra/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ios/Bookibra/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Bookibra/Resources/Debug.xcconfig b/ios/Bookibra/Resources/Debug.xcconfig new file mode 100644 index 0000000..d8e5cff --- /dev/null +++ b/ios/Bookibra/Resources/Debug.xcconfig @@ -0,0 +1 @@ +API_BASE_URL = http://192.168.1.124:8080 diff --git a/ios/Bookibra/Resources/Info.plist b/ios/Bookibra/Resources/Info.plist new file mode 100644 index 0000000..b75bd79 --- /dev/null +++ b/ios/Bookibra/Resources/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + API_BASE_URL + http://192.168.1.124:8080 + NSCameraUsageDescription + ISBN barkodu taramak için kameraya erişim gerekiyor. + + diff --git a/ios/Bookibra/Resources/Release.xcconfig b/ios/Bookibra/Resources/Release.xcconfig new file mode 100644 index 0000000..3dbde3d --- /dev/null +++ b/ios/Bookibra/Resources/Release.xcconfig @@ -0,0 +1 @@ +API_BASE_URL = http://localhost:8080 diff --git a/ios/Bookibra/Resources/en.lproj/Localizable.strings b/ios/Bookibra/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000..e041968 --- /dev/null +++ b/ios/Bookibra/Resources/en.lproj/Localizable.strings @@ -0,0 +1,18 @@ +"auth.login" = "Login"; +"auth.register" = "Register"; +"auth.email" = "Email"; +"auth.password" = "Password"; +"auth.continue" = "Continue"; +"home.myFavourite" = "My Favourite"; +"home.books" = "BOOKS"; +"home.addBooks" = "Add Books"; +"home.noBooks" = "No books yet. Tap Add Books."; +"add.title" = "Add Books"; +"add.searchByTitle" = "Search by Title"; +"add.scanBarcode" = "Scan Barcode"; +"add.filter" = "Filter"; +"add.searchPlaceholder" = "Type book title"; +"detail.add" = "Add to My Books"; +"detail.remove" = "Remove from My Books"; +"common.retry" = "Retry"; +"common.empty" = "No results"; diff --git a/ios/Bookibra/Resources/mock_book_remote.json b/ios/Bookibra/Resources/mock_book_remote.json new file mode 100644 index 0000000..c6c49f6 --- /dev/null +++ b/ios/Bookibra/Resources/mock_book_remote.json @@ -0,0 +1,21 @@ +{ + "data": [ + { + "locale": "tr", + "items": [ + { + "asin": "6053757810", + "title": "Fahrenheit 451", + "authorName": "Ray Bradbury", + "isbn": "9786053757818", + "thumbImage": "https://example.com/cover.jpg", + "date": "2018", + "publisher": "Ithaki", + "page": 280, + "description": "Distopik klasik.", + "categories": ["Novels", "Science Fiction"] + } + ] + } + ] +} diff --git a/ios/Bookibra/Resources/tr.lproj/Localizable.strings b/ios/Bookibra/Resources/tr.lproj/Localizable.strings new file mode 100644 index 0000000..23da8e9 --- /dev/null +++ b/ios/Bookibra/Resources/tr.lproj/Localizable.strings @@ -0,0 +1,18 @@ +"auth.login" = "Giriş Yap"; +"auth.register" = "Kayıt Ol"; +"auth.email" = "E-posta"; +"auth.password" = "Şifre"; +"auth.continue" = "Devam Et"; +"home.myFavourite" = "My Favourite"; +"home.books" = "BOOKS"; +"home.addBooks" = "Kitap Ekle"; +"home.noBooks" = "Henüz kitap yok. Kitap Ekle'ye dokunun."; +"add.title" = "Kitap Ekle"; +"add.searchByTitle" = "Başlığa Göre"; +"add.scanBarcode" = "Barkod Tara"; +"add.filter" = "Filtre"; +"add.searchPlaceholder" = "Kitap adı yazın"; +"detail.add" = "Kitaplığıma Ekle"; +"detail.remove" = "Kitaplığımdan Kaldır"; +"common.retry" = "Tekrar Dene"; +"common.empty" = "Sonuç bulunamadı"; diff --git a/ios/Bookibra/Services/APIClient.swift b/ios/Bookibra/Services/APIClient.swift new file mode 100644 index 0000000..2836f55 --- /dev/null +++ b/ios/Bookibra/Services/APIClient.swift @@ -0,0 +1,129 @@ +import Foundation + +protocol APIClientProtocol { + func get(path: String, queryItems: [URLQueryItem], token: String?) async throws -> T + func post(path: String, body: Body, token: String?) async throws -> T +} + +enum APIError: LocalizedError { + case invalidURL + case invalidResponse + case unauthorized + case server(status: Int, message: String) + case decoding(Error) + case transport(Error) + + var errorDescription: String? { + switch self { + case .invalidURL: return "Geçersiz URL" + case .invalidResponse: return "Geçersiz sunucu yanıtı" + case .unauthorized: return "Oturum süresi doldu" + case .server(_, let message): return message + case .decoding: return "Sunucu verisi çözümlenemedi" + case .transport(let error): return error.localizedDescription + } + } +} + +final class APIClient: APIClientProtocol { + private let baseURL: URL + private let session: URLSession + private let decoder: JSONDecoder + private let encoder: JSONEncoder + + init(baseURL: URL, session: URLSession = .shared) { + self.baseURL = baseURL + self.session = session + self.decoder = JSONDecoder() + self.encoder = JSONEncoder() + } + + func get(path: String, queryItems: [URLQueryItem] = [], token: String? = nil) async throws -> T { + var request = try buildRequest(path: path, method: "GET", queryItems: queryItems, token: token) + request.httpBody = nil + return try await perform(request) + } + + func post(path: String, body: Body, token: String? = nil) async throws -> T { + var request = try buildRequest(path: path, method: "POST", queryItems: [], token: token) + request.httpBody = try encoder.encode(body) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + return try await perform(request) + } + + private func buildRequest(path: String, method: String, queryItems: [URLQueryItem], token: String?) throws -> URLRequest { + guard var components = URLComponents(url: baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: false) else { + throw APIError.invalidURL + } + if !queryItems.isEmpty { + components.queryItems = queryItems + } + guard let url = components.url else { throw APIError.invalidURL } + + var request = URLRequest(url: url) + request.httpMethod = method + request.timeoutInterval = 20 + if let token { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + return request + } + + private func perform(_ request: URLRequest) async throws -> T { + #if DEBUG + print("[API] \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") + #endif + + do { + let (data, response) = try await session.data(for: request) + guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse } + + if http.statusCode == 401 { throw APIError.unauthorized } + + guard (200...299).contains(http.statusCode) else { + let message = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["message"] as? String ?? "Sunucu hatası" + throw APIError.server(status: http.statusCode, message: message) + } + + do { + return try decoder.decode(T.self, from: data) + } catch { + throw APIError.decoding(error) + } + } catch let error as APIError { + throw error + } catch { + throw APIError.transport(error) + } + } +} + +extension Bundle { + var apiBaseURL: URL { + let raw = (object(forInfoDictionaryKey: "API_BASE_URL") as? String)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + // 1) Normal case: full URL in Info.plist / xcconfig. + if let url = URL(string: raw), let host = url.host, !host.isEmpty { + return url + } + + // 2) If scheme is missing (e.g. "192.168.1.124:8080"), prepend http://. + if !raw.isEmpty, !raw.contains("://"), + let url = URL(string: "http://\(raw)"), + let host = url.host, !host.isEmpty { + return url + } + + // 3) Device-local fallback for current dev network. + if let fallback = URL(string: "http://192.168.1.124:8080") { + #if DEBUG + print("[API] Invalid API_BASE_URL='\(raw)'. Falling back to \(fallback.absoluteString)") + #endif + return fallback + } + + // 4) Last resort. + return URL(string: "http://localhost:8080")! + } +} diff --git a/ios/Bookibra/Services/AuthService.swift b/ios/Bookibra/Services/AuthService.swift new file mode 100644 index 0000000..9c2e7d0 --- /dev/null +++ b/ios/Bookibra/Services/AuthService.swift @@ -0,0 +1,42 @@ +import Foundation + +struct AuthResponse: Codable { + struct User: Codable { + let id: String + let email: String + } + + let token: String + let user: User +} + +protocol AuthServiceProtocol { + func login(email: String, password: String) async throws -> AuthResponse + func register(email: String, password: String) async throws -> AuthResponse + func profile(token: String) async throws -> UserProfile +} + +final class AuthService: AuthServiceProtocol { + private let client: APIClientProtocol + + init(client: APIClientProtocol) { + self.client = client + } + + func login(email: String, password: String) async throws -> AuthResponse { + try await client.post(path: "api/auth/login", body: Credentials(email: email, password: password), token: nil) + } + + func register(email: String, password: String) async throws -> AuthResponse { + try await client.post(path: "api/auth/register", body: Credentials(email: email, password: password), token: nil) + } + + func profile(token: String) async throws -> UserProfile { + try await client.get(path: "api/auth/profile", queryItems: [], token: token) + } +} + +private struct Credentials: Codable { + let email: String + let password: String +} diff --git a/ios/Bookibra/Services/BooksService.swift b/ios/Bookibra/Services/BooksService.swift new file mode 100644 index 0000000..3823726 --- /dev/null +++ b/ios/Bookibra/Services/BooksService.swift @@ -0,0 +1,52 @@ +import Foundation + +protocol BooksServiceProtocol { + func searchByTitle(_ title: String, locales: String) async throws -> [BookRemote] + func searchByISBN(_ isbn: String, locales: String) async throws -> [BookRemote] + func filter(title: String, year: String, locales: String) async throws -> [BookRemote] +} + +final class BooksService: BooksServiceProtocol { + private let client: APIClientProtocol + + init(client: APIClientProtocol) { + self.client = client + } + + func searchByTitle(_ title: String, locales: String = "tr,en") async throws -> [BookRemote] { + let response: BookSearchResponse = try await client.get( + path: "api/books/title", + queryItems: [ + .init(name: "title", value: title), + .init(name: "locales", value: locales) + ], + token: nil + ) + return response.items + } + + func searchByISBN(_ isbn: String, locales: String = "tr,en") async throws -> [BookRemote] { + let response: BookSearchResponse = try await client.get( + path: "api/books/isbn/\(isbn)", + queryItems: [ + .init(name: "locales", value: locales), + .init(name: "withGemini", value: "false") + ], + token: nil + ) + return response.items + } + + func filter(title: String, year: String, locales: String = "tr,en") async throws -> [BookRemote] { + let response: BookSearchResponse = try await client.get( + path: "api/books/filter", + queryItems: [ + .init(name: "title", value: title), + .init(name: "published", value: year), + .init(name: "locales", value: locales) + ], + token: nil + ) + return response.items + } +} diff --git a/ios/Bookibra/Services/ImageCache.swift b/ios/Bookibra/Services/ImageCache.swift new file mode 100644 index 0000000..b66f76d --- /dev/null +++ b/ios/Bookibra/Services/ImageCache.swift @@ -0,0 +1,34 @@ +import Foundation +import UIKit + +protocol ImageCacheProtocol { + func image(for url: URL) async throws -> UIImage +} + +final class ImageCache: ImageCacheProtocol { + static let shared = ImageCache() + + private let cache = NSCache() + private let session: URLSession + + init(session: URLSession = .shared) { + self.session = session + cache.countLimit = 200 + } + + func image(for url: URL) async throws -> UIImage { + if let existing = cache.object(forKey: url as NSURL) { + return existing + } + + let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 20) + let (data, _) = try await session.data(for: request) + + guard let image = UIImage(data: data) else { + throw APIError.invalidResponse + } + + cache.setObject(image, forKey: url as NSURL) + return image + } +} diff --git a/ios/Bookibra/Services/KeychainStore.swift b/ios/Bookibra/Services/KeychainStore.swift new file mode 100644 index 0000000..22d9bb8 --- /dev/null +++ b/ios/Bookibra/Services/KeychainStore.swift @@ -0,0 +1,47 @@ +import Foundation +import Security + +protocol KeychainStoreProtocol { + func save(_ value: String, for key: String) -> Bool + func read(for key: String) -> String? + func delete(for key: String) -> Bool +} + +final class KeychainStore: KeychainStoreProtocol { + func save(_ value: String, for key: String) -> Bool { + let data = Data(value.utf8) + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key, + kSecValueData: data + ] + + SecItemDelete(query as CFDictionary) + return SecItemAdd(query as CFDictionary, nil) == errSecSuccess + } + + func read(for key: String) -> String? { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne + ] + + var item: CFTypeRef? + guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, + let data = item as? Data else { + return nil + } + + return String(data: data, encoding: .utf8) + } + + func delete(for key: String) -> Bool { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key + ] + return SecItemDelete(query as CFDictionary) == errSecSuccess + } +} diff --git a/ios/Bookibra/ViewModels/AddBooksViewModel.swift b/ios/Bookibra/ViewModels/AddBooksViewModel.swift new file mode 100644 index 0000000..45b668c --- /dev/null +++ b/ios/Bookibra/ViewModels/AddBooksViewModel.swift @@ -0,0 +1,88 @@ +import Foundation + +@MainActor +final class AddBooksViewModel: ObservableObject { + enum Mode: String, CaseIterable { + case title + case scan + case filter + + var title: String { + switch self { + case .title: return String(localized: "add.searchByTitle") + case .scan: return String(localized: "add.scanBarcode") + case .filter: return String(localized: "add.filter") + } + } + } + + @Published var mode: Mode = .title + @Published var titleQuery = "" + @Published var filterTitle = "" + @Published var filterYear = "" + @Published var results: [BookRemote] = [] + @Published var isLoading = false + @Published var errorMessage: String? + + private var debounceTask: Task? + private let booksService: BooksServiceProtocol + + init(booksService: BooksServiceProtocol) { + self.booksService = booksService + } + + func titleChanged() { + debounceTask?.cancel() + + let query = titleQuery.trimmingCharacters(in: .whitespacesAndNewlines) + guard query.count >= 2 else { + results = [] + return + } + + debounceTask = Task { + try? await Task.sleep(for: .milliseconds(400)) + await searchByTitle(query) + } + } + + func searchByTitle(_ query: String? = nil) async { + let value = query ?? titleQuery + guard !value.isEmpty else { return } + isLoading = true + defer { isLoading = false } + + do { + results = try await booksService.searchByTitle(value, locales: "tr,en") + errorMessage = nil + } catch { + errorMessage = error.localizedDescription + } + } + + func searchByISBN(_ isbn: String) async { + guard !isbn.isEmpty else { return } + isLoading = true + defer { isLoading = false } + + do { + results = try await booksService.searchByISBN(isbn, locales: "tr,en") + errorMessage = nil + } catch { + errorMessage = error.localizedDescription + } + } + + func applyFilter() async { + guard !filterTitle.isEmpty, !filterYear.isEmpty else { return } + isLoading = true + defer { isLoading = false } + + do { + results = try await booksService.filter(title: filterTitle, year: filterYear, locales: "tr,en") + errorMessage = nil + } catch { + errorMessage = error.localizedDescription + } + } +} diff --git a/ios/Bookibra/ViewModels/AuthViewModel.swift b/ios/Bookibra/ViewModels/AuthViewModel.swift new file mode 100644 index 0000000..719181e --- /dev/null +++ b/ios/Bookibra/ViewModels/AuthViewModel.swift @@ -0,0 +1,53 @@ +import Foundation + +@MainActor +final class AuthViewModel: ObservableObject { + enum Mode: String, CaseIterable { + case login + case register + } + + static let tokenKey = "bookibra.jwt" + static let emailKey = "bookibra.email" + + @Published var mode: Mode = .login + @Published var email = "" + @Published var password = "" + @Published var isLoading = false + @Published var errorMessage: String? + + private let authService: AuthServiceProtocol + private let keychain: KeychainStoreProtocol + + init(authService: AuthServiceProtocol, keychain: KeychainStoreProtocol) { + self.authService = authService + self.keychain = keychain + } + + func submit(onSuccess: @escaping () -> Void) async { + guard !email.isEmpty, !password.isEmpty else { + errorMessage = "E-posta ve şifre gerekli" + return + } + + isLoading = true + defer { isLoading = false } + + do { + let response: AuthResponse + switch mode { + case .login: + response = try await authService.login(email: email, password: password) + case .register: + response = try await authService.register(email: email, password: password) + } + + _ = try await authService.profile(token: response.token) + _ = keychain.save(response.token, for: Self.tokenKey) + _ = keychain.save(response.user.email, for: Self.emailKey) + onSuccess() + } catch { + errorMessage = error.localizedDescription + } + } +} diff --git a/ios/Bookibra/ViewModels/BookDetailViewModel.swift b/ios/Bookibra/ViewModels/BookDetailViewModel.swift new file mode 100644 index 0000000..cac93a0 --- /dev/null +++ b/ios/Bookibra/ViewModels/BookDetailViewModel.swift @@ -0,0 +1,55 @@ +import Foundation +import SwiftData +import UIKit + +@MainActor +final class BookDetailViewModel: ObservableObject { + @Published var book: BookRemote + @Published var isInLibrary = false + + init(book: BookRemote) { + self.book = book + } + + func refresh(context: ModelContext) { + let all = (try? context.fetch(FetchDescriptor())) ?? [] + isInLibrary = all.contains(where: { local in + match(local: local, remote: book) + }) + } + + func toggleLibrary(context: ModelContext) { + let all = (try? context.fetch(FetchDescriptor())) ?? [] + + if let existing = all.first(where: { match(local: $0, remote: book) }) { + context.delete(existing) + isInLibrary = false + UINotificationFeedbackGenerator().notificationOccurred(.warning) + } else { + let local = LibraryBook( + title: book.title, + authorsString: book.authors.joined(separator: ", "), + coverUrlString: book.coverImageUrl?.absoluteString, + isbn10: book.isbn10, + isbn13: book.isbn13, + publishedYear: book.publishedYear, + categoriesString: book.categories.joined(separator: ", "), + summary: book.description, + language: book.language, + sourceLocale: book.sourceLocale, + remotePayloadJson: nil + ) + context.insert(local) + isInLibrary = true + UINotificationFeedbackGenerator().notificationOccurred(.success) + } + + try? context.save() + } + + private func match(local: LibraryBook, remote: BookRemote) -> Bool { + if let lhs = local.isbn13, let rhs = remote.isbn13 { return lhs == rhs } + if let lhs = local.isbn10, let rhs = remote.isbn10 { return lhs == rhs } + return local.title == remote.title + } +} diff --git a/ios/Bookibra/ViewModels/CategoryViewModel.swift b/ios/Bookibra/ViewModels/CategoryViewModel.swift new file mode 100644 index 0000000..155165f --- /dev/null +++ b/ios/Bookibra/ViewModels/CategoryViewModel.swift @@ -0,0 +1,39 @@ +import Foundation + +@MainActor +final class CategoryViewModel: ObservableObject { + enum SortOption: String, CaseIterable { + case recentlyAdded = "Recently Added" + case titleAZ = "Title A-Z" + case author = "Author" + } + + let categoryName: String + @Published var searchText = "" + @Published var sortOption: SortOption = .recentlyAdded + + init(categoryName: String) { + self.categoryName = categoryName + } + + func books(from allBooks: [LibraryBook]) -> [LibraryBook] { + var filtered = allBooks.filter { $0.categories.contains(categoryName) || (categoryName == "Design" && $0.categories.isEmpty) } + + if !searchText.isEmpty { + filtered = filtered.filter { + $0.title.localizedCaseInsensitiveContains(searchText) + || $0.authorsString.localizedCaseInsensitiveContains(searchText) + } + } + + switch sortOption { + case .recentlyAdded: + filtered.sort { $0.dateAdded > $1.dateAdded } + case .titleAZ: + filtered.sort { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } + case .author: + filtered.sort { $0.authorsString.localizedCaseInsensitiveCompare($1.authorsString) == .orderedAscending } + } + return filtered + } +} diff --git a/ios/Bookibra/ViewModels/HomeViewModel.swift b/ios/Bookibra/ViewModels/HomeViewModel.swift new file mode 100644 index 0000000..9195041 --- /dev/null +++ b/ios/Bookibra/ViewModels/HomeViewModel.swift @@ -0,0 +1,61 @@ +import Foundation +import SwiftUI + +struct ShelfCategory: Identifiable { + let id: String + let name: String + let books: [BookRemote] +} + +@MainActor +final class HomeViewModel: ObservableObject { + @Published var categories: [ShelfCategory] = [] + + func refresh(from localBooks: [LibraryBook]) { + var map: [String: [BookRemote]] = [:] + for local in localBooks { + let targets = local.categories.isEmpty ? ["Design"] : local.categories + let remote = Self.makeRemote(from: local) + for name in targets { + map[name, default: []].append(remote) + } + } + + let preferred = ["Design", "Psychology", "Novels"] + var built = preferred.map { name in + ShelfCategory(id: name, name: name, books: map[name] ?? []) + } + + let extras = map.keys + .filter { !preferred.contains($0) } + .sorted() + .map { ShelfCategory(id: $0, name: $0, books: map[$0] ?? []) } + built.append(contentsOf: extras) + + categories = built + } + + func gradient(for name: String) -> LinearGradient { + switch name.lowercased() { + case "design": return Theme.designShelf + case "psychology": return Theme.psychologyShelf + case "novels": return Theme.novelsShelf + default: return Theme.novelsShelf + } + } + + private static func makeRemote(from local: LibraryBook) -> BookRemote { + BookRemote( + title: local.title, + authors: local.authors, + publishedYear: local.publishedYear, + isbn10: local.isbn10, + isbn13: local.isbn13, + coverImageUrl: local.coverUrlString.flatMap(URL.init(string:)), + language: local.language, + description: local.summary, + categories: local.categories, + sourceLocale: local.sourceLocale + ) + } +} diff --git a/ios/Bookibra/Views/AddBooks/AddBooksView.swift b/ios/Bookibra/Views/AddBooks/AddBooksView.swift new file mode 100644 index 0000000..bdcfda6 --- /dev/null +++ b/ios/Bookibra/Views/AddBooks/AddBooksView.swift @@ -0,0 +1,115 @@ +import SwiftUI + +struct AddBooksView: View { + @EnvironmentObject private var router: AppRouter + @ObservedObject var viewModel: AddBooksViewModel + + var body: some View { + VStack(spacing: 12) { + Picker("Mode", selection: $viewModel.mode) { + ForEach(AddBooksViewModel.Mode.allCases, id: \.self) { mode in + Text(mode.title).tag(mode) + } + } + .pickerStyle(.segmented) + + Group { + switch viewModel.mode { + case .title: + titleSearch + case .scan: + scanSearch + case .filter: + filterSearch + } + } + + if let error = viewModel.errorMessage { + NetworkErrorView(message: error) { + Task { await viewModel.searchByTitle() } + } + } + + List(viewModel.results, id: \.id) { book in + Button { + router.path.append(.detail(book)) + } label: { + HStack(spacing: 12) { + AsyncImage(url: book.coverImageUrl) { phase in + if let image = phase.image { + image.resizable().scaledToFill() + } else { + RoundedRectangle(cornerRadius: 8).fill(.gray.opacity(0.2)) + } + } + .frame(width: 48, height: 72) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + VStack(alignment: .leading, spacing: 4) { + Text(book.title) + .font(.headline) + .lineLimit(2) + Text(book.authors.joined(separator: ", ")) + .font(.subheadline) + .foregroundStyle(.secondary) + .lineLimit(1) + if let year = book.publishedYear { + Text("\(year)") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + } + .listStyle(.plain) + .overlay { + if viewModel.results.isEmpty, !viewModel.isLoading { + Text(String(localized: "common.empty")) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + } + .padding(.horizontal, 16) + .navigationTitle(String(localized: "add.title")) + .navigationBarTitleDisplayMode(.inline) + .overlay { + if viewModel.isLoading { + ProgressView() + .controlSize(.large) + } + } + } + + private var titleSearch: some View { + TextField(String(localized: "add.searchPlaceholder"), text: $viewModel.titleQuery) + .textFieldStyle(.roundedBorder) + .onChange(of: viewModel.titleQuery) { _, _ in + viewModel.titleChanged() + } + } + + private var scanSearch: some View { + BarcodeScannerView { isbn in + Task { await viewModel.searchByISBN(isbn) } + } + .frame(height: 260) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + + private var filterSearch: some View { + VStack(spacing: 8) { + TextField("Title", text: $viewModel.filterTitle) + .textFieldStyle(.roundedBorder) + TextField("YYYY", text: $viewModel.filterYear) + .textFieldStyle(.roundedBorder) + .keyboardType(.numberPad) + Button("Apply") { + Task { await viewModel.applyFilter() } + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity, alignment: .trailing) + } + } +} diff --git a/ios/Bookibra/Views/AddBooks/BarcodeScannerView.swift b/ios/Bookibra/Views/AddBooks/BarcodeScannerView.swift new file mode 100644 index 0000000..c6b1a5c --- /dev/null +++ b/ios/Bookibra/Views/AddBooks/BarcodeScannerView.swift @@ -0,0 +1,154 @@ +import SwiftUI +import AVFoundation +import VisionKit + +struct BarcodeScannerView: View { + let onScanned: (String) -> Void + + var body: some View { + Group { + if DataScannerViewController.isSupported, DataScannerViewController.isAvailable { + DataScannerRepresentable(onScanned: onScanned) + } else { + AVScannerRepresentable(onScanned: onScanned) + } + } + .overlay { + RoundedRectangle(cornerRadius: 16) + .stroke(Color.white.opacity(0.75), lineWidth: 2) + } + .accessibilityLabel("ISBN barkod tarayıcı") + } +} + +@available(iOS 16.0, *) +private struct DataScannerRepresentable: UIViewControllerRepresentable { + let onScanned: (String) -> Void + + func makeCoordinator() -> Coordinator { Coordinator(onScanned: onScanned) } + + func makeUIViewController(context: Context) -> DataScannerViewController { + let vc = DataScannerViewController( + recognizedDataTypes: [.barcode(symbologies: [.ean8, .ean13, .upce, .code128])], + qualityLevel: .balanced, + recognizesMultipleItems: false, + isHighFrameRateTrackingEnabled: true, + isPinchToZoomEnabled: true, + isGuidanceEnabled: true, + isHighlightingEnabled: true + ) + vc.delegate = context.coordinator + try? vc.startScanning() + return vc + } + + func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {} + + final class Coordinator: NSObject, DataScannerViewControllerDelegate { + let onScanned: (String) -> Void + private var lastISBN: String? + private var lastEmitAt: Date = .distantPast + + init(onScanned: @escaping (String) -> Void) { + self.onScanned = onScanned + } + + func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) { + guard case .barcode(let code) = item, + let payload = code.payloadStringValue, + let normalized = ISBNNormalizer.normalize(payload) else { return } + emitIfNeeded(normalized) + } + + func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) { + guard let first = addedItems.first, + case .barcode(let code) = first, + let payload = code.payloadStringValue, + let normalized = ISBNNormalizer.normalize(payload) else { return } + emitIfNeeded(normalized) + } + + private func emitIfNeeded(_ isbn: String) { + let now = Date() + if lastISBN == isbn, now.timeIntervalSince(lastEmitAt) < 1.5 { + return + } + lastISBN = isbn + lastEmitAt = now + onScanned(isbn) + } + } +} + +private struct AVScannerRepresentable: UIViewRepresentable { + let onScanned: (String) -> Void + + func makeUIView(context: Context) -> ScannerPreviewView { + let view = ScannerPreviewView() + context.coordinator.configure(preview: view) + return view + } + + func updateUIView(_ uiView: ScannerPreviewView, context: Context) {} + + func makeCoordinator() -> Coordinator { Coordinator(onScanned: onScanned) } + + final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { + private let session = AVCaptureSession() + private let sessionQueue = DispatchQueue(label: "bookibra.av.capture.session") + private let onScanned: (String) -> Void + private var lastISBN: String? + private var lastEmitAt: Date = .distantPast + + init(onScanned: @escaping (String) -> Void) { + self.onScanned = onScanned + } + + func configure(preview: ScannerPreviewView) { + guard let device = AVCaptureDevice.default(for: .video), + let input = try? AVCaptureDeviceInput(device: device) else { return } + + if session.canAddInput(input) { session.addInput(input) } + + let output = AVCaptureMetadataOutput() + if session.canAddOutput(output) { + session.addOutput(output) + output.setMetadataObjectsDelegate(self, queue: .main) + output.metadataObjectTypes = [.ean8, .ean13, .upce, .code128] + } + + preview.previewLayer.session = session + preview.previewLayer.videoGravity = .resizeAspectFill + sessionQueue.async { [session] in + session.startRunning() + } + } + + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + guard let code = metadataObjects.compactMap({ $0 as? AVMetadataMachineReadableCodeObject }).first, + let value = code.stringValue, + let normalized = ISBNNormalizer.normalize(value) else { return } + let now = Date() + if lastISBN == normalized, now.timeIntervalSince(lastEmitAt) < 1.5 { + return + } + lastISBN = normalized + lastEmitAt = now + onScanned(normalized) + } + } +} + +private final class ScannerPreviewView: UIView { + override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self } + var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer } +} + +enum ISBNNormalizer { + static func normalize(_ value: String) -> String? { + let cleaned = value.uppercased().filter { $0.isNumber || $0 == "X" } + if cleaned.count == 13 { return cleaned } + if cleaned.count == 10 { return cleaned } + return nil + } +} diff --git a/ios/Bookibra/Views/Auth/AuthView.swift b/ios/Bookibra/Views/Auth/AuthView.swift new file mode 100644 index 0000000..b0adbd8 --- /dev/null +++ b/ios/Bookibra/Views/Auth/AuthView.swift @@ -0,0 +1,71 @@ +import SwiftUI + +struct AuthView: View { + @EnvironmentObject private var router: AppRouter + @ObservedObject var viewModel: AuthViewModel + + var body: some View { + VStack(spacing: 24) { + Spacer() + + Text("Bookibra") + .font(Theme.headerSerif(size: 48)) + .foregroundStyle(.black) + + Picker("Mode", selection: $viewModel.mode) { + Text(String(localized: "auth.login")).tag(AuthViewModel.Mode.login) + Text(String(localized: "auth.register")).tag(AuthViewModel.Mode.register) + } + .pickerStyle(.segmented) + .padding(.horizontal, 24) + + VStack(spacing: 14) { + TextField(String(localized: "auth.email"), text: $viewModel.email) + .textInputAutocapitalization(.never) + .keyboardType(.emailAddress) + .padding() + .background(Color.white, in: RoundedRectangle(cornerRadius: 12)) + + SecureField(String(localized: "auth.password"), text: $viewModel.password) + .padding() + .background(Color.white, in: RoundedRectangle(cornerRadius: 12)) + } + .padding(.horizontal, 24) + + if let error = viewModel.errorMessage { + Text(error) + .font(.footnote) + .foregroundStyle(.red) + .padding(.horizontal, 24) + .multilineTextAlignment(.center) + } + + Button { + Task { + await viewModel.submit { + router.isAuthenticated = true + } + } + } label: { + if viewModel.isLoading { + ProgressView() + .tint(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + } else { + Text(String(localized: "auth.continue")) + .font(.headline) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + } + } + .background(Color.black) + .clipShape(Capsule()) + .padding(.horizontal, 24) + + Spacer() + } + .background(Theme.background.ignoresSafeArea()) + } +} diff --git a/ios/Bookibra/Views/Category/CategoryListView.swift b/ios/Bookibra/Views/Category/CategoryListView.swift new file mode 100644 index 0000000..613a8c6 --- /dev/null +++ b/ios/Bookibra/Views/Category/CategoryListView.swift @@ -0,0 +1,71 @@ +import SwiftUI +import SwiftData + +struct CategoryListView: View { + @EnvironmentObject private var router: AppRouter + @Query(sort: \LibraryBook.dateAdded, order: .reverse) private var allBooks: [LibraryBook] + @ObservedObject var viewModel: CategoryViewModel + + var body: some View { + let books = viewModel.books(from: allBooks) + + VStack { + HStack { + TextField("Search", text: $viewModel.searchText) + .textFieldStyle(.roundedBorder) + Menu { + ForEach(CategoryViewModel.SortOption.allCases, id: \.self) { option in + Button(option.rawValue) { viewModel.sortOption = option } + } + } label: { + Image(systemName: "arrow.up.arrow.down.circle") + .font(.title3) + } + } + .padding(.horizontal) + + ScrollView { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 104), spacing: 12)], spacing: 16) { + ForEach(books, id: \.localId) { book in + let remote = BookRemote( + title: book.title, + authors: book.authors, + publishedYear: book.publishedYear, + isbn10: book.isbn10, + isbn13: book.isbn13, + coverImageUrl: book.coverUrlString.flatMap(URL.init(string:)), + language: book.language, + description: book.summary, + categories: book.categories + ) + + Button { + router.path.append(.detail(remote)) + } label: { + VStack(alignment: .leading, spacing: 6) { + AsyncImage(url: remote.coverImageUrl) { phase in + if let image = phase.image { + image.resizable().scaledToFill() + } else { + RoundedRectangle(cornerRadius: 10).fill(Color.gray.opacity(0.2)) + } + } + .frame(height: 154) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + Text(book.title) + .font(.caption) + .lineLimit(2) + .foregroundStyle(.primary) + } + } + } + } + .padding(.horizontal) + } + } + .navigationTitle(viewModel.categoryName) + .navigationBarTitleDisplayMode(.inline) + .background(Theme.background.ignoresSafeArea()) + } +} diff --git a/ios/Bookibra/Views/Detail/BookDetailView.swift b/ios/Bookibra/Views/Detail/BookDetailView.swift new file mode 100644 index 0000000..1fab297 --- /dev/null +++ b/ios/Bookibra/Views/Detail/BookDetailView.swift @@ -0,0 +1,58 @@ +import SwiftUI +import SwiftData + +struct BookDetailView: View { + @Environment(\.modelContext) private var modelContext + @ObservedObject var viewModel: BookDetailViewModel + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + AsyncImage(url: viewModel.book.coverImageUrl) { phase in + if let image = phase.image { + image.resizable().scaledToFill() + } else { + RoundedRectangle(cornerRadius: 16).fill(.gray.opacity(0.2)) + } + } + .frame(maxWidth: .infinity) + .frame(height: 320) + .clipShape(RoundedRectangle(cornerRadius: 16)) + + Text(viewModel.book.title) + .font(.title.bold()) + Text(viewModel.book.authors.joined(separator: ", ")) + .font(.headline) + .foregroundStyle(.secondary) + + if let year = viewModel.book.publishedYear { + Text("\(year)") + .font(.subheadline) + } + + if !viewModel.book.categories.isEmpty { + Text(viewModel.book.categories.joined(separator: " • ")) + .font(.subheadline) + .foregroundStyle(.secondary) + } + + if let description = viewModel.book.description { + Text(description) + .font(.body) + } + + PrimaryPillButton( + title: viewModel.isInLibrary ? String(localized: "detail.remove") : String(localized: "detail.add") + ) { + viewModel.toggleLibrary(context: modelContext) + } + .padding(.top, 8) + } + .padding(20) + } + .background(Theme.background.ignoresSafeArea()) + .task { + viewModel.refresh(context: modelContext) + } + } +} diff --git a/ios/Bookibra/Views/Home/HomeView.swift b/ios/Bookibra/Views/Home/HomeView.swift new file mode 100644 index 0000000..8be5a1c --- /dev/null +++ b/ios/Bookibra/Views/Home/HomeView.swift @@ -0,0 +1,73 @@ +import SwiftUI +import SwiftData + +struct HomeView: View { + @EnvironmentObject private var router: AppRouter + @Environment(\.dependencies) private var dependencies + @Query(sort: \LibraryBook.dateAdded, order: .reverse) private var libraryBooks: [LibraryBook] + @StateObject private var viewModel: HomeViewModel + + init(viewModel: HomeViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + ScrollView { + VStack(spacing: 28) { + header + + ForEach(viewModel.categories) { category in + ShelfSectionView( + title: category.name, + books: category.books, + gradient: viewModel.gradient(for: category.name), + imageCache: dependencies.imageCache, + onTapCategory: { router.path.append(.category(name: category.name)) }, + onTapBook: { router.path.append(.detail($0)) } + ) + } + } + .padding(.top, 16) + .padding(.bottom, 120) + } + .background(Theme.background.ignoresSafeArea()) + .safeAreaInset(edge: .bottom, spacing: 0) { + ZStack { + BlurFogOverlay() + .frame(height: 96) + PrimaryPillButton(title: String(localized: "home.addBooks")) { + router.path.append(.addBooks) + } + .padding(.horizontal, 24) + .padding(.bottom, 12) + } + .frame(height: 100) + } + .onAppear { + viewModel.refresh(from: libraryBooks) + } + .onChange(of: libraryBooks.map(\.localId)) { _, _ in + viewModel.refresh(from: libraryBooks) + } + .task(id: libraryBooks.count) { + viewModel.refresh(from: libraryBooks) + } + } + + private var header: some View { + VStack(spacing: 4) { + Text(String(localized: "home.myFavourite")) + .font(.footnote.weight(.light)) + .kerning(1.2) + .foregroundStyle(Color.black.opacity(0.7)) + + Text(String(localized: "home.books")) + .font(Theme.headerSerif(size: 56).weight(.bold)) + .foregroundStyle(.black) + .kerning(1) + } + .frame(maxWidth: .infinity) + .padding(.top, 12) + .accessibilityElement(children: .combine) + } +} diff --git a/ios/README.md b/ios/README.md new file mode 100644 index 0000000..06720d4 --- /dev/null +++ b/ios/README.md @@ -0,0 +1,23 @@ +# Bookibra iOS (SwiftUI) + +## API Base URL ayarı +1. `ios/Bookibra/Resources/Info.plist` içindeki `API_BASE_URL` anahtarını güncelle. +2. İstersen `ios/Bookibra/Resources/Debug.xcconfig` ve `ios/Bookibra/Resources/Release.xcconfig` içinde de aynı değeri tut. +3. Lokal backend için varsayılan: `http://localhost:8080` + +Not: iOS Simulator'dan host makinedeki backend'e erişimde genelde `http://127.0.0.1:8080` çalışır. Gerekirse bunu kullan. + +## Çalıştırma +1. Xcode ile `ios/Bookibra.xcodeproj` aç. +2. Scheme: `Bookibra` seç. +3. iOS 17+ simulator/device seçip Run (`Cmd+R`) yap. + +## Barkod test etme +- Gerçek kamera gerektiği için en stabil test fiziksel cihazdadır. +- Simulator'da kamera akışı sınırlı olabilir. +- Add Books > Barkod Tara ekranında ISBN-10 / ISBN-13 okutulduğunda otomatik `/api/books/isbn/:isbn` çağrılır. + +## Notlar +- JWT token Keychain'de tutulur. +- Kitaplık (`My Books`) SwiftData ile lokalde saklanır. +- Backend'de kitaplık CRUD olmasa da uygulama lokalde çalışır. diff --git a/ios/create_xcodeproj.rb b/ios/create_xcodeproj.rb new file mode 100644 index 0000000..82c283b --- /dev/null +++ b/ios/create_xcodeproj.rb @@ -0,0 +1,80 @@ +require 'xcodeproj' +require 'fileutils' + +root = File.expand_path(__dir__) +project_path = File.join(root, 'Bookibra.xcodeproj') +FileUtils.rm_rf(project_path) + +project = Xcodeproj::Project.new(project_path) +target = project.new_target(:application, 'Bookibra', :ios, '17.0') + +project.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '5.9' + config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.bookibra.ios' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0' + config.build_settings['TARGETED_DEVICE_FAMILY'] = '1,2' + config.build_settings['CODE_SIGN_STYLE'] = 'Automatic' + config.build_settings['MARKETING_VERSION'] = '1.0' + config.build_settings['CURRENT_PROJECT_VERSION'] = '1' +end + +target.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '5.9' + config.build_settings['INFOPLIST_FILE'] = 'Bookibra/Resources/Info.plist' + config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.bookibra.ios' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0' + config.build_settings['TARGETED_DEVICE_FAMILY'] = '1,2' + config.build_settings['ASSETCATALOG_COMPILER_APPICON_NAME'] = 'AppIcon' + config.build_settings['GENERATE_INFOPLIST_FILE'] = 'NO' + config.build_settings['SWIFT_EMIT_LOC_STRINGS'] = 'YES' + config.build_settings['LD_RUNPATH_SEARCH_PATHS'] = ['$(inherited)', '@executable_path/Frameworks'] +end + +# xcconfig mapping +config_map = { + 'Debug' => 'Bookibra/Resources/Debug.xcconfig', + 'Release' => 'Bookibra/Resources/Release.xcconfig' +} +project.build_configurations.each do |cfg| + path = config_map[cfg.name] + next unless path + cfg.base_configuration_reference = project.files.find { |f| f.path == path } || project.main_group.new_file(path) +end +target.build_configurations.each do |cfg| + path = config_map[cfg.name] + next unless path + cfg.base_configuration_reference = project.files.find { |f| f.path == path } || project.main_group.new_file(path) +end + +main = project.main_group +bookibra_group = main.new_group('Bookibra', 'Bookibra') + +# Source files +swift_files = Dir.glob(File.join(root, 'Bookibra', '**', '*.swift')).sort +swift_files.each do |abs| + rel = abs.sub(root + '/', '') + ref = bookibra_group.new_file(rel.sub('Bookibra/', '')) + target.source_build_phase.add_file_reference(ref) +end + +# Resource files/directories (only top-level bundles, no nested xcassets internals) +resource_paths = [ + 'Bookibra/Resources/Assets.xcassets', + 'Bookibra/Resources/en.lproj/Localizable.strings', + 'Bookibra/Resources/tr.lproj/Localizable.strings', + 'Bookibra/Resources/mock_book_remote.json' +] +resource_paths.each do |rel| + ref = bookibra_group.new_file(rel.sub('Bookibra/', '')) + target.resources_build_phase.add_file_reference(ref) +end + +# Frameworks +frameworks_group = main['Frameworks'] || main.new_group('Frameworks') +['AVFoundation.framework', 'VisionKit.framework', 'SwiftData.framework'].each do |framework| + ref = frameworks_group.new_file("System/Library/Frameworks/#{framework}") + target.frameworks_build_phase.add_file_reference(ref) +end + +project.save +puts "Created #{project_path}"