Compare commits
2 Commits
69884db0ab
...
52212f015b
| Author | SHA1 | Date | |
|---|---|---|---|
| 52212f015b | |||
| 261b2f58cc |
481
ios/Bookibra.xcodeproj/project.pbxproj
Normal file
481
ios/Bookibra.xcodeproj/project.pbxproj
Normal file
@@ -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 = "<group>"; };
|
||||||
|
1744A67EB267139F1EBDAE55 /* VisionKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VisionKit.framework; path = System/Library/Frameworks/VisionKit.framework; sourceTree = "<group>"; };
|
||||||
|
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 = "<group>"; };
|
||||||
|
1EB98ADB94703A1CE74B79CD /* BookRemote.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookRemote.swift; path = Models/BookRemote.swift; sourceTree = "<group>"; };
|
||||||
|
20947201FBE7D30CD6F69E38 /* Localizable.strings */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = Localizable.strings; path = Resources/en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
2448E99A3D8CDBD5334901C7 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Services/ImageCache.swift; sourceTree = "<group>"; };
|
||||||
|
2F8155E61788F47524B11449 /* BookibraApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookibraApp.swift; path = App/BookibraApp.swift; sourceTree = "<group>"; };
|
||||||
|
329388F7DE8EB288AEE98A23 /* AuthView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthView.swift; path = Views/Auth/AuthView.swift; sourceTree = "<group>"; };
|
||||||
|
349C2A1F39419A03C31114C3 /* BookCoverCard.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookCoverCard.swift; path = DesignSystem/Components/BookCoverCard.swift; sourceTree = "<group>"; };
|
||||||
|
3B226CA006E6EEA7DB04F100 /* Theme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = DesignSystem/Theme.swift; sourceTree = "<group>"; };
|
||||||
|
3BB571FD5EA0CB9C15DBD1DB /* AddBooksViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AddBooksViewModel.swift; path = ViewModels/AddBooksViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
471C2927142735752EA378A9 /* BarcodeScannerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarcodeScannerView.swift; path = Views/AddBooks/BarcodeScannerView.swift; sourceTree = "<group>"; };
|
||||||
|
4C361617CEBCE704307DA0A9 /* ShelfSectionView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShelfSectionView.swift; path = DesignSystem/Components/ShelfSectionView.swift; sourceTree = "<group>"; };
|
||||||
|
4DA01B4D0A49FA8E5D1E4F65 /* AddBooksView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AddBooksView.swift; path = Views/AddBooks/AddBooksView.swift; sourceTree = "<group>"; };
|
||||||
|
562B9464344AA711558F2AD0 /* SwiftData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftData.framework; path = System/Library/Frameworks/SwiftData.framework; sourceTree = "<group>"; };
|
||||||
|
72DED90139ADCDD834AE33CF /* NetworkErrorView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkErrorView.swift; path = DesignSystem/Components/NetworkErrorView.swift; sourceTree = "<group>"; };
|
||||||
|
72FE6CAAF6A00908AC19835F /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = "<group>"; };
|
||||||
|
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 = "<group>"; };
|
||||||
|
923CE0DF37333FFAF75668D5 /* UserProfile.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UserProfile.swift; path = Models/UserProfile.swift; sourceTree = "<group>"; };
|
||||||
|
95A0AD7A2591540C4ED3252F /* mock_book_remote.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; name = mock_book_remote.json; path = Resources/mock_book_remote.json; sourceTree = "<group>"; };
|
||||||
|
96C4EA4FE11115BA86AB2802 /* BookDetailViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookDetailViewModel.swift; path = ViewModels/BookDetailViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
98AAF8E8F7F53CA9CE81186B /* Release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Bookibra/Resources/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
A39116C7E45F12244CA1DC23 /* BookDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BookDetailView.swift; path = Views/Detail/BookDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
A77D7EF1C78724F79956D5B1 /* BooksService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BooksService.swift; path = Services/BooksService.swift; sourceTree = "<group>"; };
|
||||||
|
AF5F3C9BA827C89A343166A4 /* AuthViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthViewModel.swift; path = ViewModels/AuthViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
AFD5A6DD3D2BFC014B9AB859 /* Debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Bookibra/Resources/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
B618484EE9FC15B0DCA5E055 /* APIClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = APIClient.swift; path = Services/APIClient.swift; sourceTree = "<group>"; };
|
||||||
|
B93B9F6AC3711B6E32030129 /* LibraryBook.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LibraryBook.swift; path = Models/LibraryBook.swift; sourceTree = "<group>"; };
|
||||||
|
BEB5F3DC625CEAE1BEC3911F /* HomeViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeViewModel.swift; path = ViewModels/HomeViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
BF18379D6C3B16C83CCBCF22 /* HomeView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeView.swift; path = Views/Home/HomeView.swift; sourceTree = "<group>"; };
|
||||||
|
C15179D9ED03860747BBC180 /* CategoryViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CategoryViewModel.swift; path = ViewModels/CategoryViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
C8DD9EB12817D19C6BC65306 /* ScrewView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScrewView.swift; path = DesignSystem/Components/ScrewView.swift; sourceTree = "<group>"; };
|
||||||
|
CAF98B022FA5BAA8F627DAAD /* AuthService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthService.swift; path = Services/AuthService.swift; sourceTree = "<group>"; };
|
||||||
|
D117FECFDDAC2EDDC191CEC9 /* CategoryListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CategoryListView.swift; path = Views/Category/CategoryListView.swift; sourceTree = "<group>"; };
|
||||||
|
DC6F6AF589E6DD8E8EEF14CF /* BlurFogOverlay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BlurFogOverlay.swift; path = DesignSystem/Components/BlurFogOverlay.swift; sourceTree = "<group>"; };
|
||||||
|
E4DF6CD123627625C1967D42 /* AppRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRouter.swift; path = App/AppRouter.swift; sourceTree = "<group>"; };
|
||||||
|
FE7B76B449982D19D91A67F3 /* PrimaryPillButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PrimaryPillButton.swift; path = DesignSystem/Components/PrimaryPillButton.swift; sourceTree = "<group>"; };
|
||||||
|
/* 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 = "<group>";
|
||||||
|
};
|
||||||
|
8EB4884498D4ACBA5BB04312 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E592B0F1D9DDB4B2033A4A3D /* iOS */,
|
||||||
|
72FE6CAAF6A00908AC19835F /* AVFoundation.framework */,
|
||||||
|
1744A67EB267139F1EBDAE55 /* VisionKit.framework */,
|
||||||
|
562B9464344AA711558F2AD0 /* SwiftData.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
94F7452CA97CED98C86AF84E /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
7B6C1F4EB35DF0216BC86061 /* Bookibra.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E592B0F1D9DDB4B2033A4A3D /* iOS */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
192DFF277BADE93F9813BFFA /* Foundation.framework */,
|
||||||
|
);
|
||||||
|
name = iOS;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
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 = "<group>";
|
||||||
|
};
|
||||||
|
/* 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 */;
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>SchemeUserState</key>
|
||||||
|
<dict>
|
||||||
|
<key>Bookibra.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
53
ios/Bookibra/App/AppRouter.swift
Normal file
53
ios/Bookibra/App/AppRouter.swift
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
72
ios/Bookibra/App/BookibraApp.swift
Normal file
72
ios/Bookibra/App/BookibraApp.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
ios/Bookibra/DesignSystem/Components/BlurFogOverlay.swift
Normal file
17
ios/Bookibra/DesignSystem/Components/BlurFogOverlay.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
ios/Bookibra/DesignSystem/Components/BookCoverCard.swift
Normal file
57
ios/Bookibra/DesignSystem/Components/BookCoverCard.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ios/Bookibra/DesignSystem/Components/NetworkErrorView.swift
Normal file
21
ios/Bookibra/DesignSystem/Components/NetworkErrorView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ios/Bookibra/DesignSystem/Components/PrimaryPillButton.swift
Normal file
21
ios/Bookibra/DesignSystem/Components/PrimaryPillButton.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
ios/Bookibra/DesignSystem/Components/ScrewView.swift
Normal file
14
ios/Bookibra/DesignSystem/Components/ScrewView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
85
ios/Bookibra/DesignSystem/Components/ShelfSectionView.swift
Normal file
85
ios/Bookibra/DesignSystem/Components/ShelfSectionView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
ios/Bookibra/DesignSystem/Theme.swift
Normal file
29
ios/Bookibra/DesignSystem/Theme.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
135
ios/Bookibra/Models/BookRemote.swift
Normal file
135
ios/Bookibra/Models/BookRemote.swift
Normal file
@@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
63
ios/Bookibra/Models/LibraryBook.swift
Normal file
63
ios/Bookibra/Models/LibraryBook.swift
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
ios/Bookibra/Models/UserProfile.swift
Normal file
10
ios/Bookibra/Models/UserProfile.swift
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct UserProfile: Codable, Equatable {
|
||||||
|
struct User: Codable, Equatable {
|
||||||
|
let id: String
|
||||||
|
let email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: User
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
6
ios/Bookibra/Resources/Assets.xcassets/Contents.json
Normal file
6
ios/Bookibra/Resources/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
1
ios/Bookibra/Resources/Debug.xcconfig
Normal file
1
ios/Bookibra/Resources/Debug.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
API_BASE_URL = http://192.168.1.124:8080
|
||||||
46
ios/Bookibra/Resources/Info.plist
Normal file
46
ios/Bookibra/Resources/Info.plist
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>API_BASE_URL</key>
|
||||||
|
<string>http://192.168.1.124:8080</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>ISBN barkodu taramak için kameraya erişim gerekiyor.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Bookibra/Resources/Release.xcconfig
Normal file
1
ios/Bookibra/Resources/Release.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
API_BASE_URL = http://localhost:8080
|
||||||
18
ios/Bookibra/Resources/en.lproj/Localizable.strings
Normal file
18
ios/Bookibra/Resources/en.lproj/Localizable.strings
Normal file
@@ -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";
|
||||||
21
ios/Bookibra/Resources/mock_book_remote.json
Normal file
21
ios/Bookibra/Resources/mock_book_remote.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
ios/Bookibra/Resources/tr.lproj/Localizable.strings
Normal file
18
ios/Bookibra/Resources/tr.lproj/Localizable.strings
Normal file
@@ -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ı";
|
||||||
129
ios/Bookibra/Services/APIClient.swift
Normal file
129
ios/Bookibra/Services/APIClient.swift
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol APIClientProtocol {
|
||||||
|
func get<T: Decodable>(path: String, queryItems: [URLQueryItem], token: String?) async throws -> T
|
||||||
|
func post<T: Decodable, Body: Encodable>(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<T: Decodable>(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<T: Decodable, Body: Encodable>(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<T: Decodable>(_ 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")!
|
||||||
|
}
|
||||||
|
}
|
||||||
42
ios/Bookibra/Services/AuthService.swift
Normal file
42
ios/Bookibra/Services/AuthService.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
52
ios/Bookibra/Services/BooksService.swift
Normal file
52
ios/Bookibra/Services/BooksService.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
34
ios/Bookibra/Services/ImageCache.swift
Normal file
34
ios/Bookibra/Services/ImageCache.swift
Normal file
@@ -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<NSURL, UIImage>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
47
ios/Bookibra/Services/KeychainStore.swift
Normal file
47
ios/Bookibra/Services/KeychainStore.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
88
ios/Bookibra/ViewModels/AddBooksViewModel.swift
Normal file
88
ios/Bookibra/ViewModels/AddBooksViewModel.swift
Normal file
@@ -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<Void, Never>?
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
ios/Bookibra/ViewModels/AuthViewModel.swift
Normal file
53
ios/Bookibra/ViewModels/AuthViewModel.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
ios/Bookibra/ViewModels/BookDetailViewModel.swift
Normal file
55
ios/Bookibra/ViewModels/BookDetailViewModel.swift
Normal file
@@ -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<LibraryBook>())) ?? []
|
||||||
|
isInLibrary = all.contains(where: { local in
|
||||||
|
match(local: local, remote: book)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleLibrary(context: ModelContext) {
|
||||||
|
let all = (try? context.fetch(FetchDescriptor<LibraryBook>())) ?? []
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
39
ios/Bookibra/ViewModels/CategoryViewModel.swift
Normal file
39
ios/Bookibra/ViewModels/CategoryViewModel.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
61
ios/Bookibra/ViewModels/HomeViewModel.swift
Normal file
61
ios/Bookibra/ViewModels/HomeViewModel.swift
Normal file
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
ios/Bookibra/Views/AddBooks/AddBooksView.swift
Normal file
115
ios/Bookibra/Views/AddBooks/AddBooksView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
ios/Bookibra/Views/AddBooks/BarcodeScannerView.swift
Normal file
154
ios/Bookibra/Views/AddBooks/BarcodeScannerView.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
71
ios/Bookibra/Views/Auth/AuthView.swift
Normal file
71
ios/Bookibra/Views/Auth/AuthView.swift
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
71
ios/Bookibra/Views/Category/CategoryListView.swift
Normal file
71
ios/Bookibra/Views/Category/CategoryListView.swift
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
58
ios/Bookibra/Views/Detail/BookDetailView.swift
Normal file
58
ios/Bookibra/Views/Detail/BookDetailView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
ios/Bookibra/Views/Home/HomeView.swift
Normal file
73
ios/Bookibra/Views/Home/HomeView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
ios/README.md
Normal file
23
ios/README.md
Normal file
@@ -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.
|
||||||
80
ios/create_xcodeproj.rb
Normal file
80
ios/create_xcodeproj.rb
Normal file
@@ -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}"
|
||||||
Reference in New Issue
Block a user