backup, UI/UX finished everywhere

This commit is contained in:
Emin 2024-09-10 14:56:12 +05:00
parent c5e3417af0
commit d1bf71528e
47 changed files with 1290 additions and 197 deletions

View file

@ -4003,7 +4003,7 @@
"compose_review" = "Leave feedback";
"see_all" = "All reviews";
"see_all_reviews" = "All reviews";
"more" = "Unfold";
@ -4017,6 +4017,8 @@
"upload_photo" = "Upload photo";
"images_number_warning" = "You can send no more than 10 images";
"send" = "Send";
"profile_tourism" = "Profile";
@ -4091,7 +4093,11 @@
"plz_wait_dowloading" = "Please, wait, data being downloaded";
"empty_list" = "Пусто";
"no_content" = "Empty";
"empty_list" = "Empty";
"back" = "Back";
"review_will_be_published" = "Review will be published when you are online";

View file

@ -4003,7 +4003,7 @@
"compose_review" = "Leave feedback";
"see_all" = "All reviews";
"see_all_reviews" = "All reviews";
"more" = "Unfold";
@ -4017,6 +4017,8 @@
"upload_photo" = "Upload photo";
"images_number_warning" = "You can send no more than 10 images";
"send" = "Send";
"profile_tourism" = "Profile";
@ -4091,7 +4093,11 @@
"plz_wait_dowloading" = "Please, wait, data being downloaded";
"empty_list" = "Пусто";
"no_content" = "Empty";
"empty_list" = "Empty";
"back" = "Back";
"review_will_be_published" = "Review will be published when you are online";

View file

@ -4003,7 +4003,7 @@
"compose_review" = "Оставить отзыв";
"see_all" = "Все отзывы";
"see_all_reviews" = "Все отзывы";
"more" = "Развернуть";
@ -4017,6 +4017,8 @@
"upload_photo" = "Загрузить фото";
"images_number_warning" = "Можно отправить не более 10 изображений";
"send" = "Отправить";
"profile_tourism" = "Профиль";
@ -4091,8 +4093,12 @@
"plz_wait_dowloading" = "Пожалуйста подождите данные скачиваются";
"no_content" = "Пусто";
"empty_list" = "Пусто";
"back" = "Назад";
"review_will_be_published" = "Отзыв будет публикован когда будете онлайн";
"review_was_published" = "Отзыв был успешно опубликован";

View file

@ -188,7 +188,7 @@
3D2D79D72C7D0AC00062BC3D /* AppTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79D62C7D0ABF0062BC3D /* AppTextField.swift */; };
3D2D79D92C7D15190062BC3D /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79D82C7D15190062BC3D /* PrimaryButton.swift */; };
3D2D79DB2C7D15410062BC3D /* SecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79DA2C7D15410062BC3D /* SecondaryButton.swift */; };
3D2D79DD2C7DE34B0062BC3D /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79DC2C7DE34B0062BC3D /* PhotoPickerView.swift */; };
3D2D79DD2C7DE34B0062BC3D /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2D79DC2C7DE34B0062BC3D /* ImagePicker.swift */; };
3D585BF42C760850005DF71F /* UIScreenExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D585BF32C760850005DF71F /* UIScreenExtensions.swift */; };
3DA3FC992C75ED2A0065E4D6 /* changeTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA3FC982C75ED2A0065E4D6 /* changeTheme.swift */; };
3DBD7BE42425015C00ED9FE8 /* ParntersStyleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD7BE32425015C00ED9FE8 /* ParntersStyleSheet.swift */; };
@ -301,8 +301,6 @@
527D5E7F2C60E69C00736A85 /* Layouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527D5E7E2C60E69C00736A85 /* Layouting.swift */; };
527D5E822C60EFEE00736A85 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527D5E812C60EFEE00736A85 /* UIViewExtensions.swift */; };
528D72A12C5BBBF700D53210 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 528D72A02C5BBBF700D53210 /* Colors.xcassets */; };
5292123D2C7359FC007B97E1 /* CountryPickerView in Frameworks */ = {isa = PBXBuildFile; productRef = 5292123C2C7359FC007B97E1 /* CountryPickerView */; };
529A5F0A2C858F82004FE4A1 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 529A5F092C858F82004FE4A1 /* SDWebImageSwiftUI */; };
529A5F162C8595BB004FE4A1 /* PersonalData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F112C859535004FE4A1 /* PersonalData.xcdatamodeld */; };
529A5F192C85BFF0004FE4A1 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F182C85BFF0004FE4A1 /* ToastView.swift */; };
529A5F1E2C86DDE5004FE4A1 /* PlaceDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */; };
@ -588,6 +586,23 @@
CDCA27842245090900167D87 /* ListenerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA27832245090900167D87 /* ListenerContainer.swift */; };
CDCA278622451F5000167D87 /* RouteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA278522451F5000167D87 /* RouteInfo.swift */; };
CDCA278E2248F34C00167D87 /* MWMRoutingManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = CDCA278B2248F34C00167D87 /* MWMRoutingManager.mm */; };
CED0E00E2C8ACBCA008C61CA /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E00D2C8ACBCA008C61CA /* SDWebImageSwiftUI */; };
CED0E0112C8ACBE1008C61CA /* CountryPickerView in Frameworks */ = {isa = PBXBuildFile; productRef = CED0E0102C8ACBE1008C61CA /* CountryPickerView */; };
CED0E0172C8ACF0D008C61CA /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */; };
CED0E0192C8AD57C008C61CA /* EmptyUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0182C8AD57C008C61CA /* EmptyUI.swift */; };
CED0E01B2C8B048C008C61CA /* AllPicsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E01A2C8B048C008C61CA /* AllPicsScreen.swift */; };
CED0E0222C8B22CD008C61CA /* ReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0212C8B22CD008C61CA /* ReviewView.swift */; };
CED0E0242C8C6DF9008C61CA /* RatingBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0232C8C6DF9008C61CA /* RatingBarView.swift */; };
CED0E0262C8C85BD008C61CA /* PostReviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0252C8C85BD008C61CA /* PostReviewViewModel.swift */; };
CED0E0282C8C85C9008C61CA /* PostReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0272C8C85C9008C61CA /* PostReviewView.swift */; };
CED0E02A2C8C88B9008C61CA /* FlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0292C8C88B9008C61CA /* FlowLayout.swift */; };
CED0E02C2C8F6BFF008C61CA /* MultiImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E02B2C8F6BFF008C61CA /* MultiImagePicker.swift */; };
CED0E0312C900BB2008C61CA /* AllReviewsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0302C900BB2008C61CA /* AllReviewsScreen.swift */; };
CED0E0332C900D4C008C61CA /* ReviewsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0322C900D4C008C61CA /* ReviewsViewModel.swift */; };
CED0E0352C902527008C61CA /* LanguageDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0342C902527008C61CA /* LanguageDTO.swift */; };
CED0E0372C902532008C61CA /* ThemeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0362C902532008C61CA /* ThemeDTO.swift */; };
CED0E0392C904868008C61CA /* NavigationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E0382C904868008C61CA /* NavigationUtils.swift */; };
CED0E03B2C904A06008C61CA /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED0E03A2C904A06008C61CA /* FavoritesViewModel.swift */; };
ED0B1C312BC2951F00FB8EDD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = ED0B1C302BC2951F00FB8EDD /* PrivacyInfo.xcprivacy */; };
ED1080A72B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */; };
ED1263AB2B6F99F900AD99F3 /* UIView+AddSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1263AA2B6F99F900AD99F3 /* UIView+AddSeparator.swift */; };
@ -1198,7 +1213,7 @@
3D2D79D62C7D0ABF0062BC3D /* AppTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTextField.swift; sourceTree = "<group>"; };
3D2D79D82C7D15190062BC3D /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = "<group>"; };
3D2D79DA2C7D15410062BC3D /* SecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryButton.swift; sourceTree = "<group>"; };
3D2D79DC2C7DE34B0062BC3D /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = "<group>"; };
3D2D79DC2C7DE34B0062BC3D /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
3D585BF32C760850005DF71F /* UIScreenExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScreenExtensions.swift; sourceTree = "<group>"; };
3DA3FC982C75ED2A0065E4D6 /* changeTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = changeTheme.swift; sourceTree = "<group>"; };
3DBD7BE32425015C00ED9FE8 /* ParntersStyleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParntersStyleSheet.swift; sourceTree = "<group>"; };
@ -1613,6 +1628,21 @@
CDCA278C2248F34C00167D87 /* MWMRouterResultCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWMRouterResultCode.h; sourceTree = "<group>"; };
CDCA278F2248F3B800167D87 /* MWMLocationModeListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMLocationModeListener.h; sourceTree = "<group>"; };
CDE0F3AD225B8D45008BA5C3 /* MWMSpeedCameraManagerMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMSpeedCameraManagerMode.h; sourceTree = "<group>"; };
CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = "<group>"; };
CED0E0182C8AD57C008C61CA /* EmptyUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUI.swift; sourceTree = "<group>"; };
CED0E01A2C8B048C008C61CA /* AllPicsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPicsScreen.swift; sourceTree = "<group>"; };
CED0E0212C8B22CD008C61CA /* ReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewView.swift; sourceTree = "<group>"; };
CED0E0232C8C6DF9008C61CA /* RatingBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingBarView.swift; sourceTree = "<group>"; };
CED0E0252C8C85BD008C61CA /* PostReviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostReviewViewModel.swift; sourceTree = "<group>"; };
CED0E0272C8C85C9008C61CA /* PostReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostReviewView.swift; sourceTree = "<group>"; };
CED0E0292C8C88B9008C61CA /* FlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowLayout.swift; sourceTree = "<group>"; };
CED0E02B2C8F6BFF008C61CA /* MultiImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiImagePicker.swift; sourceTree = "<group>"; };
CED0E0302C900BB2008C61CA /* AllReviewsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllReviewsScreen.swift; sourceTree = "<group>"; };
CED0E0322C900D4C008C61CA /* ReviewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsViewModel.swift; sourceTree = "<group>"; };
CED0E0342C902527008C61CA /* LanguageDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageDTO.swift; sourceTree = "<group>"; };
CED0E0362C902532008C61CA /* ThemeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeDTO.swift; sourceTree = "<group>"; };
CED0E0382C904868008C61CA /* NavigationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationUtils.swift; sourceTree = "<group>"; };
CED0E03A2C904A06008C61CA /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = "<group>"; };
ED097E762BB80C320006ED01 /* OMapsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OMapsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
ED0B1C302BC2951F00FB8EDD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialMediaCollectionViewHeader.swift; sourceTree = "<group>"; };
@ -1968,7 +1998,6 @@
FA853BEF26BC5BA40026D455 /* libdescriptions.a in Frameworks */,
FA853BEB26BC5B9E0026D455 /* libbsdiff.a in Frameworks */,
FA853BED26BC5B9E0026D455 /* libmwm_diff.a in Frameworks */,
5292123D2C7359FC007B97E1 /* CountryPickerView in Frameworks */,
FA853BE926BC5B8B0026D455 /* libopen_location_code.a in Frameworks */,
FA853BE726BC5B820026D455 /* libstb_image.a in Frameworks */,
FA853BE526BC5B660026D455 /* libvulkan_wrapper.a in Frameworks */,
@ -1978,14 +2007,15 @@
FA853BDB26BC54CD0026D455 /* libtraffic.a in Frameworks */,
FA853BD926BC54C80026D455 /* libexpat.a in Frameworks */,
FA853BD726BC54650026D455 /* libpugixml.a in Frameworks */,
529A5F0A2C858F82004FE4A1 /* SDWebImageSwiftUI in Frameworks */,
FA853BD526BC545D0026D455 /* libagg.a in Frameworks */,
FA853BD326BC54530026D455 /* libtransit.a in Frameworks */,
FA853BA926BC3B8A0026D455 /* libbase.a in Frameworks */,
FA853BAB26BC3B8A0026D455 /* libcoding.a in Frameworks */,
FA853BAD26BC3B8A0026D455 /* libdrape.a in Frameworks */,
CED0E00E2C8ACBCA008C61CA /* SDWebImageSwiftUI in Frameworks */,
FA853BAF26BC3B8A0026D455 /* libdrape_frontend.a in Frameworks */,
FA853BB126BC3B8A0026D455 /* libeditor.a in Frameworks */,
CED0E0112C8ACBE1008C61CA /* CountryPickerView in Frameworks */,
FA853BB326BC3B8A0026D455 /* libgeometry.a in Frameworks */,
FA853BB526BC3B8A0026D455 /* libicu.a in Frameworks */,
FA853BB726BC3B8A0026D455 /* libindexer.a in Frameworks */,
@ -3110,6 +3140,8 @@
children = (
52ED91AA2C7302A7000EE25B /* CurrencyRatesDTO.swift */,
52ED91AF2C73030D000EE25B /* PersonalDataDTO.swift */,
CED0E0342C902527008C61CA /* LanguageDTO.swift */,
CED0E0362C902532008C61CA /* ThemeDTO.swift */,
);
path = Profile;
sourceTree = "<group>";
@ -3195,9 +3227,12 @@
52B573FD2C624A520047FAC9 /* CountryPickerUtils.swift */,
52522F4B2C6E10FD0015709C /* LoadImg.swift */,
3D2D79D42C7CF6970062BC3D /* Spacers.swift */,
3D2D79DC2C7DE34B0062BC3D /* PhotoPickerView.swift */,
3D2D79DC2C7DE34B0062BC3D /* ImagePicker.swift */,
CED0E02B2C8F6BFF008C61CA /* MultiImagePicker.swift */,
529A5F622C86E39A004FE4A1 /* AppSearchBar.swift */,
529A5F612C86E39A004FE4A1 /* HorizontalSingleChoice.swift */,
CED0E0182C8AD57C008C61CA /* EmptyUI.swift */,
CED0E0292C8C88B9008C61CA /* FlowLayout.swift */,
);
path = Components;
sourceTree = "<group>";
@ -3215,6 +3250,8 @@
children = (
527D5E7E2C60E69C00736A85 /* Layouting.swift */,
3DA3FC982C75ED2A0065E4D6 /* changeTheme.swift */,
CED0E0162C8ACF0D008C61CA /* RoundedCornerShape.swift */,
CED0E0382C904868008C61CA /* NavigationUtils.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -3262,6 +3299,7 @@
isa = PBXGroup;
children = (
529A5F5D2C86E37A004FE4A1 /* PlacesItem.swift */,
CED0E0232C8C6DF9008C61CA /* RatingBarView.swift */,
);
path = Special;
sourceTree = "<group>";
@ -3279,6 +3317,7 @@
isa = PBXGroup;
children = (
529A5F692C8707F9004FE4A1 /* FavoritesViewController.swift */,
CED0E03A2C904A06008C61CA /* FavoritesViewModel.swift */,
);
path = Favorites;
sourceTree = "<group>";
@ -3301,6 +3340,7 @@
52A48AF22C8989780081E522 /* Reviews */,
52EF1B612C8989F1003046A4 /* PlaceViewController.swift */,
52EF1B652C8989F9003046A4 /* PlaceViewModel.swift */,
CED0E01A2C8B048C008C61CA /* AllPicsScreen.swift */,
);
name = PlaceDetails;
path = Profile/PlaceDetails;
@ -3325,7 +3365,11 @@
52A48AF22C8989780081E522 /* Reviews */ = {
isa = PBXGroup;
children = (
CED0E0202C8B22BD008C61CA /* Components */,
52ECA8122C8A0D7A00F213B3 /* ReviewsScreen.swift */,
CED0E0252C8C85BD008C61CA /* PostReviewViewModel.swift */,
CED0E0302C900BB2008C61CA /* AllReviewsScreen.swift */,
CED0E0322C900D4C008C61CA /* ReviewsViewModel.swift */,
);
path = Reviews;
sourceTree = "<group>";
@ -3797,6 +3841,15 @@
path = Location;
sourceTree = "<group>";
};
CED0E0202C8B22BD008C61CA /* Components */ = {
isa = PBXGroup;
children = (
CED0E0272C8C85C9008C61CA /* PostReviewView.swift */,
CED0E0212C8B22CD008C61CA /* ReviewView.swift */,
);
path = Components;
sourceTree = "<group>";
};
ED1ADA312BC6B19E0029209F /* Tests */ = {
isa = PBXGroup;
children = (
@ -4587,8 +4640,8 @@
);
name = OMaps;
packageProductDependencies = (
5292123C2C7359FC007B97E1 /* CountryPickerView */,
529A5F092C858F82004FE4A1 /* SDWebImageSwiftUI */,
CED0E00D2C8ACBCA008C61CA /* SDWebImageSwiftUI */,
CED0E0102C8ACBE1008C61CA /* CountryPickerView */,
);
productName = Maps;
productReference = 6741AA5D1BF340DE002C974C /* Organic Maps (Debug).app */;
@ -4687,8 +4740,8 @@
);
mainGroup = 29B97314FDCFA39411CA2CEA /* Maps */;
packageReferences = (
5292123B2C7359FC007B97E1 /* XCRemoteSwiftPackageReference "CountryPickerView" */,
529A5F082C858F82004FE4A1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
CED0E00C2C8ACBCA008C61CA /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
CED0E00F2C8ACBE1008C61CA /* XCRemoteSwiftPackageReference "CountryPickerView" */,
);
productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */;
projectDirPath = "";
@ -4945,6 +4998,7 @@
F6E2FF5D1E097BA00083EBEC /* MWMRecentTrackSettingsController.mm in Sources */,
34AB66651FC5AA330078E451 /* TransportTransitTrain.swift in Sources */,
FA85D44E279B738F00B858E9 /* CopyableLabel.swift in Sources */,
CED0E0262C8C85BD008C61CA /* PostReviewViewModel.swift in Sources */,
993DF11C23F6BDB100AC231A /* UITableViewHeaderFooterViewRenderer.swift in Sources */,
99A906E623F6F7030005872B /* OpeningHoursViewController.swift in Sources */,
343064411E9FDC7300DC7665 /* SearchIndex.swift in Sources */,
@ -4988,6 +5042,7 @@
47D48BF52432A7CA00FEFB1F /* ChartViewRenderer.swift in Sources */,
CDCA273A2237FCFE00167D87 /* SearchTemplateBuilder.swift in Sources */,
993DF10B23F6BDB100AC231A /* CheckmarkRenderer.swift in Sources */,
CED0E02C2C8F6BFF008C61CA /* MultiImagePicker.swift in Sources */,
F6E2FED01E097BA00083EBEC /* MWMSearchFilterViewController.mm in Sources */,
34D3B01B1E389D05004100F9 /* MWMButtonCell.m in Sources */,
337F98B421D3C9F200C8AC27 /* SearchHistoryViewController.swift in Sources */,
@ -5015,6 +5070,7 @@
999FC12023ABA9AD00B0E6F9 /* SearchStyleSheet.swift in Sources */,
3D15ACEE2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm in Sources */,
52EF1B662C8989F9003046A4 /* PlaceViewModel.swift in Sources */,
CED0E0242C8C6DF9008C61CA /* RatingBarView.swift in Sources */,
9977E6A32480F9BF0073780C /* BottomMenuLayerButtonRenderer.swift in Sources */,
3454D7D11E07F045004AF2AD /* UIImage+RGBAData.m in Sources */,
52B573EC2C61E1C10047FAC9 /* SignInViewController.swift in Sources */,
@ -5043,6 +5099,7 @@
34D3B0301E389D05004100F9 /* MWMEditorCategoryCell.m in Sources */,
52B573F72C61F4D00047FAC9 /* PasswordTextField.swift in Sources */,
F653CE191C71F62700A453F1 /* MWMAddPlaceNavigationBar.mm in Sources */,
CED0E0332C900D4C008C61CA /* ReviewsViewModel.swift in Sources */,
340475621E081A4600C92850 /* MWMNetworkPolicy+UI.m in Sources */,
F6E2FEE51E097BA00083EBEC /* MWMSearchNoResults.m in Sources */,
3DBD7BE42425015C00ED9FE8 /* ParntersStyleSheet.swift in Sources */,
@ -5152,6 +5209,7 @@
990128562449A82500C72B10 /* BottomTabBarView.swift in Sources */,
529A5F422C86E108004FE4A1 /* Category.swift in Sources */,
F6E2FD711E097BA00083EBEC /* MWMMapDownloaderTableViewCell.m in Sources */,
CED0E0352C902527008C61CA /* LanguageDTO.swift in Sources */,
F6E2FE4F1E097BA00083EBEC /* MWMActionBarButton.m in Sources */,
47F86CFF20C936FC00FEE291 /* TabView.swift in Sources */,
5260D3CE2C64F60200C673B4 /* APIEndpoints.swift in Sources */,
@ -5166,6 +5224,7 @@
52ED91B02C73030D000EE25B /* PersonalDataDTO.swift in Sources */,
EDE243E72B6D55610057369B /* InfoView.swift in Sources */,
F692F3831EA0FAF5001E82EB /* MWMAutoupdateController.mm in Sources */,
CED0E01B2C8B048C008C61CA /* AllPicsScreen.swift in Sources */,
34BF0CC71C31304A00D097EB /* MWMAuthorizationCommon.mm in Sources */,
527D5E822C60EFEE00736A85 /* UIViewExtensions.swift in Sources */,
34AB664D1FC5AA330078E451 /* RouteManagerFooterView.swift in Sources */,
@ -5186,6 +5245,7 @@
343E75981E5B1EE20041226A /* MWMCollectionViewController.m in Sources */,
34E776141F14B17F003040B3 /* AvailableArea.swift in Sources */,
34AB66081FC5AA320078E451 /* MWMNavigationDashboardManager.mm in Sources */,
CED0E03B2C904A06008C61CA /* FavoritesViewModel.swift in Sources */,
3404F490202898CC0090E401 /* BMCModels.swift in Sources */,
F6E2FD561E097BA00083EBEC /* MWMMapDownloaderButtonTableViewCell.m in Sources */,
9901284F244732DB00C72B10 /* BottomTabBarPresenter.swift in Sources */,
@ -5210,6 +5270,7 @@
993DF11923F6BDB100AC231A /* UITextFieldRenderer.swift in Sources */,
5260D3E82C66439400C673B4 /* AuthRepository.swift in Sources */,
342CC5F21C2D7730005F3FE5 /* MWMAuthorizationLoginViewController.mm in Sources */,
CED0E0282C8C85C9008C61CA /* PostReviewView.swift in Sources */,
529A5F5E2C86E37A004FE4A1 /* PlacesItem.swift in Sources */,
340475591E081A4600C92850 /* WebViewController.m in Sources */,
529A5F262C86DE9D004FE4A1 /* ReviewsDTO.swift in Sources */,
@ -5221,6 +5282,7 @@
99F3EB1123F418C900C713F8 /* PlacePageBuilder.swift in Sources */,
52522F402C6DDF290015709C /* CurrencyRates.swift in Sources */,
4735008A23A83CF700661A95 /* DownloadedMapsDataSource.swift in Sources */,
CED0E02A2C8C88B9008C61CA /* FlowLayout.swift in Sources */,
3D2D79D92C7D15190062BC3D /* PrimaryButton.swift in Sources */,
CD9AD96F2281DF3600EC174A /* CategoryInfo.swift in Sources */,
3D2D79BC2C7C5E300062BC3D /* ProfileRepositoryImpl.swift in Sources */,
@ -5257,7 +5319,7 @@
47B9065221C7FA400079C85E /* MWMWebImage.m in Sources */,
47A13CAD24BE9AA500027D4F /* DatePickerViewRenderer.swift in Sources */,
F6E2FE7C1E097BA00083EBEC /* MWMPlacePageOpeningHoursCell.mm in Sources */,
3D2D79DD2C7DE34B0062BC3D /* PhotoPickerView.swift in Sources */,
3D2D79DD2C7DE34B0062BC3D /* ImagePicker.swift in Sources */,
340E1EFB1E2F614400CE49BF /* Storyboard.swift in Sources */,
34E776331F15FAC2003040B3 /* MWMPlacePageManagerHelper.mm in Sources */,
52ED919D2C71F639000EE25B /* SimpleResponse.swift in Sources */,
@ -5314,6 +5376,7 @@
52A48AE72C8882A90081E522 /* ReviewsRepository.swift in Sources */,
34AB66591FC5AA330078E451 /* TransportTransitFlowLayout.swift in Sources */,
EDBD680B2B62572E005DD151 /* LocationServicesDisabledAlert.swift in Sources */,
CED0E0312C900BB2008C61CA /* AllReviewsScreen.swift in Sources */,
3486B5191E27AD3B0069C126 /* MWMFrameworkListener.mm in Sources */,
3404756B1E081A4600C92850 /* MWMSearch+CoreSpotlight.mm in Sources */,
CD9AD96C2281B56900EC174A /* CPViewPortState.swift in Sources */,
@ -5329,6 +5392,7 @@
EDFDFB4C2B722C9C0013A44C /* InfoTableViewCell.swift in Sources */,
5260D3DE2C66237700C673B4 /* AuthService.swift in Sources */,
47CA68F8250F8AB700671019 /* BookmarksListSectionHeader.swift in Sources */,
CED0E0172C8ACF0D008C61CA /* RoundedCornerShape.swift in Sources */,
F6BD1D211CA412920047B8E8 /* MWMOsmAuthAlert.mm in Sources */,
47CF2E6323BA0DD500D11C30 /* CopyLabel.swift in Sources */,
47CA68D12500435E00671019 /* BookmarksListViewController.swift in Sources */,
@ -5371,6 +5435,7 @@
52CD2D892C6F0AF200CCC439 /* ProfileRepository.swift in Sources */,
529A5F3D2C86E08E004FE4A1 /* FavoritesIdsDTO.swift in Sources */,
99012851244732DB00C72B10 /* BottomTabBarViewController.swift in Sources */,
CED0E0222C8B22CD008C61CA /* ReviewView.swift in Sources */,
52522F3E2C6DDF190015709C /* PersonalData.swift in Sources */,
F63AF5061EA6162400A1DB98 /* FilterTypeCell.swift in Sources */,
993DF10623F6BDB100AC231A /* UIColor+rgba.swift in Sources */,
@ -5383,6 +5448,7 @@
52522F392C6DD9DA0015709C /* ProfileViewModel.swift in Sources */,
47F67D1521CAB21B0069754E /* MWMImageCoder.m in Sources */,
529A5F352C86DF99004FE4A1 /* PlaceLocation.swift in Sources */,
CED0E0392C904868008C61CA /* NavigationUtils.swift in Sources */,
34AB66861FC5AA330078E451 /* MWMNavigationInfoView.mm in Sources */,
34C9BD051C6DB693000DC38D /* MWMViewController.m in Sources */,
5260D3E02C6624B900C673B4 /* AuthRepositoryImpl.swift in Sources */,
@ -5469,6 +5535,8 @@
3454D7E31E07F045004AF2AD /* UITextView+RuntimeAttributes.m in Sources */,
F6A2184A1CA3F26800BE2CC6 /* MWMEditorViralActivityItem.mm in Sources */,
993DF10923F6BDB100AC231A /* IFonts.swift in Sources */,
CED0E0372C902532008C61CA /* ThemeDTO.swift in Sources */,
CED0E0192C8AD57C008C61CA /* EmptyUI.swift in Sources */,
47699A0821F08E37009E6585 /* NSDate+TimeDistance.m in Sources */,
34845DB31E165E24003D55B9 /* SearchNoResultsViewController.swift in Sources */,
47E3C72B2111E62A008B3B27 /* FadeOutAnimatedTransitioning.swift in Sources */,
@ -5867,35 +5935,35 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5292123B2C7359FC007B97E1 /* XCRemoteSwiftPackageReference "CountryPickerView" */ = {
CED0E00C2C8ACBCA008C61CA /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.1.2;
};
};
CED0E00F2C8ACBE1008C61CA /* XCRemoteSwiftPackageReference "CountryPickerView" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kizitonwose/CountryPickerView.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.0.0;
};
};
529A5F082C858F82004FE4A1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI.git";
requirement = {
kind = exactVersion;
version = 2.0.2;
minimumVersion = 3.3.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
5292123C2C7359FC007B97E1 /* CountryPickerView */ = {
CED0E00D2C8ACBCA008C61CA /* SDWebImageSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
package = 5292123B2C7359FC007B97E1 /* XCRemoteSwiftPackageReference "CountryPickerView" */;
productName = CountryPickerView;
};
529A5F092C858F82004FE4A1 /* SDWebImageSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
package = 529A5F082C858F82004FE4A1 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
package = CED0E00C2C8ACBCA008C61CA /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
productName = SDWebImageSwiftUI;
};
CED0E0102C8ACBE1008C61CA /* CountryPickerView */ = {
isa = XCSwiftPackageProductDependency;
package = CED0E00F2C8ACBE1008C61CA /* XCRemoteSwiftPackageReference "CountryPickerView" */;
productName = CountryPickerView;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View file

@ -3,6 +3,24 @@ struct Constants {
static let imageUrlExample = "https://img.freepik.com/free-photo/young-woman-hiker-taking-photo-with-smartphone-on-mountains-peak-in-winter_335224-427.jpg?w=2000"
static let thumbnailUrlExample = "https://render.fineartamerica.com/images/images-profile-flow/400/images-medium-large-5/awesome-solitude-bess-hamiti.jpg"
static let logoUrlExample = "https://brandeps.com/logo-download/O/OSCE-logo-vector-01.svg"
static let anotherImageExample = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSiceDsFSiLmW2Jl-XP3m5UXRdyLRKBQTlPGQ&s"
static let reviewExample = Review(
id: 1,
placeId: 1,
rating: 5,
user: User(id: 1, name: "John Doe", pfpUrl: Constants.imageUrlExample, countryCodeName: "US"),
date: "2024-09-01",
comment: "Amazing place! The views are incredible and the atmosphere is so calming.The views are incredible and the atmosphere is so calming.The views are incredible and the atmosphere is so calming.The views are incredible and the atmosphere is so calming.The views are incredible and the atmosphere is so calming.The views are incredible and the atmosphere is so calming.",
picsUrls: [
Constants.imageUrlExample,
Constants.thumbnailUrlExample,
Constants.imageUrlExample,
Constants.thumbnailUrlExample,
Constants.imageUrlExample,
Constants.thumbnailUrlExample
]
)
// MARK: - Data
static let categories: [String: String] = [
@ -27,8 +45,10 @@ struct Constants {
cover: Constants.imageUrlExample,
pics: [
Constants.imageUrlExample,
Constants.thumbnailUrlExample,
Constants.imageUrlExample,
Constants.thumbnailUrlExample
Constants.imageUrlExample,
Constants.anotherImageExample
],
reviews: [
Review(

View file

@ -0,0 +1,3 @@
struct LanguageDTO : Codable {
let language: String
}

View file

@ -0,0 +1,3 @@
struct ThemeDTO : Codable {
let theme: String
}

View file

@ -111,12 +111,22 @@ class ProfileServiceImpl: ProfileService {
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
func updateLanguage(code: String) {
// TODO: cmon
Task {
await AppNetworkHelper.put(
path: APIEndpoints.updateLanguageUrl,
body: LanguageDTO(language: code)
) as Result<SimpleResponse, ResourceError>
}
}
func updateTheme(code: String) {
// TODO: cmon
Task {
await AppNetworkHelper.put(
path: APIEndpoints.updateThemeUrl,
body: ThemeDTO(theme: code)
) as Result<SimpleResponse, ResourceError>
}
}
}

View file

@ -99,7 +99,7 @@ class AppNetworkHelper {
return .failure(ResourceError.other(message: "Encoding error"))
}
}
static func postWithoutBody<T: Decodable>(
path: String,
headers: [String: String] = [:],
@ -117,6 +117,30 @@ class AppNetworkHelper {
)
}
static func put<T: Decodable, U: Encodable>(
path: String,
body: U,
headers: [String: String] = [:],
decoder: JSONDecoder = JSONDecoder()
) async -> Result<T, ResourceError> {
guard let url = URL(string: path) else {
return .failure(.other(message: "Invalid URL"))
}
do {
let jsonData = try AppNetworkHelper.encodeRequestBody(body)
return await performRequest(
url: url,
method: "PUT",
body: jsonData,
headers: headers,
decoder: decoder
)
} catch {
return .failure(ResourceError.other(message: "Encoding error"))
}
}
static func performRequest<T: Decodable>(
url: URL,
method: String,
@ -169,6 +193,8 @@ class AppNetworkHelper {
switch httpResponse.statusCode {
case 200...299:
return try decodeResponse(data: data, as: T.self)
case 401:
throw ResourceError.unauthed
case 422:
let decodedResponse = try decodeResponse(data: data, as: ErrorResponse.self)
throw ResourceError.errorToUser(message: decodedResponse.message)

View file

@ -4,13 +4,14 @@ import Combine
class ProfileRepositoryImpl: ProfileRepository {
private let profileService: ProfileService
private let persistenceController: PersonalDataPersistenceController
private let userPreferences = UserPreferences.shared
let personalDataPassThroughSubject = PassthroughSubject<PersonalData, ResourceError>()
private var cancellables = Set<AnyCancellable>()
init(personalDataService: ProfileService, personalDataPersistenceController: PersonalDataPersistenceController) {
self.profileService = personalDataService
init(profileService: ProfileService, personalDataPersistenceController: PersonalDataPersistenceController) {
self.profileService = profileService
self.persistenceController = personalDataPersistenceController
}
@ -77,10 +78,12 @@ class ProfileRepositoryImpl: ProfileRepository {
}
func updateLanguage(code: String) {
// TODO: cmon
userPreferences.setLanguage(value: code)
profileService.updateLanguage(code: code)
}
func updateTheme(code: String) {
// TODO: cmon
userPreferences.setTheme(value: code)
profileService.updateTheme(code: code)
}
}

View file

@ -1,8 +1,9 @@
import Foundation
enum ResourceError: Error {
enum ResourceError: Error, Equatable {
case serverError(message: String)
case cacheError
case unauthed
case other(message: String)
case errorToUser(message: String)
@ -12,6 +13,8 @@ enum ResourceError: Error {
return L("server_error")
case .cacheError:
return L("cache_error")
case .unauthed:
return L("unauthed_error")
case .other:
return L("smth_went_wrong")
case .errorToUser(let message):

View file

@ -1,6 +1,6 @@
import Foundation
struct Review: Codable {
struct Review: Codable, Hashable {
let id: Int64
let placeId: Int64
let rating: Int

View file

@ -1,6 +1,6 @@
import Foundation
struct User: Codable {
struct User: Codable, Hashable {
let id: Int64
let name: String
let pfpUrl: String?

View file

@ -70,3 +70,30 @@ struct UICountryAsLabelView: UIViewRepresentable {
// nothing, go home
}
}
func getCountryFlag(code: String) -> CountryPickerView {
let cpv = CountryPickerView()
cpv.translatesAutoresizingMaskIntoConstraints = false
cpv.showCountryNameInView = false
cpv.showPhoneCodeInView = false
cpv.showCountryCodeInView = false
cpv.isUserInteractionEnabled = false
cpv.setCountryByCode(code)
return cpv
}
struct UICountryFlagView: UIViewRepresentable {
let code: String
init(code: String) {
self.code = code
}
func makeUIView(context: Context) -> CountryPickerView {
return getCountryFlag(code: code)
}
func updateUIView(_ uiView: CountryPickerView, context: Context) {
// nothing, go home
}
}

View file

@ -0,0 +1,7 @@
import SwiftUI
struct EmptyUI: View {
var body: some View {
Text(L("no_content"))
}
}

View file

@ -0,0 +1,93 @@
import SwiftUI
struct FlowStack<Data: RandomAccessCollection, Content: View>: View where Data.Element: Hashable {
let data: Data
let spacing: CGFloat
let alignment: HorizontalAlignment
@ViewBuilder let content: (Data.Element) -> Content
@State private var availableWidth: CGFloat = 0
var body: some View {
ZStack(alignment: Alignment(horizontal: alignment, vertical: .center)) {
SwiftUI.Color.clear
.frame(height: 1)
.readSize { size in
availableWidth = size.width
}
_FlowStack(
availableWidth: availableWidth,
data: data,
spacing: spacing,
alignment: alignment,
content: content
)
}
}
}
private struct _FlowStack<Data: RandomAccessCollection, Content: View>: View where Data.Element: Hashable {
let availableWidth: CGFloat
let data: Data
let spacing: CGFloat
let alignment: HorizontalAlignment
@ViewBuilder let content: (Data.Element) -> Content
@State private var elementsSize: [Data.Element: CGSize] = [:]
var body: some View {
VStack(alignment: alignment, spacing: spacing) {
ForEach(computeRows(), id: \.self) { rowElements in
HStack(spacing: spacing) {
ForEach(rowElements, id: \.self) { element in
content(element)
.fixedSize()
.readSize { size in
elementsSize[element] = size
}
}
}
}
}
}
private func computeRows() -> [[Data.Element]] {
var rows: [[Data.Element]] = [[]]
var currentRow = 0
var remainingWidth = availableWidth
for element in data {
let elementSize = elementsSize[element, default: CGSize(width: availableWidth, height: 1)]
if remainingWidth - (elementSize.width + spacing) >= 0 {
rows[currentRow].append(element)
} else {
currentRow += 1
rows.append([element])
remainingWidth = availableWidth
}
remainingWidth -= elementSize.width + spacing
}
return rows
}
}
private extension View {
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
SwiftUI.Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

View file

@ -17,7 +17,8 @@ struct LoadImageView: View {
.onFailure(perform: { error in
isError = true
})
.indicator(.activity).scaledToFill()
.indicator(.activity)
.scaledToFill()
.transition(.fade(duration: 0.2))
} else {
Text(L("no_image"))

View file

@ -0,0 +1,53 @@
import SwiftUI
import PhotosUI
struct MultiImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) private var presentationMode
@Binding var selectedImages: [UIImage]
let limit = 10
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.selectionLimit = limit
config.filter = .images
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
if(self.selectedImages.count > limit) {
let numOfRedundantImages = self.selectedImages.count - limit
selectedImages.removeLast(numOfRedundantImages)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
var parent: MultiImagePicker
init(_ parent: MultiImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
parent.presentationMode.wrappedValue.dismiss()
for result in results {
if result.itemProvider.canLoadObject(ofClass: UIImage.self) {
result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
if let image = image as? UIImage {
DispatchQueue.main.async {
self.parent.selectedImages.append(image)
}
}
}
}
}
}
}
}

View file

@ -1,19 +1,17 @@
import SwiftUI
struct BackButtonWithText: View {
var onBackClick: () -> Void
var body: some View {
Button(action: onBackClick) {
HStack {
Image(systemName: "chevron.left")
.resizable()
.frame(width: 16, height: 16)
Text("Back")
.font(.body)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
var onBackClick: () -> Void
var body: some View {
Button(action: onBackClick) {
HStack {
Image(systemName: "chevron.left")
.resizable()
.frame(width: 8, height: 16)
Text(L("back"))
.font(.body)
}
}
}
}

View file

@ -0,0 +1,20 @@
import SwiftUI
struct RatingBarView: View {
@Binding var rating: Double
let size: CGFloat
var body: some View {
HStack(spacing: 4) {
ForEach(0..<5) { index in
Image(systemName: index < Int(rating) ? "star.fill" : "star")
.resizable()
.frame(width: size, height: size)
.foregroundColor(Color.starYellow)
.onTapGesture {
rating = Double(index + 1)
}
}
}
}
}

View file

@ -26,6 +26,9 @@ class CategoriesViewController: UIViewController {
goToSearchScreen: { query in
let destinationVC = SearchViewController(searchVM: self.searchVM)
self.navigationController?.pushViewController(destinationVC, animated: true)
},
goToPlaceScreen: { id in
self.goToPlaceScreen(id: id)
}
)
)
@ -35,6 +38,7 @@ class CategoriesViewController: UIViewController {
struct CategoriesScreen: View {
@ObservedObject var categoriesVM: CategoriesViewModel
var goToSearchScreen: (String) -> Void
var goToPlaceScreen: (Int64) -> Void
var body: some View {
ScrollView {
@ -68,9 +72,8 @@ struct CategoriesScreen: View {
ForEach(categoriesVM.places) { place in
PlacesItem(
place: place,
onPlaceClick: { clickedPlace in
// Handle place click
print("Place clicked: \(clickedPlace.name)")
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
onFavoriteChanged: { isFavorite in
categoriesVM.toggleFavorite(for: place.id, isFavorite: isFavorite)

View file

@ -1,15 +1,63 @@
import SwiftUI
class FavoritesViewController: UIViewController {
private var favoritesVM: FavoritesViewModel = FavoritesViewModel()
override func viewDidLoad() {
super.viewDidLoad()
integrateSwiftUIScreen(FavoritesScreen())
integrateSwiftUIScreen(
FavoritesScreen(
favoritesVM: favoritesVM,
goToPlaceScreen: { id in
self.goToPlaceScreen(id: id)
}
)
)
}
}
struct FavoritesScreen: View {
@ObservedObject var favoritesVM: FavoritesViewModel
var goToPlaceScreen: (Int64) -> Void
var body: some View {
Text("favorites")
ScrollView {
VStack(alignment: .leading) {
VerticalSpace(height: 16)
VStack {
AppTopBar(title: L("favorites"))
AppSearchBar(
query: $favoritesVM.query,
onSearchClicked: { query in
// nothing, actually, it will be real time
},
onClearClicked: {
favoritesVM.clearQuery()
}
)
}
.padding(16)
VStack(spacing: 20) {
LazyVStack(spacing: 16) {
ForEach(favoritesVM.places) { place in
PlacesItem(
place: place,
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
onFavoriteChanged: { isFavorite in
favoritesVM.toggleFavorite(for: place.id, isFavorite: isFavorite)
}
)
}
}
.padding(.horizontal, 16)
}
VerticalSpace(height: 32)
}
}
}
}

View file

@ -0,0 +1,23 @@
import Combine
class FavoritesViewModel: ObservableObject {
@Published var query = ""
func clearQuery() { query = "" }
@Published var places: [PlaceShort] = []
init() {
// TODO: put real data
places = [
PlaceShort(id: 1, name: "sight 1", cover: Constants.imageUrlExample, rating: 4.5, excerpt: "yep, just a placeyep, just a placeyep, just a placeyep, just a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a placejust a placeyep, just a place", isFavorite: false),
PlaceShort(id: 2, name: "sight 2", cover: Constants.imageUrlExample, rating: 4.0, excerpt: "yep, just a place", isFavorite: true)
]
}
func toggleFavorite(for placeId: Int64, isFavorite: Bool) {
if let index = places.firstIndex(where: { $0.id == placeId }) {
places[index].isFavorite = isFavorite
}
}
}

View file

@ -71,23 +71,23 @@ struct Place: View {
}
.frame(width: width)
.background(SwiftUI.Color.black.opacity(0.5))
VStack {
HStack {
Spacer()
HStack {
Spacer()
VStack {
Button(action: {
onFavoriteChanged(!isFavorite)
}) {
Image(systemName: isFavorite ? "heart.fill" : "heart")
.foregroundColor(.white)
.padding(8)
.padding(12)
.background(SwiftUI.Color.white.opacity(0.2))
.clipShape(Circle())
}
Spacer()
}
Spacer()
}
.padding(12)
.frame(width: width, height: height)
}
.frame(width: width, height: height)
.clipShape(RoundedRectangle(cornerRadius: 16))

View file

@ -34,9 +34,7 @@ class HomeViewController: UIViewController {
self.navigationController?.pushViewController(destinationVC, animated: false)
},
goToPlaceScreen: { id in
let destinationVC = PlaceViewController(placeId: id)
self.navigationController?.pushViewController(destinationVC, animated: false)
self.tabBarController?.tabBar.isHidden = true
self.goToPlaceScreen(id: id)
}
)
)
@ -104,7 +102,7 @@ struct HomeScreen: View {
goToPlaceScreen(place.id)
},
setFavoriteChanged: { place, isFavorite in
// TODO: cmon
}
)
}
@ -114,10 +112,10 @@ struct HomeScreen: View {
title: L("restaurants"),
items: restaurants,
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
setFavoriteChanged: { place, isFavorite in
// TODO: cmon
}
)
}

View file

@ -27,7 +27,7 @@ struct PersonalDataScreen: View {
@ObservedObject var profileVM: ProfileViewModel
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var showPhotoPicker = false
@State private var showImagePicker = false
var body: some View {
ScrollView {
@ -65,7 +65,7 @@ struct PersonalDataScreen: View {
.textStyle(TextStyle.h4)
}
.onTapGesture {
showPhotoPicker = true
showImagePicker = true
}
}
@ -102,14 +102,14 @@ struct PersonalDataScreen: View {
)
}
.padding()
.sheet(isPresented: $showPhotoPicker) {
.sheet(isPresented: $showImagePicker) {
ImagePicker(sourceType: .photoLibrary, selectedImage: $profileVM.pfpToUpload)
}
}
.overlay(
Group {
if profileVM.shouldShowMessage {
ToastView(message: profileVM.messageToShow, isPresented: $profileVM.shouldShowMessage)
if profileVM.shouldShowMessageOnPersonalDataScreen {
ToastView(message: profileVM.messageToShowOnPersonalDataScreen, isPresented: $profileVM.shouldShowMessageOnPersonalDataScreen)
.padding(.bottom)
}
},

View file

@ -0,0 +1,36 @@
import SwiftUI
struct AllPicsScreen: View {
let urls: [String]
@Environment(\.presentationMode) var presentationMode
let minWidth = UIScreen.main.bounds.width / 2 - 16
let maxHeight = 150.0
var body: some View {
VStack(alignment: .leading) {
BackButtonWithText {
presentationMode.wrappedValue.dismiss()
}
ScrollView {
LazyVGrid(
columns: [
GridItem(.flexible(minimum: minWidth, maximum: minWidth)),
GridItem(.flexible(minimum: minWidth, maximum: minWidth))
],
spacing: 16
) {
ForEach(urls, id: \.self) { url in
LoadImageView(url: url)
.frame(maxWidth: minWidth, maxHeight: maxHeight)
.clipShape(RoundedRectangle(cornerRadius: 8))
.scaledToFill()
}
}
}
}
.padding(.horizontal, 16)
}
}

View file

@ -1,5 +1,4 @@
import SwiftUI
import SDWebImageSwiftUI
struct PlaceTopBar: View {
let title: String
@ -9,24 +8,22 @@ struct PlaceTopBar: View {
let onFavoriteChanged: (Bool) -> Void
let onMapClick: () -> Void
private let height: CGFloat = 160
private let height: CGFloat = 150
private let padding: CGFloat = 16
private let shape = RoundedCornerShape(corners: [.bottomLeft, .bottomRight], radius: 20)
var body: some View {
ZStack {
// Load image
LoadImageView(url: picUrl)
.frame(height: height)
.clipShape(
RoundedRectangle(cornerRadius: 20, style: .continuous)
)
.clipShape(shape
)
// Black overlay with opacity
SwiftUI.Color.black.opacity(0.3)
.frame(height: height)
.clipShape(
RoundedRectangle(cornerRadius: 20, style: .continuous)
)
.clipShape(shape)
// Top actions: Back, Favorite, Map
VStack {
@ -51,7 +48,7 @@ struct PlaceTopBar: View {
)
}
.padding(.horizontal, padding)
.padding(.top, 48)
.padding(.top, UIApplication.shared.statusBarFrame.height)
VerticalSpace(height: 32)
@ -79,7 +76,7 @@ struct PlaceTopBarAction: View {
Image(systemName: iconName)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.frame(width: 22, height: 22)
.padding(8)
.background(SwiftUI.Color.white.opacity(0.2))
.clipShape(Circle())

View file

@ -1,9 +1,56 @@
import SwiftUI
struct DescriptionScreen: View {
var description: String?
var onCreateRoute: (() -> Void)?
var body: some View {
ScrollView {
Text("Description")
}
ZStack {
// description
if let description = description {
ScrollView {
VStack(alignment: .leading) {
VerticalSpace(height: 16)
description.htmlToAttributedString()
.textStyle(.b1)
}
VerticalSpace(height: 100) // it's needed for visibility over the button below
}
} else {
EmptyUI()
}
// create route button
if let onCreateRoute = onCreateRoute {
VStack() {
Spacer()
PrimaryButton(
label: NSLocalizedString("show_route", comment: ""),
onClick: onCreateRoute
)
.padding(.bottom, 32)
.frame(maxWidth: .infinity, alignment: .bottom)
}.frame(minHeight: 0, maxHeight: .infinity)
}
}.padding(.horizontal, 16)
}
}
extension String {
func htmlToAttributedString() -> Text {
// Assuming you have a function to convert HTML to an attributed string
// Here's a basic version:
guard let data = self.data(using: .utf8) else { return Text(self) }
if let attributedString = try? NSAttributedString(
data: data,
options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue],
documentAttributes: nil
) {
return Text(attributedString.string)
}
return Text(self)
}
}

View file

@ -1,9 +1,54 @@
import SwiftUI
struct GalleryScreen: View {
let urls: [String]?
let secondRowHeight = 100.0
let shape = RoundedRectangle(cornerRadius: 8)
@State var goToAllGalleryScreen = false
var body: some View {
ScrollView {
Text("Gallery")
if let urls = urls, !urls.isEmpty {
VStack {
LoadImageView(url: urls.first)
.frame(height: 200)
.clipShape(shape)
VerticalSpace(height: 16)
HStack(spacing: 16) {
if urls.count > 1 {
LoadImageView(url: urls[1])
.frame(height: secondRowHeight)
.clipShape(shape)
.aspectRatio(1, contentMode: .fit)
if urls.count > 2 {
NavigationLink(destination: AllPicsScreen(urls: urls)) {
ZStack {
LoadImageView(url: urls[2])
.frame(height: secondRowHeight)
if urls.count > 3 {
SwiftUI.Color.black.opacity(0.5)
.frame(height: secondRowHeight)
.clipShape(shape)
Text("+\(urls.count - 3)")
.font(.headline)
.foregroundColor(.white)
}
}
.clipShape(shape)
.aspectRatio(1, contentMode: .fit)
}
}
}
}
Spacer()
}.padding(.horizontal, 16)
} else {
EmptyUI()
}
}
}

View file

@ -3,7 +3,7 @@ import SwiftUI
let tabBarShape = RoundedRectangle(cornerRadius: 50)
struct PlaceTabsBar: View {
let tabTitles = ["Description", "Gallery", "Reviews"]
let tabTitles = [L("description_tourism"), L("gallery"), L("reviews")]
@Binding var selectedTab: Int
@ -30,10 +30,11 @@ struct PlaceTabsBar: View {
var body: some View {
Button(action: action) {
Text(title)
.padding(8)
.textStyle(TextStyle.b1)
.padding(.vertical, 4)
.padding(.horizontal, 6)
.background(isSelected ? Color.selected : SwiftUI.Color.clear)
.cornerRadius(8)
.foregroundColor(Color.onBackground)
.foregroundColor(isSelected ? Color.onSelected : Color.onSurface)
.clipShape(tabBarShape)
}
}

View file

@ -40,34 +40,45 @@ struct PlaceScreen: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
PlaceTopBar(
title: "place",
picUrl: Constants.imageUrlExample,
onBackClick: {
presentationMode.wrappedValue.dismiss()
showBottomBar()
},
isFavorite: false,
onFavoriteChanged: { isFavorite in
// TODO: Cmon
},
onMapClick: {
// TODO: Cmon
}
)
if let place = placeVM.place {
VStack {
PlaceTabsBar(selectedTab: $selectedTab)
PlaceTopBar(
title: "place",
picUrl: Constants.imageUrlExample,
onBackClick: {
showBottomBar()
presentationMode.wrappedValue.dismiss()
},
isFavorite: false,
onFavoriteChanged: { isFavorite in
// TODO: Cmon
},
onMapClick: {
// TODO: Cmon
}
)
SwiftUI.TabView(selection: $selectedTab) {
DescriptionScreen().tag(0)
GalleryScreen().tag(1)
ReviewsScreen().tag(2)
VStack {
PlaceTabsBar(selectedTab: $selectedTab)
.padding()
SwiftUI.TabView(selection: $selectedTab) {
DescriptionScreen(
description: place.description,
onCreateRoute: {
// TODO: cmon
}
)
.tag(0)
GalleryScreen(urls: place.pics)
.tag(1)
ReviewsScreen(placeId: place.id, rating: place.rating)
.tag(2)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}.padding(16)
}
.edgesIgnoringSafeArea(.all)
}
.edgesIgnoringSafeArea(.all)
}
}

View file

@ -0,0 +1,24 @@
import SwiftUI
import Combine
struct AllReviewsScreen: View {
@ObservedObject var reviewsVM: ReviewsViewModel
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack(alignment: .leading) {
BackButtonWithText {
presentationMode.wrappedValue.dismiss()
}
ScrollView {
LazyVStack(spacing: 16) {
ForEach(reviewsVM.reviews, id: \.self) { review in
ReviewView(review: review, onDeleteClick: nil)
}
}
}
}
.padding(.horizontal, 16)
}
}

View file

@ -0,0 +1,134 @@
import SwiftUI
import Combine
import PhotosUI
struct PostReviewView: View {
@ObservedObject var postReviewVM: PostReviewViewModel
let placeId: Int64
let onPostReviewSuccess: () -> Void
@State private var showImagePicker = false
var body: some View {
ScrollView {
VStack {
Spacer().frame(height: 32)
Text(L("review_title"))
.font(.title)
Spacer().frame(height: 32)
VStack(alignment: .center) {
Text(L("tap_to_rate"))
.font(.body)
Spacer().frame(height: 4)
RatingBarView(rating: $postReviewVM.rating, size: 25)
}
Spacer().frame(height: 16)
MultilineTextField(L("text"), text: $postReviewVM.comment, minHeight: 80)
Spacer().frame(height: 16)
// Display the selected images
FlowStack(data: postReviewVM.files, spacing: 16, alignment: .center) { file in
ImagePreviewView(image: file) {
postReviewVM.removeFile(file)
}
}
Spacer().frame(height: 32)
if(postReviewVM.files.count < 10) {
VStack(alignment: .leading) {
PrimaryButton(
label: L("upload_photo"),
onClick: {
showImagePicker = true
},
isLoading: postReviewVM.isPosting
)
Spacer().frame(height: 4)
Text(L("images_number_warning"))
.textStyle(TextStyle.b1)
.foregroundColor(Color.hint)
Spacer().frame(height: 16)
}
}
PrimaryButton(
label: L("send"),
onClick: {
postReviewVM.postReview(placeId: placeId)
},
isLoading: postReviewVM.isPosting
)
Spacer().frame(height: 64)
}
.padding(.horizontal, 16)
.onReceive(postReviewVM.uiEvents) { event in
switch event {
case .closeReviewBottomSheet:
onPostReviewSuccess()
case .showToast(let message):
// TODO: cmon
print(message)
}
}
.sheet(isPresented: $showImagePicker) {
MultiImagePicker(selectedImages: $postReviewVM.files)
}
}
}
}
struct ImagePreviewView: View {
let image: UIImage
let onDelete: () -> Void
var body: some View {
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.cornerRadius(12)
Button(action: onDelete) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
}
.offset(x: 10, y: -10)
}
}
}
struct MultilineTextField: View {
@Binding var text: String
let placeholder: String
let minHeight: CGFloat
init(_ placeholder: String, text: Binding<String>, minHeight: CGFloat = 100) {
self._text = text
self.placeholder = placeholder
self.minHeight = minHeight
}
var body: some View {
ZStack(alignment: .topLeading) {
TextEditor(text: $text)
.frame(minHeight: minHeight)
.padding(4)
if text.isEmpty {
Text(placeholder)
.foregroundColor(SwiftUI.Color(.placeholderText))
.padding(.horizontal, 8)
.padding(.vertical, 12)
}
}
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(SwiftUI.Color.gray.opacity(0.2), lineWidth: 1)
)
}
}

View file

@ -0,0 +1,158 @@
import SwiftUI
import SDWebImageSwiftUI
struct ReviewView: View {
let review: Review
let onDeleteClick: (() -> Void)?
@State private var expandedComment = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Divider()
HStack {
UserView(user: review.user)
Spacer()
if review.deletionPlanned {
Text(L("deletionPlanned"))
.textStyle(TextStyle.b2)
.foregroundColor(Color.onBackground)
} else if let date = review.date {
Text(date)
.textStyle(TextStyle.b2)
.foregroundColor(Color.onBackground)
}
}
ReadOnlyRatingBarView(rating: Double(review.rating), size: 24)
if !review.picsUrls.isEmpty {
HStack(spacing: 8) {
ForEach(Array(review.picsUrls.prefix(3).enumerated()), id: \.offset) { index, url in
if index == 2 && review.picsUrls.count > 3 {
NavigationLink(destination: AllPicsScreen(urls: review.picsUrls)) {
ShowMoreView(url: url, remaining: review.picsUrls.count - 3)
}
} else {
ReviewPicView(url: url)
}
}
}
}
if let comment = review.comment, !comment.isEmpty {
CommentView(comment: comment, expanded: $expandedComment)
}
if let onDeleteClick = onDeleteClick {
Button(action: onDeleteClick) {
Text(L("delete_review"))
.foregroundColor(Color.heartRed)
}
}
}
}
}
struct UserView: View {
let user: User
var body: some View {
HStack {
if let pfpUrl = user.pfpUrl {
WebImage(url: URL(string: pfpUrl))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 66, height: 66)
.clipShape(Circle())
}
HStack() {
Text(user.name)
.textStyle(TextStyle.h3)
.foregroundColor(Color.onBackground)
UICountryFlagView(code: user.countryCodeName)
.scaledToFit()
.frame(height: 30)
}
Spacer()
}
}
}
struct ReadOnlyRatingBarView: View {
let rating: Double
let size: CGFloat
var body: some View {
HStack(spacing: 4) {
ForEach(0..<5) { index in
Image(systemName: index < Int(rating) ? "star.fill" : "star")
.resizable()
.frame(width: size, height: size)
.foregroundColor(Color.starYellow)
}
}
}
}
struct CommentView: View {
let comment: String
@Binding var expanded: Bool
var body: some View {
VStack(alignment: .leading) {
Text(comment)
.textStyle(TextStyle.b1)
.lineLimit(expanded ? nil : 2)
.onTapGesture {
expanded.toggle()
}
VerticalSpace(height: 16)
if !expanded {
Button(L("more")) { expanded.toggle() }
.foregroundColor(Color.primary)
} else {
Button(L("less")) { expanded.toggle() }
.foregroundColor(Color.primary)
}
}
.padding()
.background(Color.surface)
.cornerRadius(10)
}
}
let reviewPicWidth = 73.0
let reviewPicHeight = 65.0
struct ReviewPicView: View {
let url: String
var body: some View {
WebImage(url: URL(string: url))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: reviewPicWidth, height: reviewPicHeight)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
struct ShowMoreView: View {
let url: String
let remaining: Int
var body: some View {
ZStack {
ReviewPicView(url: url)
SwiftUI.Color.black.opacity(0.5)
Text("+\(remaining)")
.textStyle(TextStyle.h3)
.foregroundColor(SwiftUI.Color.white)
}
.frame(width: reviewPicWidth, height: reviewPicHeight)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}

View file

@ -0,0 +1,62 @@
import Foundation
import SwiftUI
import Combine
class PostReviewViewModel: ObservableObject {
@Published var rating: Double = 5
@Published var comment: String = ""
@Published var files: [UIImage] = []
@Published var isPosting: Bool = false
private var cancellables = Set<AnyCancellable>()
// private let reviewsRepository: ReviewsRepository
let uiEvents = PassthroughSubject<UiEvent, Never>()
// init(reviewsRepository: ReviewsRepository) {
// self.reviewsRepository = reviewsRepository
// }
func setRating(_ value: Double) {
rating = value
}
func addPickedImage() {
// guard let pickedImage = pickedImage else { return }
// Task {
// if let data = try? await pickedImage.loadTransferable(type: Data.self), let image = UIImage(data: data) {
// files.append(image)
// }
// }
}
func removeFile(_ file: UIImage) {
files.removeAll { $0 == file }
}
func postReview(placeId: Int64) {
// isPosting = true
//
// let review = ReviewToPost(placeId: placeId, comment: comment, rating: rating, images: files)
// reviewsRepository.postReview(review)
// .receive(on: DispatchQueue.main)
// .sink { completion in
// self.isPosting = false
// switch completion {
// case .finished:
// self.uiEvents.send(.showToast(message: "Review Posted"))
// self.uiEvents.send(.closeReviewBottomSheet)
// case .failure(let error):
// self.uiEvents.send(.showToast(message: error.localizedDescription))
// }
// } receiveValue: { response in
// print("Review posted successfully")
// }
// .store(in: &cancellables)
}
}
enum UiEvent {
case closeReviewBottomSheet
case showToast(message: String)
}

View file

@ -1,9 +1,65 @@
import SwiftUI
struct ReviewsScreen: View {
@ObservedObject var reviewsVM = ReviewsViewModel()
let placeId: Int64
let rating: Double?
@State private var showReviewSheet = false
var body: some View {
VStack {
Text("Reviews")
ScrollView {
VStack {
// overal rating
HStack(alignment: .center) {
Image(systemName: "star.fill")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(Color.starYellow)
if let rating = rating {
Text("\(String(format: "%.1f", rating) )/5")
.font(.system(size: 30))
}
Spacer()
Button(L("compose_review")) {
showReviewSheet = true
}
.foregroundColor(Color.primary)
}
VerticalSpace(height: 16)
HStack {
Spacer()
NavigationLink(destination: AllReviewsScreen(reviewsVM: reviewsVM)) {
Text(L("see_all_reviews"))
.foregroundColor(Color.primary)
}
}
// user review
ReviewView(
review: Constants.reviewExample,
onDeleteClick: {}
)
// most recent recent review
ReviewView(
review: Constants.reviewExample,
onDeleteClick: nil
)
}
}
.padding(.horizontal, 16)
.sheet(isPresented: $showReviewSheet) {
PostReviewView(
postReviewVM: PostReviewViewModel(),
placeId: placeId) {
// TODO: cmon
}
}
}
}

View file

@ -0,0 +1,19 @@
import Combine
class ReviewsViewModel: ObservableObject {
@Published var reviews: [Review] = [Constants.reviewExample]
@Published var userReview: Review? = nil
@Published var isThereReviewPlannedToPublish = false
func getReviews(id: Int64) {
// TODO: cmon user review and all other reviews
}
func deleteReview() {
// TODO: cmon
}
init() {
// TODO: cmon get isThereReviewPlannedToPublish from DB
}
}

View file

@ -2,26 +2,28 @@ import UIKit
import SwiftUI
class ProfileViewController: UIViewController {
private var profileVM: ProfileViewModel
init(profileVM: ProfileViewModel) {
self.profileVM = profileVM
super.init(
nibName: nil,
bundle: nil
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let profileVM = ProfileViewModel(
currencyRepository: CurrencyRepositoryImpl(
currencyService: CurrencyServiceImpl(),
currencyPersistenceController: CurrencyPersistenceController.shared
),
profileRepository: ProfileRepositoryImpl(
personalDataService: ProfileServiceImpl(userPreferences: UserPreferences.shared),
personalDataPersistenceController: PersonalDataPersistenceController.shared
),
authRepository: AuthRepositoryImpl(authService: AuthServiceImpl()),
userPreferences: UserPreferences.shared
)
integrateSwiftUIScreen(
ProfileScreen(
profileVM: profileVM,
onPersonalDataClick: {
let destinationVC = PersonalDataViewController(profileVM: profileVM)
let destinationVC = PersonalDataViewController(profileVM: self.profileVM)
self.navigationController?.pushViewController(destinationVC, animated: true)
}
)
@ -32,21 +34,22 @@ class ProfileViewController: UIViewController {
struct ProfileScreen: View {
@ObservedObject var profileVM: ProfileViewModel
let onPersonalDataClick: () -> Void
@ObservedObject var themeVM: ThemeViewModel = ThemeViewModel(userPreferences: UserPreferences.shared)
@ObservedObject var themeVM: ThemeViewModel = ThemeViewModel(
profileRepository: ProfileRepositoryImpl(
profileService: ProfileServiceImpl(userPreferences: UserPreferences.shared),
personalDataPersistenceController: PersonalDataPersistenceController.shared
),
userPreferences: UserPreferences.shared
)
@State private var isSheetOpen = false
@State private var signOutLoading = false
@State private var navigateToPersonalData = false
func onLanguageClick () {
navigateToLanguageSettings()
profileVM.setLanguageOnSystemLocaleChange()
}
var body: some View {
NavigationLink(destination: PersonalDataScreen(profileVM: profileVM), isActive: $navigateToPersonalData) {
EmptyView()
}.hidden()
ScrollView {
VStack (alignment: .leading) {
AppTopBar(title: L("tourism_profile"))
@ -92,9 +95,19 @@ struct ProfileScreen: View {
}
.padding(16)
}
.overlay(
Group {
if profileVM.shouldShowMessageOnProfileScreen {
ToastView(message: profileVM.messageToShowOnProfileScreen, isPresented: $profileVM.shouldShowMessageOnProfileScreen)
.padding(.bottom)
}
},
alignment: .bottom
)
.sheet(isPresented: $isSheetOpen) {
SignOutWarning(
onSignOutClick: {
isSheetOpen = false
signOutLoading = true
profileVM.signOut()
},
@ -203,7 +216,7 @@ struct ThemeSwitch: View {
var body: some View {
HStack {
Text("Dark Theme")
Text(L("Dark Theme"))
.textStyle(TextStyle.b1)
Spacer()

View file

@ -9,8 +9,11 @@ class ProfileViewModel: ObservableObject {
private let userPreferences: UserPreferences
var onSignOutCompleted: (() -> Void)? = nil
@Published var messageToShow = ""
@Published var shouldShowMessage = false
@Published var messageToShowOnProfileScreen = ""
@Published var shouldShowMessageOnProfileScreen = false
@Published var messageToShowOnPersonalDataScreen = ""
@Published var shouldShowMessageOnPersonalDataScreen = false
@Published var pfpFromRemote: URL? = nil
@Published var pfpToUpload = UIImage()
@ -21,7 +24,6 @@ class ProfileViewModel: ObservableObject {
@Published var email: String = ""
@Published var countryCodeName: String? = nil
@Published var personalData: PersonalData? = nil
@Published var signOutResponse: SimpleResponse? = nil
@Published var currencyRates: CurrencyRates? = nil
private var cancellables = Set<AnyCancellable>()
@ -68,7 +70,10 @@ class ProfileViewModel: ObservableObject {
profileRepository.personalDataPassThroughSubject
.sink { completion in
if case let .failure(error) = completion {
self.showMessage(error.errorDescription)
if(error == ResourceError.unauthed) {
self.onSignOutCompleted?()
}
self.showMessageOnProfileScreen(error.errorDescription)
}
} receiveValue: { resource in
self.personalData = resource
@ -96,15 +101,15 @@ class ProfileViewModel: ObservableObject {
)
.sink { completion in
if case let .failure(error) = completion {
self.showMessage(error.errorDescription)
self.showMessageOnPersonalDataScreen(error.errorDescription)
}
} receiveValue: { resource in
self.updatePersonalDataInMemory(personalData: resource)
self.showMessage(L("saved"))
self.showMessageOnPersonalDataScreen(L("saved"))
}
.store(in: &cancellables)
} else {
self.showMessage(L("please_fill_all_fields"))
self.showMessageOnPersonalDataScreen(L("please_fill_all_fields"))
}
}
@ -118,7 +123,7 @@ class ProfileViewModel: ObservableObject {
currencyRepository.currencyPassThroughSubject
.sink { completion in
if case let .failure(error) = completion {
self.showMessage(error.errorDescription)
self.showMessageOnProfileScreen(error.errorDescription)
}
} receiveValue: { resource in
self.currencyRates = resource
@ -128,23 +133,37 @@ class ProfileViewModel: ObservableObject {
currencyRepository.getCurrency()
}
// TODO: this doesn't work, try to find some other solutions
// I tried to update language remotely after user set the new language
func setLanguageOnSystemLocaleChange() {
NotificationCenter.default.addObserver(self, selector: #selector(localeChanged), name: NSLocale.currentLocaleDidChangeNotification, object: nil)
}
@objc func localeChanged() {
profileRepository.updateLanguage(code: NSLocale.current.identifier)
}
func signOut() {
authRepository.signOut()
.sink { completion in
if case let .failure(error) = completion {
self.showMessage(error.errorDescription)
self.showMessageOnProfileScreen(error.errorDescription)
}
} receiveValue: { response in
self.signOutResponse = response
self.userPreferences.setToken(value: nil)
self.showMessageOnProfileScreen(response.message)
self.onSignOutCompleted?()
self.showMessage(response.message)
}
.store(in: &cancellables)
}
func showMessage(_ message: String) {
messageToShow = message
shouldShowMessage = true
func showMessageOnPersonalDataScreen(_ message: String) {
messageToShowOnPersonalDataScreen = message
shouldShowMessageOnPersonalDataScreen = true
}
func showMessageOnProfileScreen(_ message: String) {
messageToShowOnProfileScreen = message
shouldShowMessageOnProfileScreen = true
}
}

View file

@ -18,13 +18,19 @@ class SearchViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
integrateSwiftUIScreen(SearchScreen(searchVM: searchVM))
integrateSwiftUIScreen(SearchScreen(
searchVM: searchVM,
goToPlaceScreen: { id in
self.goToPlaceScreen(id: id)
}
))
}
}
struct SearchScreen: View {
@ObservedObject var searchVM: SearchViewModel
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var goToPlaceScreen: (Int64) -> Void
var body: some View {
ScrollView {
@ -53,9 +59,8 @@ struct SearchScreen: View {
ForEach(searchVM.places) { place in
PlacesItem(
place: place,
onPlaceClick: { clickedPlace in
// Handle place click
print("Place clicked: \(clickedPlace.name)")
onPlaceClick: { place in
goToPlaceScreen(place.id)
},
onFavoriteChanged: { isFavorite in
searchVM.toggleFavorite(for: place.id, isFavorite: isFavorite)

View file

@ -3,6 +3,12 @@ import SwiftUI
class TabBarController: UITabBarController {
override func viewDidAppear(_ animated: Bool) {
if let theme = UserPreferences.shared.getTheme() {
changeTheme(themeCode: theme.code)
}
}
override func viewDidLoad() {
super.viewDidLoad()
@ -22,7 +28,22 @@ class TabBarController: UITabBarController {
// creating shared ViewModels
let categoriesVM = CategoriesViewModel()
let searchViewModel = SearchViewModel()
let searchVM = SearchViewModel()
let profileVM = ProfileViewModel(
currencyRepository: CurrencyRepositoryImpl(
currencyService: CurrencyServiceImpl(),
currencyPersistenceController: CurrencyPersistenceController.shared
),
profileRepository: ProfileRepositoryImpl (
profileService: ProfileServiceImpl(userPreferences: UserPreferences.shared),
personalDataPersistenceController: PersonalDataPersistenceController.shared
),
authRepository: AuthRepositoryImpl(authService: AuthServiceImpl()),
userPreferences: UserPreferences.shared
)
profileVM.onSignOutCompleted = {
self.performSegue(withIdentifier: "TourismMain2Auth", sender: nil)
}
// navigation functions
let goToCategoriesTab = { self.selectedIndex = 1 }
@ -30,15 +51,15 @@ class TabBarController: UITabBarController {
// creating ViewControllers
let homeVC = HomeViewController(
categoriesVM: categoriesVM,
searchVM: searchViewModel,
searchVM: searchVM,
goToCategoriesTab: goToCategoriesTab
)
let categoriesVC = CategoriesViewController(
categoriesVM: categoriesVM,
searchVM: searchViewModel
searchVM: searchVM
)
let favoritesVC = FavoritesViewController()
let profileVC = ProfileViewController()
let profileVC = ProfileViewController(profileVM: profileVM)
// setting up navigation
homeNav.viewControllers = [homeVC]

View file

@ -2,39 +2,26 @@ import Foundation
import Combine
class ThemeViewModel: ObservableObject {
// private let profileRepository: ProfileRepository
private let profileRepository: ProfileRepository
private let userPreferences: UserPreferences
@Published var theme: UserPreferences.Theme?
private var cancellables = Set<AnyCancellable>()
init(
// profileRepository: ProfileRepository,
profileRepository: ProfileRepository,
userPreferences: UserPreferences) {
// self.profileRepository = profileRepository
self.userPreferences = userPreferences
self.theme = userPreferences.getTheme()
}
func setTheme(themeCode: String) {
if let newTheme = userPreferences.themes.first(where: { $0.code == themeCode }) {
self.theme = newTheme
userPreferences.setTheme(value: themeCode)
self.profileRepository = profileRepository
self.userPreferences = userPreferences
self.theme = userPreferences.getTheme()
}
func setTheme(themeCode: String) {
profileRepository.updateTheme(code: themeCode)
}
func updateThemeOnServer(themeCode: String) {
// profileRepository.updateTheme(themeCode)
// .sink { completion in
// if case let .failure(error) = completion {
// // Handle error if needed
// }
// } receiveValue: { response in
// // Handle success if needed
// }
// .store(in: &cancellables)
profileRepository.updateTheme(code: themeCode)
}
}

View file

@ -20,6 +20,9 @@
<color key="backgroundColor" name="Background"/>
</view>
<navigationItem key="navigationItem" id="OYr-7O-lYe"/>
<connections>
<segue destination="Q4m-49-p55" kind="presentation" identifier="TourismMain2Auth" modalPresentationStyle="fullScreen" id="mcy-b6-vPp"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
@ -43,6 +46,14 @@
</objects>
<point key="canvasLocation" x="23.664122137404579" y="-2.1126760563380285"/>
</scene>
<!--Auth-->
<scene sceneID="4jT-sB-oOW">
<objects>
<viewControllerPlaceholder storyboardName="Auth" id="Q4m-49-p55" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="85I-7x-WNl" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1734" y="-2"/>
</scene>
</scenes>
<resources>
<namedColor name="Background">

View file

@ -0,0 +1,7 @@
extension UIViewController {
func goToPlaceScreen(id: Int64) {
let destinationVC = PlaceViewController(placeId: id)
self.navigationController?.pushViewController(destinationVC, animated: false)
self.tabBarController?.tabBar.isHidden = true
}
}

View file

@ -0,0 +1,15 @@
import SwiftUI
struct RoundedCornerShape: Shape {
var corners: UIRectCorner
var radius: CGFloat
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}