forked from organicmaps/organicmaps
backup, UI/UX finished everywhere
This commit is contained in:
parent
c5e3417af0
commit
d1bf71528e
47 changed files with 1290 additions and 197 deletions
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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" = "Отзыв был успешно опубликован";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
struct LanguageDTO : Codable {
|
||||
let language: String
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
struct ThemeDTO : Codable {
|
||||
let theme: String
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
struct Review: Codable {
|
||||
struct Review: Codable, Hashable {
|
||||
let id: Int64
|
||||
let placeId: Int64
|
||||
let rating: Int
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
struct User: Codable {
|
||||
struct User: Codable, Hashable {
|
||||
let id: Int64
|
||||
let name: String
|
||||
let pfpUrl: String?
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct EmptyUI: View {
|
||||
var body: some View {
|
||||
Text(L("no_content"))
|
||||
}
|
||||
}
|
93
iphone/Maps/Tourism/Presentation/Components/FlowLayout.swift
Normal file
93
iphone/Maps/Tourism/Presentation/Components/FlowLayout.swift
Normal 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) {}
|
||||
}
|
|
@ -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"))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue