forked from organicmaps/organicmaps
android: disable car support, sent to Play Store for review; ios: backup
This commit is contained in:
parent
52bf2acb91
commit
5ae27cbccb
54 changed files with 871 additions and 350 deletions
3
android/app/.gitignore
vendored
3
android/app/.gitignore
vendored
|
@ -31,3 +31,6 @@
|
|||
|
||||
# ignore autogenerated metadata (see prepareGoogleReleaseListing in build.gradle)
|
||||
/src/google/play/listings
|
||||
|
||||
# ignore google releases
|
||||
/google/release
|
||||
|
|
|
@ -87,7 +87,7 @@ def getCommitMessage() {
|
|||
def osName = System.properties['os.name'].toLowerCase()
|
||||
|
||||
project.ext.appId = 'tj.tourism.rebus'
|
||||
project.ext.appName = 'Tourism'
|
||||
project.ext.appName = 'Tourism Map Tajikistan'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
|
@ -111,10 +111,10 @@ android {
|
|||
defaultConfig {
|
||||
// Default package name is taken from the manifest and should be app.organicmaps
|
||||
def ver = getVersion()
|
||||
versionCode = ver.V1
|
||||
versionName = ver.V2
|
||||
println('Version: ' + versionName)
|
||||
println('VersionCode: ' + versionCode)
|
||||
versionCode = 2
|
||||
versionName = "1.0.0"
|
||||
// println('Version: ' + versionName)
|
||||
// println('VersionCode: ' + versionCode)
|
||||
minSdk propMinSdkVersion.toInteger()
|
||||
targetSdk propTargetSdkVersion.toInteger()
|
||||
applicationId project.ext.appId
|
||||
|
|
|
@ -44,8 +44,8 @@
|
|||
//
|
||||
-->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES" />
|
||||
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
|
||||
<!-- <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES" />-->
|
||||
<!-- <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />-->
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
|
@ -444,16 +444,16 @@
|
|||
android:label="@string/driving_options_title" />
|
||||
<activity android:name=".MapPlaceholderActivity" />
|
||||
|
||||
<service
|
||||
android:name=".car.CarAppService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="location">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.car.app.CarAppService" />
|
||||
<!-- <service-->
|
||||
<!-- android:name=".car.CarAppService"-->
|
||||
<!-- android:exported="true"-->
|
||||
<!-- android:foregroundServiceType="location">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="androidx.car.app.CarAppService" />-->
|
||||
|
||||
<category android:name="androidx.car.app.category.NAVIGATION" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<!-- <category android:name="androidx.car.app.category.NAVIGATION" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </service>-->
|
||||
<service
|
||||
android:name=".routing.NavigationService"
|
||||
android:enabled="true"
|
||||
|
@ -483,12 +483,12 @@
|
|||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
<meta-data
|
||||
android:name="androidx.car.app.minCarApiLevel"
|
||||
android:value="5" />
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="com.google.android.gms.car.application"-->
|
||||
<!-- android:resource="@xml/automotive_app_desc" />-->
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="androidx.car.app.minCarApiLevel"-->
|
||||
<!-- android:value="5" />-->
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -47,6 +47,6 @@ interface ReviewsDao {
|
|||
@Query("SELECT * FROM reviews_planned_to_post")
|
||||
fun getReviewsPlannedToPost(): List<ReviewPlannedToPostEntity>
|
||||
|
||||
@Query("SELECT * FROM reviews_planned_to_post")
|
||||
fun getReviewsPlannedToPostFlow(): Flow<List<ReviewPlannedToPostEntity>>
|
||||
@Query("SELECT * FROM reviews_planned_to_post WHERE placeId = :placeId")
|
||||
fun getReviewsPlannedToPostFlow(placeId: Long): Flow<List<ReviewPlannedToPostEntity>>
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ class ReviewsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
fun isThereReviewPlannedToPublish(): Flow<Boolean> = channelFlow {
|
||||
reviewsDao.getReviewsPlannedToPostFlow().collectLatest { reviewsEntities ->
|
||||
fun isThereReviewPlannedToPublish(placeId: Long): Flow<Boolean> = channelFlow {
|
||||
reviewsDao.getReviewsPlannedToPostFlow(placeId).collectLatest { reviewsEntities ->
|
||||
send(reviewsEntities.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ class ReviewsRepository(
|
|||
try {
|
||||
saveToInternalStorage(imageFiles, context)
|
||||
reviewsDao.insertReviewPlannedToPost(review.toReviewPlannedToPostEntity(imageFiles))
|
||||
emit(Resource.Error(context.getString(R.string.review_will_be_published)))
|
||||
emit(Resource.Error(context.getString(R.string.review_will_be_published_when_online)))
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printStackTrace()
|
||||
emit(Resource.Error(context.getString(R.string.smth_went_wrong)))
|
||||
|
|
|
@ -16,6 +16,7 @@ fun CountryAsLabel(modifier: Modifier = Modifier, countryCodeName: String, conte
|
|||
.inflate(R.layout.ccp_as_country_label, null, false)
|
||||
val ccp = view.findViewById<CountryCodePicker>(R.id.ccp)
|
||||
ccp.contentColor = contentColor
|
||||
ccp.setCountryForNameCode("BO")
|
||||
ccp.setCountryForNameCode(countryCodeName)
|
||||
ccp.showArrow(false)
|
||||
ccp.setCcpClickable(false)
|
||||
|
|
|
@ -105,7 +105,7 @@ fun HomeScreen(
|
|||
},
|
||||
contentWindowInsets = WindowInsets(left = 0.dp, right = 0.dp, top = 0.dp, bottom = 0.dp)
|
||||
) { paddingValues ->
|
||||
if (downloadResponse is Resource.Success)
|
||||
if (downloadResponse is Resource.Success || downloadResponse is Resource.Idle)
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
|
|
|
@ -53,7 +53,6 @@ class HomeViewModel @Inject constructor(
|
|||
if (resource is Resource.Success) {
|
||||
resource.data?.let {
|
||||
_sights.value = it
|
||||
Log.d("lok narosh", it.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +72,6 @@ class HomeViewModel @Inject constructor(
|
|||
.collectLatest { resource ->
|
||||
if (resource is Resource.Success) {
|
||||
resource.data?.let {
|
||||
Log.d("lok narosh", it.toString())
|
||||
_restaurants.value = it
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ class PostReviewViewModel @Inject constructor(
|
|||
uiChannel.send(
|
||||
UiEvent.ShowToast(it.message ?: context.getString(R.string.smth_went_wrong))
|
||||
)
|
||||
if (it.message == context.getString(R.string.review_will_be_published)) {
|
||||
if (it.message == context.getString(R.string.review_will_be_published_when_online)) {
|
||||
uiChannel.send(UiEvent.CloseReviewBottomSheet)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,8 +70,10 @@ class ReviewsViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
reviewsRepository.isThereReviewPlannedToPublish().collectLatest {
|
||||
_isThereReviewPlannedToPublish.value = it
|
||||
userReview.value?.id?.let { placeId ->
|
||||
reviewsRepository.isThereReviewPlannedToPublish(placeId).collectLatest {
|
||||
_isThereReviewPlannedToPublish.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
|
@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
|
@ -44,6 +46,7 @@ import app.tourism.ui.common.HorizontalSpace
|
|||
import app.tourism.ui.common.LoadImg
|
||||
import app.tourism.ui.common.VerticalSpace
|
||||
import app.tourism.ui.common.special.CountryAsLabel
|
||||
import app.tourism.ui.common.special.CountryFlag
|
||||
import app.tourism.ui.common.special.RatingBar
|
||||
import app.tourism.ui.screens.main.place_details.gallery.imageShape
|
||||
import app.tourism.ui.theme.TextStyles
|
||||
|
@ -138,20 +141,22 @@ fun User(modifier: Modifier = Modifier, user: User) {
|
|||
url = user.pfpUrl,
|
||||
)
|
||||
HorizontalSpace(width = 12.dp)
|
||||
Column {
|
||||
VerticalSpace(height = 6.dp)
|
||||
Row(modifier = Modifier) {
|
||||
Column {
|
||||
VerticalSpace(3.dp)
|
||||
CountryFlag(
|
||||
modifier = Modifier.width(50.dp),
|
||||
countryCodeName = user.countryCodeName,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.weight(1f).width(IntrinsicSize.Min),
|
||||
text = user.name,
|
||||
style = TextStyles.h4,
|
||||
fontWeight = FontWeight.W600,
|
||||
maxLines = 1,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
CountryAsLabel(
|
||||
Modifier.fillMaxWidth(),
|
||||
user.countryCodeName,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground.toArgb(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ class ProfileViewModel @Inject constructor(
|
|||
profileRepository.updateProfile(
|
||||
fullName = fullName.value,
|
||||
country = countryCodeName.value ?: "",
|
||||
email = if (currentEmail == email.value) null else email.value,
|
||||
email = email.value,
|
||||
pfpFile.value
|
||||
).collectLatest { resource ->
|
||||
if (resource is Resource.Success) {
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
android:paddingVertical="12dp"
|
||||
app:ccp_autoDetectLanguage="true"
|
||||
app:ccp_contentColor="@color/white_primary"
|
||||
app:ccpDialog_backgroundColor="@color/transparent"
|
||||
app:ccpDialog_backgroundColor="@color/black_secondary"
|
||||
app:ccpDialog_textColor="@color/white_primary"
|
||||
app:ccp_arrowColor="@color/white_primary"
|
||||
app:ccp_flagBorderColor="@color/white_primary"
|
||||
app:ccp_textGravity="LEFT"
|
||||
app:ccp_padding="0dp"
|
||||
app:ccpDialog_background="@color/transparent"
|
||||
app:ccpDialog_background="@color/black_secondary"
|
||||
app:ccpDialog_cornerRadius="16dp"
|
||||
app:ccp_showFullName="true"
|
||||
app:ccp_showPhoneCode="false">
|
||||
|
|
|
@ -2226,7 +2226,7 @@
|
|||
<string name="deletionPlanned">В процессе удаления</string>
|
||||
<string name="plz_wait_dowloading">Пожалуйста подождите данные скачиваются</string>
|
||||
<string name="empty_list">Пусто</string>
|
||||
<string name="review_will_be_published">Отзыв будет публикован когда будете онлайн</string>
|
||||
<string name="review_will_be_published_when_online">Отзыв будет публикован когда будете онлайн</string>
|
||||
<string name="review_was_published">Отзыв был успешно опубликован</string>
|
||||
<string name="failed_to_publish_review">Не удалось публиковать отзыв</string>
|
||||
<string name="plz_dont_go_out_of_tjk">Поажалуйста, не выходите за рамки Таджикистана, вы должны быть в Таджикистане</string>
|
||||
|
|
|
@ -2268,7 +2268,7 @@
|
|||
<string name="deletionPlanned">Deleting…</string>
|
||||
<string name="plz_wait_dowloading">Please, wait, data being downloaded</string>
|
||||
<string name="empty_list">Пусто</string>
|
||||
<string name="review_will_be_published">Review will be published when you are online</string>
|
||||
<string name="review_will_be_published_when_online">Review will be published when you are online</string>
|
||||
<string name="review_was_published">Review was successfully published</string>
|
||||
<string name="failed_to_publish_review">Failed to publish review\n</string>
|
||||
<string name="plz_dont_go_out_of_tjk">Please, don\'t go out of Tajikistan, it\'s Tajikistan app</string>
|
||||
|
|
|
@ -4053,7 +4053,7 @@
|
|||
|
||||
"retry" = "Try again";
|
||||
|
||||
"no_network" = "Couldn't reach the server, please check connection";
|
||||
"no_connection" = "Couldn't reach the server, please check connection";
|
||||
|
||||
"no_image" = "No image";
|
||||
|
||||
|
@ -4101,7 +4101,7 @@
|
|||
|
||||
"back" = "Back";
|
||||
|
||||
"review_will_be_published" = "Review will be published when you are online";
|
||||
"review_will_be_published_when_online" = "Review will be published when you are online";
|
||||
|
||||
"review_was_published" = "Review was successfully published";
|
||||
|
||||
|
|
|
@ -4053,7 +4053,7 @@
|
|||
|
||||
"retry" = "Try again";
|
||||
|
||||
"no_network" = "Couldn't reach the server, please check connection";
|
||||
"no_connection" = "Couldn't reach the server, please check connection";
|
||||
|
||||
"no_image" = "No image";
|
||||
|
||||
|
@ -4101,7 +4101,7 @@
|
|||
|
||||
"back" = "Back";
|
||||
|
||||
"review_will_be_published" = "Review will be published when you are online";
|
||||
"review_will_be_published_when_online" = "Review will be published when you are online";
|
||||
|
||||
"review_was_published" = "Review was successfully published";
|
||||
|
||||
|
|
|
@ -4053,7 +4053,7 @@
|
|||
|
||||
"retry" = "Попробовать заново";
|
||||
|
||||
"no_network" = "Не удается соединиться с сервером, проверьте интернет подключение";
|
||||
"no_connection" = "Не удается соединиться с сервером, проверьте интернет подключение";
|
||||
|
||||
"no_image" = "Нет фото";
|
||||
|
||||
|
@ -4101,7 +4101,7 @@
|
|||
|
||||
"back" = "Назад";
|
||||
|
||||
"review_will_be_published" = "Отзыв будет публикован когда будете онлайн";
|
||||
"review_will_be_published_when_online" = "Отзыв будет публикован когда будете онлайн";
|
||||
|
||||
"review_was_published" = "Отзыв был успешно опубликован";
|
||||
|
||||
|
|
|
@ -305,14 +305,11 @@
|
|||
529A5F192C85BFF0004FE4A1 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F182C85BFF0004FE4A1 /* ToastView.swift */; };
|
||||
529A5F1E2C86DDE5004FE4A1 /* PlaceDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */; };
|
||||
529A5F202C86DE14004FE4A1 /* CoordinatesDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */; };
|
||||
529A5F222C86DE50004FE4A1 /* ReviewDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F212C86DE50004FE4A1 /* ReviewDTO.swift */; };
|
||||
529A5F242C86DE7D004FE4A1 /* ReviewIdsDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F232C86DE7D004FE4A1 /* ReviewIdsDTO.swift */; };
|
||||
529A5F262C86DE9D004FE4A1 /* ReviewsDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F252C86DE9D004FE4A1 /* ReviewsDTO.swift */; };
|
||||
529A5F222C86DE50004FE4A1 /* Reviews DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F212C86DE50004FE4A1 /* Reviews DTOs.swift */; };
|
||||
529A5F282C86DEC5004FE4A1 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F272C86DEC5004FE4A1 /* UserDTO.swift */; };
|
||||
529A5F2B2C86DF2D004FE4A1 /* PlaceShort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F2A2C86DF2D004FE4A1 /* PlaceShort.swift */; };
|
||||
529A5F2D2C86DF3B004FE4A1 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F2C2C86DF3B004FE4A1 /* User.swift */; };
|
||||
529A5F2F2C86DF51004FE4A1 /* ReviewToPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F2E2C86DF51004FE4A1 /* ReviewToPost.swift */; };
|
||||
529A5F312C86DF61004FE4A1 /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F302C86DF61004FE4A1 /* Review.swift */; };
|
||||
529A5F312C86DF61004FE4A1 /* Review Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F302C86DF61004FE4A1 /* Review Models.swift */; };
|
||||
529A5F332C86DF6F004FE4A1 /* PlaceFull.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F322C86DF6F004FE4A1 /* PlaceFull.swift */; };
|
||||
529A5F352C86DF99004FE4A1 /* PlaceLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F342C86DF99004FE4A1 /* PlaceLocation.swift */; };
|
||||
529A5F372C86E02E004FE4A1 /* AllDataDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529A5F362C86E02E004FE4A1 /* AllDataDTO.swift */; };
|
||||
|
@ -590,6 +587,8 @@
|
|||
CE64501D2C93F8350075A59B /* ReviewsPersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */; };
|
||||
CE6450202C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */; };
|
||||
CE6450242C9772310075A59B /* DownloadProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6450232C9772310075A59B /* DownloadProgress.swift */; };
|
||||
CE6450282C99572F0075A59B /* ImageStoreUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6450272C99572F0075A59B /* ImageStoreUtils.swift */; };
|
||||
CEA45BC42C9AE01000ABE6B2 /* DataSyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */; };
|
||||
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 */; };
|
||||
|
@ -1373,14 +1372,11 @@
|
|||
529A5F182C85BFF0004FE4A1 /* ToastView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||
529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceDTO.swift; sourceTree = "<group>"; };
|
||||
529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesDTO.swift; sourceTree = "<group>"; };
|
||||
529A5F212C86DE50004FE4A1 /* ReviewDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewDTO.swift; sourceTree = "<group>"; };
|
||||
529A5F232C86DE7D004FE4A1 /* ReviewIdsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewIdsDTO.swift; sourceTree = "<group>"; };
|
||||
529A5F252C86DE9D004FE4A1 /* ReviewsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsDTO.swift; sourceTree = "<group>"; };
|
||||
529A5F212C86DE50004FE4A1 /* Reviews DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Reviews DTOs.swift"; sourceTree = "<group>"; };
|
||||
529A5F272C86DEC5004FE4A1 /* UserDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = "<group>"; };
|
||||
529A5F2A2C86DF2D004FE4A1 /* PlaceShort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceShort.swift; sourceTree = "<group>"; };
|
||||
529A5F2C2C86DF3B004FE4A1 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
|
||||
529A5F2E2C86DF51004FE4A1 /* ReviewToPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewToPost.swift; sourceTree = "<group>"; };
|
||||
529A5F302C86DF61004FE4A1 /* Review.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = "<group>"; };
|
||||
529A5F302C86DF61004FE4A1 /* Review Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review Models.swift"; sourceTree = "<group>"; };
|
||||
529A5F322C86DF6F004FE4A1 /* PlaceFull.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceFull.swift; sourceTree = "<group>"; };
|
||||
529A5F342C86DF99004FE4A1 /* PlaceLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceLocation.swift; sourceTree = "<group>"; };
|
||||
529A5F362C86E02E004FE4A1 /* AllDataDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDataDTO.swift; sourceTree = "<group>"; };
|
||||
|
@ -1643,6 +1639,8 @@
|
|||
CE64501C2C93F8350075A59B /* ReviewsPersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceController.swift; sourceTree = "<group>"; };
|
||||
CE64501F2C9402EC0075A59B /* ReviewsPersistenceControllerTesterBro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsPersistenceControllerTesterBro.swift; sourceTree = "<group>"; };
|
||||
CE6450232C9772310075A59B /* DownloadProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgress.swift; sourceTree = "<group>"; };
|
||||
CE6450272C99572F0075A59B /* ImageStoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStoreUtils.swift; sourceTree = "<group>"; };
|
||||
CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSyncer.swift; 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>"; };
|
||||
|
@ -3173,9 +3171,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
529A5F1D2C86DDE5004FE4A1 /* PlaceDTO.swift */,
|
||||
529A5F212C86DE50004FE4A1 /* ReviewDTO.swift */,
|
||||
529A5F232C86DE7D004FE4A1 /* ReviewIdsDTO.swift */,
|
||||
529A5F252C86DE9D004FE4A1 /* ReviewsDTO.swift */,
|
||||
529A5F212C86DE50004FE4A1 /* Reviews DTOs.swift */,
|
||||
529A5F272C86DEC5004FE4A1 /* UserDTO.swift */,
|
||||
529A5F1F2C86DE14004FE4A1 /* CoordinatesDTO.swift */,
|
||||
);
|
||||
|
@ -3264,6 +3260,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
527D5E7A2C60E05D00736A85 /* LanguageUtils.swift */,
|
||||
CE6450272C99572F0075A59B /* ImageStoreUtils.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3302,8 +3299,7 @@
|
|||
children = (
|
||||
529A5F2A2C86DF2D004FE4A1 /* PlaceShort.swift */,
|
||||
529A5F2C2C86DF3B004FE4A1 /* User.swift */,
|
||||
529A5F2E2C86DF51004FE4A1 /* ReviewToPost.swift */,
|
||||
529A5F302C86DF61004FE4A1 /* Review.swift */,
|
||||
529A5F302C86DF61004FE4A1 /* Review Models.swift */,
|
||||
529A5F322C86DF6F004FE4A1 /* PlaceFull.swift */,
|
||||
CED0E0442C918ED4008C61CA /* Hash.swift */,
|
||||
);
|
||||
|
@ -3405,6 +3401,7 @@
|
|||
52E2D39B2C58E72900A8843A /* Screens */,
|
||||
524634CC2C57232400FDCABA /* TourismMain.storyboard */,
|
||||
52522F3A2C6DDA750015709C /* ThemeViewModel.swift */,
|
||||
CEA45BC32C9AE01000ABE6B2 /* DataSyncer.swift */,
|
||||
);
|
||||
path = Home;
|
||||
sourceTree = "<group>";
|
||||
|
@ -5040,6 +5037,7 @@
|
|||
471A7BB8247FE3C300A0D4C1 /* URL+Query.swift in Sources */,
|
||||
47F86D0120C93D8D00FEE291 /* TabViewController.swift in Sources */,
|
||||
52E95F022C6B32E500A3FE2E /* ErrorResponse.swift in Sources */,
|
||||
CE6450282C99572F0075A59B /* ImageStoreUtils.swift in Sources */,
|
||||
99536113235DB86C008B218F /* InsetsLabel.swift in Sources */,
|
||||
52ECA8182C8A255900F213B3 /* PlaceTabsBar.swift in Sources */,
|
||||
6741A9A51BF340DE002C974C /* MWMShareActivityItem.mm in Sources */,
|
||||
|
@ -5208,7 +5206,6 @@
|
|||
34AB66051FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.mm in Sources */,
|
||||
993DF12A23F6BDB100AC231A /* Style.swift in Sources */,
|
||||
34ABA6171C2D185C00FE1BEC /* MWMAuthorizationOSMLoginViewController.mm in Sources */,
|
||||
529A5F242C86DE7D004FE4A1 /* ReviewIdsDTO.swift in Sources */,
|
||||
ED9966802B94FBC20083CE55 /* ColorPicker.swift in Sources */,
|
||||
993DF10423F6BDB100AC231A /* UIView+styleName.swift in Sources */,
|
||||
998927302449DE1500260CE2 /* TabBarArea.swift in Sources */,
|
||||
|
@ -5257,7 +5254,7 @@
|
|||
34C9BD031C6DB693000DC38D /* MWMTableViewController.m in Sources */,
|
||||
52E95F0D2C6C797B00A3FE2E /* ProfileViewController.swift in Sources */,
|
||||
F6E2FD8C1E097BA00083EBEC /* MWMNoMapsView.m in Sources */,
|
||||
529A5F312C86DF61004FE4A1 /* Review.swift in Sources */,
|
||||
529A5F312C86DF61004FE4A1 /* Review Models.swift in Sources */,
|
||||
34D3B0361E389D05004100F9 /* MWMEditorSelectTableViewCell.m in Sources */,
|
||||
990128562449A82500C72B10 /* BottomTabBarView.swift in Sources */,
|
||||
529A5F422C86E108004FE4A1 /* Category.swift in Sources */,
|
||||
|
@ -5327,7 +5324,6 @@
|
|||
CED0E0282C8C85C9008C61CA /* PostReviewView.swift in Sources */,
|
||||
529A5F5E2C86E37A004FE4A1 /* PlacesItem.swift in Sources */,
|
||||
340475591E081A4600C92850 /* WebViewController.m in Sources */,
|
||||
529A5F262C86DE9D004FE4A1 /* ReviewsDTO.swift in Sources */,
|
||||
3404F4992028A20D0090E401 /* BMCCategoryCell.swift in Sources */,
|
||||
F62607FD207B790300176C5A /* SpinnerAlert.swift in Sources */,
|
||||
3444DFD21F17620C00E73099 /* MWMMapWidgetsHelper.mm in Sources */,
|
||||
|
@ -5362,7 +5358,7 @@
|
|||
F660DEE51EAF4F59004DC056 /* MWMLocationManager+SpeedAndAltitude.swift in Sources */,
|
||||
F6E2FDF21E097BA00083EBEC /* MWMOpeningHoursAddScheduleTableViewCell.mm in Sources */,
|
||||
3304306D21D4EAFB00317CA3 /* SearchCategoryCell.swift in Sources */,
|
||||
529A5F222C86DE50004FE4A1 /* ReviewDTO.swift in Sources */,
|
||||
529A5F222C86DE50004FE4A1 /* Reviews DTOs.swift in Sources */,
|
||||
ED79A5AB2BD7AA9C00952D1F /* LoadingOverlayViewController.swift in Sources */,
|
||||
34AB66111FC5AA320078E451 /* NavigationTurnsView.swift in Sources */,
|
||||
475ED78624C7C7300063ADC7 /* ValueStepperViewRenderer.swift in Sources */,
|
||||
|
@ -5387,7 +5383,6 @@
|
|||
477219052243E79500E5B227 /* DrivingOptionsViewController.swift in Sources */,
|
||||
CDB4D4E4222E8FF600104869 /* CarPlayService.swift in Sources */,
|
||||
F6E2FF3C1E097BA00083EBEC /* MWMSearchTableView.m in Sources */,
|
||||
529A5F2F2C86DF51004FE4A1 /* ReviewToPost.swift in Sources */,
|
||||
52ED91A72C72C58A000EE25B /* CurrencyPersistenceController.swift in Sources */,
|
||||
F6E2FF661E097BA00083EBEC /* MWMTTSSettingsViewController.mm in Sources */,
|
||||
3454D7C21E07F045004AF2AD /* NSString+Categories.m in Sources */,
|
||||
|
@ -5483,6 +5478,7 @@
|
|||
F6E2FE221E097BA00083EBEC /* MWMOpeningHoursEditorViewController.mm in Sources */,
|
||||
ED79A5D72BDF8D6100952D1F /* SynchronizationStateManager.swift in Sources */,
|
||||
999FC12B23ABB4B800B0E6F9 /* FontStyleSheet.swift in Sources */,
|
||||
CEA45BC42C9AE01000ABE6B2 /* DataSyncer.swift in Sources */,
|
||||
47CA68DA2500469400671019 /* BookmarksListBuilder.swift in Sources */,
|
||||
34D3AFE21E376F7E004100F9 /* UITableView+Updates.swift in Sources */,
|
||||
3404164C1E7BF42E00E2B6D6 /* UIView+Coordinates.swift in Sources */,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G93" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24A335" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="FavoriteSyncEntity" representedClassName="FavoriteSyncEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="isFavorite" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="placeId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
|
@ -32,9 +32,8 @@
|
|||
</entity>
|
||||
<entity name="ReviewPlannedToPostEntity" representedClassName="ReviewPlannedToPostEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="comment" attributeType="String"/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="imagesJson" attributeType="String"/>
|
||||
<attribute name="placeId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rating" attributeType="String"/>
|
||||
<attribute name="rating" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -1,4 +1,4 @@
|
|||
struct UserEntity: Encodable {
|
||||
struct UserEntity: Codable {
|
||||
let userId: Int64
|
||||
let fullName: String
|
||||
let avatar: String
|
||||
|
|
|
@ -81,3 +81,38 @@ extension UserEntity {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ReviewEntity {
|
||||
func toReview() -> Review {
|
||||
|
||||
let user = DBUtils.decodeFromJsonString(self.userJson ?? "", to: UserEntity.self)?.toUser()
|
||||
let picsUrls = DBUtils.decodeFromJsonString(self.picsUrlsJson ?? "", to: [String].self)
|
||||
|
||||
return Review(
|
||||
id: self.id,
|
||||
placeId: self.placeId,
|
||||
rating: Int(self.rating),
|
||||
user: user,
|
||||
date: self.date,
|
||||
comment: self.comment,
|
||||
picsUrls: picsUrls ?? [],
|
||||
deletionPlanned: self.deletionPlanned
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ReviewPlannedToPostEntity {
|
||||
func toReviewToPostDTO() -> ReviewToPostDTO {
|
||||
var images = [String]()
|
||||
if let imagesJson = self.imagesJson {
|
||||
images = DBUtils.decodeFromJsonString(imagesJson, to: [String].self) ?? []
|
||||
}
|
||||
|
||||
return ReviewToPostDTO(
|
||||
placeId: self.placeId,
|
||||
comment: self.comment ?? "",
|
||||
rating: Int(self.rating),
|
||||
images: images.map { URL(string: $0)! }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,7 +300,7 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
}
|
||||
}
|
||||
|
||||
func addFavoriteSync(placeId: Int64, isFavorite: Bool) {
|
||||
func addFavoritingRecordForSync(placeId: Int64, isFavorite: Bool) {
|
||||
let context = container.viewContext
|
||||
let favoriteSyncEntity = FavoriteSyncEntity(context: context)
|
||||
favoriteSyncEntity.placeId = placeId
|
||||
|
@ -313,23 +313,28 @@ class PlacesPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
}
|
||||
}
|
||||
|
||||
func removeFavoriteSync(placeIds: [Int64]) {
|
||||
func removeFavoritingRecordsForSync(placeIds: [Int64]) {
|
||||
let context = container.viewContext
|
||||
let fetchRequest: NSFetchRequest<FavoriteSyncEntity> = FavoriteSyncEntity.fetchRequest()
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = FavoriteSyncEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "placeId IN %@", placeIds)
|
||||
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
deleteRequest.resultType = .resultTypeObjectIDs
|
||||
|
||||
do {
|
||||
let favoriteSyncs = try context.fetch(fetchRequest)
|
||||
for favoriteSync in favoriteSyncs {
|
||||
context.delete(favoriteSync)
|
||||
}
|
||||
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
|
||||
let changes: [AnyHashable: Any] = [
|
||||
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
|
||||
]
|
||||
|
||||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
|
||||
try context.save()
|
||||
} catch {
|
||||
print("Failed to remove favorite syncs: \(error)")
|
||||
print("Failed to remove favoriting records for sync: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func getFavoriteSyncData() -> [FavoriteSyncEntity] {
|
||||
func getFavoritingRecordsForSync() -> [FavoriteSyncEntity] {
|
||||
let context = container.viewContext
|
||||
let fetchRequest: NSFetchRequest<FavoriteSyncEntity> = FavoriteSyncEntity.fetchRequest()
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
|
||||
do {
|
||||
let results = try context.fetch(fetchRequest)
|
||||
let reviewEntity: ReviewEntity
|
||||
if let existingReview = results.first {
|
||||
// Update existing review
|
||||
updateReviewEntity(existingReview, with: review)
|
||||
|
@ -55,7 +54,6 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
for review in reviews {
|
||||
fetchRequest.predicate = NSPredicate(format: "id == %lld", review.id)
|
||||
let results = try context.fetch(fetchRequest)
|
||||
let reviewEntity: ReviewEntity
|
||||
if let existingReview = results.first {
|
||||
// Update existing review
|
||||
updateReviewEntity(existingReview, with: review)
|
||||
|
@ -75,7 +73,7 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
private func updateReviewEntity(_ entity: ReviewEntity, with review: Review) {
|
||||
entity.placeId = review.placeId
|
||||
entity.rating = Int16(review.rating)
|
||||
entity.userJson = DBUtils.encodeToJsonString(review.user.toUserEntity())
|
||||
entity.userJson = DBUtils.encodeToJsonString(review.user?.toUserEntity())
|
||||
entity.date = review.date
|
||||
entity.comment = review.comment
|
||||
entity.picsUrlsJson = DBUtils.encodeToJsonString(review.picsUrls)
|
||||
|
@ -120,8 +118,6 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
let context = container.viewContext
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewEntity.fetchRequest()
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
// Configure the request to return the IDs of the deleted objects
|
||||
deleteRequest.resultType = .resultTypeObjectIDs
|
||||
|
||||
do {
|
||||
|
@ -140,14 +136,18 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
|
||||
func deleteAllPlaceReviews(placeId: Int64) {
|
||||
let context = container.viewContext
|
||||
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
deleteRequest.resultType = .resultTypeObjectIDs
|
||||
|
||||
do {
|
||||
let reviews = try context.fetch(fetchRequest)
|
||||
for review in reviews {
|
||||
context.delete(review)
|
||||
}
|
||||
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
|
||||
let changes: [AnyHashable: Any] = [
|
||||
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
|
||||
]
|
||||
|
||||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
|
||||
try context.save()
|
||||
} catch {
|
||||
print(error)
|
||||
|
@ -158,7 +158,7 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
func observeReviewsForPlace(placeId: Int64) {
|
||||
let fetchRequest: NSFetchRequest<ReviewEntity> = ReviewEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
|
||||
|
||||
reviewsForPlaceFetchedResultsController = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
|
@ -172,7 +172,9 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
do {
|
||||
try reviewsForPlaceFetchedResultsController?.performFetch()
|
||||
if let results = reviewsForPlaceFetchedResultsController?.fetchedObjects {
|
||||
reviewsForPlaceSubject.send(results)
|
||||
reviewsForPlaceSubject.send(results.map({ reviews in
|
||||
reviews.toReview()
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
|
@ -213,10 +215,14 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
|
||||
// // MARK: - Planned Review Operations
|
||||
|
||||
func insertReviewPlannedToPost(_ review: ReviewPlannedToPostEntity) {
|
||||
func insertReviewPlannedToPost(_ review: ReviewToPost) {
|
||||
let context = container.viewContext
|
||||
let newReview = ReviewPlannedToPostEntity(context: context)
|
||||
// Set properties of newReview based on the input review
|
||||
newReview.placeId = review.placeId
|
||||
newReview.comment = review.comment
|
||||
newReview.rating = Int32(review.rating)
|
||||
let imagesJson = DBUtils.encodeToJsonString(review.images)
|
||||
newReview.imagesJson = imagesJson
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
|
@ -228,14 +234,19 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
|
||||
func deleteReviewPlannedToPost(placeId: Int64) {
|
||||
let context = container.viewContext
|
||||
let fetchRequest: NSFetchRequest<ReviewPlannedToPostEntity> = ReviewPlannedToPostEntity.fetchRequest()
|
||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ReviewPlannedToPostEntity.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
|
||||
|
||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
deleteRequest.resultType = .resultTypeObjectIDs
|
||||
|
||||
do {
|
||||
let reviews = try context.fetch(fetchRequest)
|
||||
for review in reviews {
|
||||
context.delete(review)
|
||||
}
|
||||
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
|
||||
let changes: [AnyHashable: Any] = [
|
||||
NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []
|
||||
]
|
||||
|
||||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
|
||||
try context.save()
|
||||
} catch {
|
||||
print(error)
|
||||
|
@ -256,29 +267,32 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
}
|
||||
}
|
||||
|
||||
func observeReviewsPlannedToPost() {
|
||||
let fetchRequest: NSFetchRequest<ReviewPlannedToPostEntity> = ReviewPlannedToPostEntity.fetchRequest()
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "placeId", ascending: true)]
|
||||
|
||||
reviewsPlannedToPostFetchedResultsController = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
managedObjectContext: container.viewContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
||||
reviewsPlannedToPostFetchedResultsController?.delegate = self
|
||||
|
||||
do {
|
||||
try reviewsPlannedToPostFetchedResultsController?.performFetch()
|
||||
if let results = reviewsPlannedToPostFetchedResultsController?.fetchedObjects {
|
||||
reviewsPlannedToPostSubject.send(results)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
reviewsPlannedToPostSubject.send(completion: .failure(ResourceError.cacheError))
|
||||
}
|
||||
}
|
||||
// we only use it to limit the user from reviewing when he already made review
|
||||
// for a place
|
||||
// func observeReviewsPlannedToPost(placeId: Int64) {
|
||||
// let fetchRequest: NSFetchRequest<ReviewPlannedToPostEntity> = ReviewPlannedToPostEntity.fetchRequest()
|
||||
// fetchRequest.sortDescriptors = [NSSortDescriptor(key: "placeId", ascending: true)]
|
||||
// fetchRequest.predicate = NSPredicate(format: "placeId == %lld", placeId)
|
||||
//
|
||||
// reviewsPlannedToPostFetchedResultsController = NSFetchedResultsController(
|
||||
// fetchRequest: fetchRequest,
|
||||
// managedObjectContext: container.viewContext,
|
||||
// sectionNameKeyPath: nil,
|
||||
// cacheName: nil
|
||||
// )
|
||||
//
|
||||
// reviewsPlannedToPostFetchedResultsController?.delegate = self
|
||||
//
|
||||
// do {
|
||||
// try reviewsPlannedToPostFetchedResultsController?.performFetch()
|
||||
// if let results = reviewsPlannedToPostFetchedResultsController?.fetchedObjects {
|
||||
// reviewsPlannedToPostSubject.send(results)
|
||||
// }
|
||||
// } catch {
|
||||
// print(error)
|
||||
// reviewsPlannedToPostSubject.send(completion: .failure(ResourceError.cacheError))
|
||||
// }
|
||||
// }
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
|
@ -288,7 +302,9 @@ class ReviewsPersistenceController: NSObject, NSFetchedResultsControllerDelegate
|
|||
|
||||
switch controller {
|
||||
case reviewsForPlaceFetchedResultsController:
|
||||
reviewsForPlaceSubject.send(fetchedObjects as! [Review])
|
||||
let reviewsEntities = fetchedObjects as! [ReviewEntity]
|
||||
let reviews = reviewsEntities.map { reviewEntity in reviewEntity.toReview() }
|
||||
reviewsForPlaceSubject.send(reviews)
|
||||
case reviewsPlannedToPostFetchedResultsController:
|
||||
reviewsPlannedToPostSubject.send(fetchedObjects as! [ReviewPlannedToPostEntity])
|
||||
default:
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct ReviewIdsDTO: Codable {
|
||||
let feedbacks: [Int64]
|
||||
}
|
|
@ -21,3 +21,21 @@ struct ReviewDTO: Codable {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ReviewIdsDTO: Codable {
|
||||
let feedbacks: [Int64]
|
||||
}
|
||||
|
||||
|
||||
struct ReviewsDTO: Codable {
|
||||
let data: [ReviewDTO]
|
||||
}
|
||||
|
||||
|
||||
struct ReviewToPostDTO: Codable {
|
||||
let placeId: Int64
|
||||
let comment: String
|
||||
let rating: Int
|
||||
let images: [URL]
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct ReviewsDTO: Codable {
|
||||
let data: [ReviewDTO]
|
||||
}
|
|
@ -2,20 +2,91 @@ import Combine
|
|||
|
||||
protocol ReviewsService {
|
||||
func getReviewsByPlaceId(id: Int64) async throws -> ReviewsDTO
|
||||
func postReview(review: ReviewToPost) async throws -> ReviewDTO
|
||||
func deleteReview(feedbacks: ReviewIdsDTO) async throws -> SimpleResponse
|
||||
func postReview(review: ReviewToPostDTO) async throws -> SimpleResponse
|
||||
func deleteReview(reviews: ReviewIdsDTO) async throws -> SimpleResponse
|
||||
}
|
||||
|
||||
class ReviewsServiceImpl : ReviewsService {
|
||||
let userPreferences: UserPreferences
|
||||
|
||||
init(userPreferences: UserPreferences) {
|
||||
self.userPreferences = userPreferences
|
||||
}
|
||||
|
||||
func getReviewsByPlaceId(id: Int64) async throws -> ReviewsDTO {
|
||||
return try await AppNetworkHelper.get(path: APIEndpoints.getReviewsByPlaceIdUrl(id: id))
|
||||
}
|
||||
|
||||
func postReview(review: ReviewToPost) async throws -> ReviewDTO {
|
||||
return try await AppNetworkHelper.post(path: APIEndpoints.postReviewUrl, body: review)
|
||||
func postReview(review: ReviewToPostDTO) async throws -> SimpleResponse {
|
||||
guard let url = URL(string: APIEndpoints.postReviewUrl) else {
|
||||
throw ResourceError.other(message: "Invalid URL")
|
||||
}
|
||||
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
if let token = userPreferences.getToken() {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let parameters: [[String: Any]] = [
|
||||
["key": "message", "value": review.comment, "type": "text"],
|
||||
["key": "mark_id", "value": "\(review.placeId)", "type": "text"],
|
||||
["key": "points", "value": "\(review.rating)", "type": "text"]
|
||||
] + review.images.map { ["key": "images[]", "src": $0.path, "type": "file"] }
|
||||
|
||||
let body = try createBody(with: parameters, boundary: boundary)
|
||||
request.httpBody = body
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if !(200...299).contains(httpResponse.statusCode) {
|
||||
throw ResourceError.other(message: "Response not successful")
|
||||
}
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
return try decoder.decode(SimpleResponse.self, from: data)
|
||||
}
|
||||
|
||||
func deleteReview(feedbacks: ReviewIdsDTO) async throws -> SimpleResponse {
|
||||
return try await AppNetworkHelper.delete(path: APIEndpoints.deleteReviewsUrl, body: feedbacks)
|
||||
private func createBody(with parameters: [[String: Any]], boundary: String) throws -> Data {
|
||||
var body = Data()
|
||||
|
||||
for param in parameters {
|
||||
if param["disabled"] != nil { continue }
|
||||
|
||||
let paramName = param["key"] as! String
|
||||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
body.append("Content-Disposition:form-data; name=\"\(paramName)\"".data(using: .utf8)!)
|
||||
|
||||
if let contentType = param["contentType"] as? String {
|
||||
body.append("\r\nContent-Type: \(contentType)".data(using: .utf8)!)
|
||||
}
|
||||
|
||||
let paramType = param["type"] as! String
|
||||
if paramType == "text" {
|
||||
let paramValue = param["value"] as! String
|
||||
body.append("\r\n\r\n\(paramValue)\r\n".data(using: .utf8)!)
|
||||
} else {
|
||||
let paramSrc = param["src"] as! String
|
||||
let fileURL = URL(fileURLWithPath: paramSrc)
|
||||
let fileName = fileURL.lastPathComponent
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
body.append("; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
|
||||
body.append("Content-Type: \"content-type header\"\r\n\r\n".data(using: .utf8)!)
|
||||
body.append(data)
|
||||
body.append("\r\n".data(using: .utf8)!)
|
||||
}
|
||||
}
|
||||
|
||||
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
|
||||
return body
|
||||
}
|
||||
|
||||
func deleteReview(reviews: ReviewIdsDTO) async throws -> SimpleResponse {
|
||||
return try await AppNetworkHelper.delete(path: APIEndpoints.deleteReviewsUrl, body: reviews)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,7 +163,7 @@ class AppNetworkHelper {
|
|||
headers: headers,
|
||||
decoder: decoder
|
||||
)
|
||||
} catch {
|
||||
} catch let error as NSError {
|
||||
print(error)
|
||||
throw ResourceError.other(message: "Encoding error")
|
||||
}
|
||||
|
|
|
@ -181,7 +181,7 @@ class PlacesRepositoryImpl: PlacesRepository {
|
|||
func setFavorite(placeId: Int64, isFavorite: Bool) {
|
||||
placesPersistenceController.setFavorite(placeId: placeId, isFavorite: isFavorite)
|
||||
|
||||
placesPersistenceController.addFavoriteSync(
|
||||
placesPersistenceController.addFavoritingRecordForSync(
|
||||
placeId: placeId,
|
||||
isFavorite: isFavorite
|
||||
)
|
||||
|
@ -196,14 +196,42 @@ class PlacesRepositoryImpl: PlacesRepository {
|
|||
try await placesService.removeFromFavorites(ids: favoritesIdsDto)
|
||||
}
|
||||
|
||||
placesPersistenceController.removeFavoriteSync(placeIds: [placeId])
|
||||
placesPersistenceController.removeFavoritingRecordsForSync(placeIds: [placeId])
|
||||
} catch {
|
||||
placesPersistenceController.addFavoriteSync(placeId: placeId, isFavorite: isFavorite)
|
||||
print("Failed to setFavorite")
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncFavorites() {
|
||||
// TODO: cmon
|
||||
let syncData = placesPersistenceController.getFavoritingRecordsForSync()
|
||||
|
||||
let favoritesToAdd = syncData.filter { $0.isFavorite }.map { $0.placeId }
|
||||
let favoritesToRemove = syncData.filter { !$0.isFavorite }.map { $0.placeId }
|
||||
|
||||
if !favoritesToAdd.isEmpty {
|
||||
Task {
|
||||
do {
|
||||
let response =
|
||||
try await placesService.addFavorites(ids: FavoritesIdsDTO(marks: favoritesToAdd))
|
||||
placesPersistenceController.removeFavoritingRecordsForSync(placeIds: favoritesToAdd)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !favoritesToRemove.isEmpty {
|
||||
Task {
|
||||
do {
|
||||
let response =
|
||||
try await placesService.removeFromFavorites(ids: FavoritesIdsDTO(marks: favoritesToRemove))
|
||||
placesPersistenceController.removeFavoritingRecordsForSync(placeIds: favoritesToRemove)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ class ProfileRepositoryImpl: ProfileRepository {
|
|||
} receiveValue: { personalData in
|
||||
self.personalDataPassThroughSubject.send(personalData)
|
||||
}
|
||||
.store(in: &cancellables) // Store the cancellable
|
||||
.store(in: &cancellables)
|
||||
|
||||
persistenceController.observePersonalData()
|
||||
|
||||
|
@ -40,6 +40,9 @@ class ProfileRepositoryImpl: ProfileRepository {
|
|||
}
|
||||
|
||||
let newPersonalData = remotePersonalData.toPersonalData()
|
||||
|
||||
userPreferences.setUserId(value: String(newPersonalData.id))
|
||||
|
||||
return self.persistenceController.updatePersonalData(personalData: newPersonalData)
|
||||
.map { newPersonalData }
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -52,7 +55,7 @@ class ProfileRepositoryImpl: ProfileRepository {
|
|||
} receiveValue: { personalData in
|
||||
// Yes, nothing, we observe anyway
|
||||
}
|
||||
.store(in: &cancellables) // Store the cancellable
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func updateProfile(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Combine
|
||||
|
||||
class ReviewsRepositoryImpl : ReviewsRepository {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var reviewsPersistenceController: ReviewsPersistenceController
|
||||
var reviewsService: ReviewsService
|
||||
|
||||
|
@ -9,16 +11,16 @@ class ReviewsRepositoryImpl : ReviewsRepository {
|
|||
|
||||
init(
|
||||
reviewsPersistenceController: ReviewsPersistenceController,
|
||||
reviewsService: ReviewsService,
|
||||
reviewsResource: PassthroughSubject<[Review], ResourceError>
|
||||
reviewsService: ReviewsService
|
||||
) {
|
||||
self.reviewsPersistenceController = reviewsPersistenceController
|
||||
self.reviewsService = reviewsService
|
||||
|
||||
self.reviewsResource = reviewsPersistenceController.reviewsForPlaceSubject
|
||||
reviewsPersistenceController.reviewsPlannedToPostSubject.sink { completion in } receiveValue: { reviews in
|
||||
self.isThereReviewPlannedToPublishResource.send(reviews.isEmpty)
|
||||
reviewsPersistenceController.reviewsPlannedToPostSubject.sink { _ in } receiveValue: {
|
||||
reviews in self.isThereReviewPlannedToPublishResource.send(reviews.isEmpty)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func observeReviewsForPlace(id: Int64) {
|
||||
|
@ -28,27 +30,99 @@ class ReviewsRepositoryImpl : ReviewsRepository {
|
|||
let reviewsDTO = try await reviewsService.getReviewsByPlaceId(id: id)
|
||||
let reviews = reviewsDTO.data.map { reviewDto in reviewDto.toReview() }
|
||||
|
||||
reviewsPersistenceController.deleteAllPlaceReviews(placeId: id)
|
||||
reviewsPersistenceController.putReviews(reviews)
|
||||
self.reviewsPersistenceController.deleteAllPlaceReviews(placeId: id)
|
||||
self.reviewsPersistenceController.putReviews(reviews)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func isThereReviewPlannedToPublish(for placeId: Int64) {
|
||||
reviewsPersistenceController.getReviewsPlannedToPost()
|
||||
func checkIfThereIsReviewPlannedToPublish(for placeId: Int64) {
|
||||
reviewsPersistenceController.observeReviewsForPlace(placeId: placeId)
|
||||
}
|
||||
|
||||
func postReview(review: ReviewToPost) -> AnyPublisher<SimpleResponse, ResourceError> {
|
||||
// TODO: cmon
|
||||
return PassthroughSubject<SimpleResponse, ResourceError>().eraseToAnyPublisher()
|
||||
return Future<SimpleResponse, ResourceError> { promise in
|
||||
Task {
|
||||
if Reachability.isConnectedToNetwork() {
|
||||
do {
|
||||
let response = try await self.reviewsService.postReview(review: review.toReviewToPostDTO())
|
||||
self.updateReviewsForDb(id: review.placeId)
|
||||
promise(.success(SimpleResponse(message: response.message)))
|
||||
} catch let error as ResourceError {
|
||||
print(error)
|
||||
promise(.failure(error))
|
||||
}
|
||||
} else {
|
||||
// images files already were saved in viewmodel, so no need to save them here
|
||||
self.reviewsPersistenceController.insertReviewPlannedToPost(review)
|
||||
promise(.failure(ResourceError.errorToUser(message: L("review_will_be_published_when_online"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func deleteReview(id: Int64) -> AnyPublisher<SimpleResponse, ResourceError> {
|
||||
// TODO: cmon
|
||||
return PassthroughSubject<SimpleResponse, ResourceError>().eraseToAnyPublisher()
|
||||
return Future<SimpleResponse, ResourceError> { promise in
|
||||
Task {
|
||||
do {
|
||||
let response = try await self.reviewsService.deleteReview(reviews: ReviewIdsDTO(feedbacks: [id]))
|
||||
self.reviewsPersistenceController.deleteReview(id: id)
|
||||
promise(.success(response))
|
||||
} catch let error as ResourceError {
|
||||
self.reviewsPersistenceController.markReviewForDeletion(id: id, deletionPlanned: true)
|
||||
promise(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func syncReviews() {
|
||||
// TODO: cmon
|
||||
Task {
|
||||
try await deleteReviewsPlannedForDeletion()
|
||||
try await publishReviewsPlannedToPost()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteReviewsPlannedForDeletion() async throws {
|
||||
let reviews = reviewsPersistenceController.getReviewsPlannedForDeletion()
|
||||
|
||||
if !reviews.isEmpty {
|
||||
let reviewsIds = reviews.map(\.id)
|
||||
let response = try await reviewsService.deleteReview(reviews: ReviewIdsDTO(feedbacks: reviewsIds))
|
||||
reviewsPersistenceController.deleteReviews(ids: reviewsIds)
|
||||
}
|
||||
}
|
||||
|
||||
private func publishReviewsPlannedToPost() async throws {
|
||||
let reviewsPlannedToPostEntities = reviewsPersistenceController.getReviewsPlannedToPost()
|
||||
if !reviewsPlannedToPostEntities.isEmpty {
|
||||
let reviewsDTO = reviewsPlannedToPostEntities.map {$0.toReviewToPostDTO()}
|
||||
reviewsDTO.forEach { reviewDTO in
|
||||
Task {
|
||||
do {
|
||||
let response = try await reviewsService.postReview(review: reviewDTO)
|
||||
updateReviewsForDb(id: reviewDTO.placeId)
|
||||
reviewsPersistenceController.deleteReviewPlannedToPost(placeId: reviewDTO.placeId)
|
||||
try reviewDTO.images.forEach { URL in
|
||||
try FileManager.default.removeItem(at: URL)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateReviewsForDb(id: Int64) {
|
||||
Task {
|
||||
let reviewsDTO = try await reviewsService.getReviewsByPlaceId(id: id)
|
||||
if !reviewsDTO.data.isEmpty {
|
||||
reviewsPersistenceController.deleteAllReviews()
|
||||
let reviews = reviewsDTO.data.map{ $0.toReview() }
|
||||
reviewsPersistenceController.putReviews(reviews)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
enum ResourceError: Error, Equatable {
|
||||
case noConnection
|
||||
case serverError(message: String)
|
||||
case cacheError
|
||||
case unauthed
|
||||
|
@ -9,6 +10,8 @@ enum ResourceError: Error, Equatable {
|
|||
|
||||
var errorDescription: String {
|
||||
switch self {
|
||||
case .noConnection:
|
||||
return L("no_connection")
|
||||
case .serverError:
|
||||
return L("server_error")
|
||||
case .cacheError:
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import Foundation
|
||||
|
||||
struct Review: Codable, Hashable {
|
||||
let id: Int64
|
||||
let placeId: Int64
|
||||
let rating: Int
|
||||
let user: User?
|
||||
let date: String?
|
||||
let comment: String?
|
||||
let picsUrls: [String]
|
||||
var deletionPlanned: Bool = false
|
||||
}
|
||||
|
||||
|
||||
struct ReviewToPost: Codable {
|
||||
let placeId: Int64
|
||||
let comment: String
|
||||
let rating: Int
|
||||
let images: [URL]
|
||||
|
||||
func toReviewToPostDTO() -> ReviewToPostDTO {
|
||||
return ReviewToPostDTO(placeId: placeId, comment: comment, rating: rating, images: images)
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct Review: Codable, Hashable {
|
||||
let id: Int64
|
||||
let placeId: Int64
|
||||
let rating: Int
|
||||
let user: User
|
||||
let date: String?
|
||||
let comment: String?
|
||||
let picsUrls: [String]
|
||||
var deletionPlanned: Bool = false
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct ReviewToPost: Codable {
|
||||
let placeId: Int64
|
||||
let comment: String
|
||||
let rating: Int
|
||||
let images: [URL] // Using URL to represent file paths
|
||||
}
|
|
@ -5,7 +5,7 @@ protocol ReviewsRepository {
|
|||
func observeReviewsForPlace(id: Int64)
|
||||
|
||||
var isThereReviewPlannedToPublishResource: PassthroughSubject<Bool, Never> { get }
|
||||
func isThereReviewPlannedToPublish(for placeId: Int64)
|
||||
func checkIfThereIsReviewPlannedToPublish(for placeId: Int64)
|
||||
|
||||
func postReview(review: ReviewToPost) -> AnyPublisher<SimpleResponse, ResourceError>
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class WelcomeViewController: UIViewController {
|
|||
let label = UILabel()
|
||||
label.text = "©"
|
||||
label.textColor = .white
|
||||
UIKitFont.applyStyle(to: label, style: UIKitFont.h1)
|
||||
UIKitFont.applyStyle(to: label, style: UIKitFont.h2)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
|
|
@ -4,16 +4,33 @@ import SDWebImageSwiftUI
|
|||
struct LoadImageView: View {
|
||||
let url: String?
|
||||
|
||||
@State var isError = false
|
||||
|
||||
var body: some View {
|
||||
if let urlString = url {
|
||||
WebImage(url: URL(string: urlString))
|
||||
.resizable()
|
||||
.indicator(.activity)
|
||||
.scaledToFill()
|
||||
.transition(.fade(duration: 0.2))
|
||||
ZStack(alignment: .center) {
|
||||
WebImage(url: URL(string: urlString))
|
||||
.onSuccess(perform: { Image, data, cache in
|
||||
self.isError = false
|
||||
})
|
||||
.onFailure(perform: { isError in
|
||||
self.isError = true
|
||||
})
|
||||
.resizable()
|
||||
.indicator(.activity)
|
||||
.scaledToFill()
|
||||
.transition(.fade(duration: 0.2))
|
||||
if(isError) {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.font(.system(size: 30))
|
||||
.background(SwiftUI.Color.clear)
|
||||
.foregroundColor(Color.hint)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(L("no_image"))
|
||||
.foregroundColor(Color.surface)
|
||||
.foregroundColor(Color.hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
80
iphone/Maps/Tourism/Presentation/Home/DataSyncer.swift
Normal file
80
iphone/Maps/Tourism/Presentation/Home/DataSyncer.swift
Normal file
|
@ -0,0 +1,80 @@
|
|||
import Network
|
||||
import SystemConfiguration
|
||||
|
||||
class DataSyncer {
|
||||
private let reviewsRepository: ReviewsRepository
|
||||
private let placesRepository: PlacesRepository
|
||||
|
||||
init(reviewsRepository: ReviewsRepository, placesRepository: PlacesRepository) {
|
||||
self.reviewsRepository = reviewsRepository
|
||||
self.placesRepository = placesRepository
|
||||
}
|
||||
|
||||
private let monitor = NWPathMonitor()
|
||||
private let queue = DispatchQueue.global(qos: .background)
|
||||
|
||||
var isConnected: Bool = false
|
||||
var isExpensive: Bool = false
|
||||
|
||||
func startMonitoring() {
|
||||
monitor.pathUpdateHandler = { path in
|
||||
self.isConnected = path.status == .satisfied
|
||||
self.isExpensive = path.isExpensive
|
||||
|
||||
if path.status == .satisfied {
|
||||
print("Connected to the internet.")
|
||||
self.reviewsRepository.syncReviews()
|
||||
self.placesRepository.syncFavorites()
|
||||
} else {
|
||||
print("No internet connection.")
|
||||
}
|
||||
|
||||
if path.isExpensive {
|
||||
print("Connection is on an expensive network, like cellular.")
|
||||
}
|
||||
}
|
||||
|
||||
monitor.start(queue: queue)
|
||||
}
|
||||
|
||||
func stopMonitoring() {
|
||||
monitor.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class Reachability {
|
||||
|
||||
class func isConnectedToNetwork() -> Bool {
|
||||
|
||||
var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
|
||||
zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
|
||||
zeroAddress.sin_family = sa_family_t(AF_INET)
|
||||
|
||||
let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
|
||||
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
|
||||
SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
|
||||
}
|
||||
}
|
||||
|
||||
var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
|
||||
if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
|
||||
return false
|
||||
}
|
||||
|
||||
/* Only Working for WIFI
|
||||
let isReachable = flags == .reachable
|
||||
let needsConnection = flags == .connectionRequired
|
||||
|
||||
return isReachable && !needsConnection
|
||||
*/
|
||||
|
||||
// Working for Cellular and WIFI
|
||||
let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
|
||||
let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
|
||||
let ret = (isReachable && !needsConnection)
|
||||
|
||||
return ret
|
||||
|
||||
}
|
||||
}
|
|
@ -45,56 +45,57 @@ struct Place: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack() {
|
||||
LoadImageView(url: place.cover)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack() {
|
||||
VStack(alignment: .leading) {
|
||||
Text(place.name)
|
||||
.font(.semiBold(size: 15))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
VerticalSpace(height: 4)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text(String(format: "%.1f", place.rating ?? 0.0))
|
||||
.font(.semiBold(size: 15))
|
||||
.foregroundColor(.white)
|
||||
Image(systemName: "star.fill")
|
||||
.resizable()
|
||||
.foregroundColor(Color.starYellow)
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: width)
|
||||
.background(SwiftUI.Color.black.opacity(0.5))
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
LoadImageView(url: place.cover)
|
||||
|
||||
VStack {
|
||||
Button(action: {
|
||||
onFavoriteChanged(!isFavorite)
|
||||
}) {
|
||||
Image(systemName: isFavorite ? "heart.fill" : "heart")
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
.background(SwiftUI.Color.white.opacity(0.2))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
Spacer()
|
||||
Spacer()
|
||||
HStack() {
|
||||
VStack(alignment: .leading) {
|
||||
Text(place.name)
|
||||
.font(.semiBold(size: 15))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
VerticalSpace(height: 4)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text(String(format: "%.1f", place.rating ?? 0.0))
|
||||
.font(.semiBold(size: 15))
|
||||
.foregroundColor(.white)
|
||||
Image(systemName: "star.fill")
|
||||
.resizable()
|
||||
.foregroundColor(Color.starYellow)
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: width)
|
||||
.background(SwiftUI.Color.black.opacity(0.5))
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(width: width, height: height)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
Button(action: {
|
||||
onFavoriteChanged(!isFavorite)
|
||||
}) {
|
||||
Image(systemName: isFavorite ? "heart.fill" : "heart")
|
||||
.foregroundColor(.white)
|
||||
.padding(12)
|
||||
.background(SwiftUI.Color.white.opacity(0.2))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(width: width, height: height)
|
||||
}
|
||||
.frame(width: width, height: height)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.contentShape(Rectangle()) // Add this line
|
||||
.onTapGesture(perform: onPlaceClick)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,13 +44,15 @@ struct PersonalDataScreen: View {
|
|||
// pfp
|
||||
Group {
|
||||
if profileVM.isImagePickerUsed {
|
||||
Image(uiImage: profileVM.pfpToUpload).resizable()
|
||||
Image(uiImage: profileVM.pfpToUpload)
|
||||
.resizable()
|
||||
} else {
|
||||
LoadImageView(url: profileVM.pfpFromRemote?.absoluteString)
|
||||
}
|
||||
}
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 100)
|
||||
.background(Color.surface)
|
||||
.clipShape(Circle())
|
||||
|
||||
Spacer().frame(width: 32)
|
||||
|
|
|
@ -31,6 +31,10 @@ struct AllPicsScreen: View {
|
|||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, UIApplication.shared.statusBarFrame.height)
|
||||
.padding(.bottom, 48)
|
||||
.background(Color.background)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ class PlaceViewController: UIViewController {
|
|||
|
||||
struct PlaceScreen: View {
|
||||
@ObservedObject var placeVM: PlaceViewModel
|
||||
let reviewsVM: ReviewsViewModel
|
||||
let id: Int64
|
||||
let showBottomBar: () -> Void
|
||||
|
||||
|
@ -46,6 +47,21 @@ struct PlaceScreen: View {
|
|||
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
|
||||
init(placeVM: PlaceViewModel, id: Int64, showBottomBar: @escaping () -> Void) {
|
||||
self.placeVM = placeVM
|
||||
self.id = id
|
||||
self.showBottomBar = showBottomBar
|
||||
|
||||
self.reviewsVM = ReviewsViewModel(
|
||||
reviewsRepository: ReviewsRepositoryImpl(
|
||||
reviewsPersistenceController: ReviewsPersistenceController.shared,
|
||||
reviewsService: ReviewsServiceImpl(userPreferences: UserPreferences.shared)
|
||||
),
|
||||
userPreferences: UserPreferences.shared,
|
||||
id: id
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if let place = placeVM.place {
|
||||
VStack {
|
||||
|
@ -79,7 +95,11 @@ struct PlaceScreen: View {
|
|||
.tag(0)
|
||||
GalleryScreen(urls: place.pics)
|
||||
.tag(1)
|
||||
ReviewsScreen(placeId: place.id, rating: place.rating)
|
||||
ReviewsScreen(
|
||||
reviewsVM: reviewsVM,
|
||||
placeId: place.id,
|
||||
rating: place.rating
|
||||
)
|
||||
.tag(2)
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
|
|
|
@ -19,6 +19,10 @@ struct AllReviewsScreen: View {
|
|||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, UIApplication.shared.statusBarFrame.height)
|
||||
.padding(.bottom, 48)
|
||||
.background(Color.background)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,14 @@ struct PostReviewView: View {
|
|||
|
||||
@State private var showImagePicker = false
|
||||
|
||||
@State private var message = ""
|
||||
@State private var showMessage = false
|
||||
|
||||
private func showMessage(_ message: String) {
|
||||
self.message = message
|
||||
self.showMessage = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack {
|
||||
|
@ -30,21 +38,20 @@ struct PostReviewView: View {
|
|||
Spacer().frame(height: 16)
|
||||
|
||||
// Display the selected images
|
||||
FlowStack(data: postReviewVM.files, spacing: 16, alignment: .center) { file in
|
||||
FlowStack(data: postReviewVM.images, spacing: 16, alignment: .center) { file in
|
||||
ImagePreviewView(image: file) {
|
||||
postReviewVM.removeFile(file)
|
||||
}
|
||||
}
|
||||
Spacer().frame(height: 32)
|
||||
|
||||
if(postReviewVM.files.count < 10) {
|
||||
if(postReviewVM.images.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"))
|
||||
|
@ -68,14 +75,22 @@ struct PostReviewView: View {
|
|||
.onReceive(postReviewVM.uiEvents) { event in
|
||||
switch event {
|
||||
case .closeReviewBottomSheet:
|
||||
onPostReviewSuccess()
|
||||
onPostReviewSuccess()
|
||||
case .showToast(let message):
|
||||
// TODO: cmon
|
||||
print(message)
|
||||
showMessage(message)
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
Group {
|
||||
if showMessage {
|
||||
ToastView(message: message, isPresented: $showMessage)
|
||||
.padding(.bottom)
|
||||
}
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
MultiImagePicker(selectedImages: $postReviewVM.files)
|
||||
MultiImagePicker(selectedImages: $postReviewVM.images)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,32 +118,32 @@ struct ImagePreviewView: View {
|
|||
|
||||
|
||||
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)
|
||||
)
|
||||
@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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import SwiftUI
|
||||
import SDWebImageSwiftUI
|
||||
|
||||
struct ReviewView: View {
|
||||
let review: Review
|
||||
|
@ -12,8 +11,10 @@ struct ReviewView: View {
|
|||
Divider()
|
||||
|
||||
HStack {
|
||||
UserView(user: review.user)
|
||||
Spacer()
|
||||
if let user = review.user {
|
||||
UserView(user: user)
|
||||
Spacer()
|
||||
}
|
||||
if review.deletionPlanned {
|
||||
Text(L("deletionPlanned"))
|
||||
.textStyle(TextStyle.b2)
|
||||
|
@ -61,15 +62,14 @@ struct UserView: View {
|
|||
var body: some View {
|
||||
HStack {
|
||||
if let pfpUrl = user.pfpUrl {
|
||||
WebImage(url: URL(string: pfpUrl))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
LoadImageView(url: pfpUrl)
|
||||
.frame(width: 66, height: 66)
|
||||
.background(Color.surface)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
HStack() {
|
||||
Text(user.name)
|
||||
.textStyle(TextStyle.h3)
|
||||
.textStyle(TextStyle.h4)
|
||||
.foregroundColor(Color.onBackground)
|
||||
UICountryFlagView(code: user.countryCodeName)
|
||||
.scaledToFit()
|
||||
|
@ -132,10 +132,9 @@ struct ReviewPicView: View {
|
|||
let url: String
|
||||
|
||||
var body: some View {
|
||||
WebImage(url: URL(string: url))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
LoadImageView(url: url)
|
||||
.frame(width: reviewPicWidth, height: reviewPicHeight)
|
||||
.background(Color.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,56 +3,58 @@ import SwiftUI
|
|||
import Combine
|
||||
|
||||
class PostReviewViewModel: ObservableObject {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let reviewsRepository: ReviewsRepository
|
||||
|
||||
@Published var rating: Double = 5
|
||||
@Published var comment: String = ""
|
||||
@Published var files: [UIImage] = []
|
||||
@Published var images: [UIImage] = []
|
||||
@Published var isPosting: Bool = false
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
// private let reviewsRepository: ReviewsRepository
|
||||
@Published var retrievedImages: [UIImage] = []
|
||||
|
||||
let uiEvents = PassthroughSubject<UiEvent, Never>()
|
||||
|
||||
// init(reviewsRepository: ReviewsRepository) {
|
||||
// self.reviewsRepository = reviewsRepository
|
||||
// }
|
||||
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 }
|
||||
images.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)
|
||||
isPosting = true
|
||||
|
||||
let urls = saveMultipleImages(images, placeId: placeId)
|
||||
print(urls)
|
||||
let review = ReviewToPost(
|
||||
placeId: placeId,
|
||||
comment: comment,
|
||||
rating: Int(rating),
|
||||
images: urls
|
||||
)
|
||||
|
||||
reviewsRepository.postReview(review: 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.errorDescription))
|
||||
}
|
||||
} receiveValue: { response in
|
||||
print(response)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ReviewsScreen: View {
|
||||
// @ObservedObject var reviewsVM = ReviewsViewModel()
|
||||
@ObservedObject var reviewsVM: ReviewsViewModel
|
||||
|
||||
let placeId: Int64
|
||||
let rating: Double?
|
||||
|
||||
init(
|
||||
reviewsVM: ReviewsViewModel,
|
||||
placeId: Int64,
|
||||
rating: Double?
|
||||
) {
|
||||
self.reviewsVM = reviewsVM
|
||||
self.placeId = placeId
|
||||
self.rating = rating
|
||||
}
|
||||
|
||||
@State private var showReviewSheet = false
|
||||
|
||||
var body: some View {
|
||||
|
@ -35,31 +45,44 @@ struct ReviewsScreen: View {
|
|||
HStack {
|
||||
Spacer()
|
||||
|
||||
// NavigationLink(destination: AllReviewsScreen(reviewsVM: reviewsVM)) {
|
||||
// Text(L("see_all_reviews"))
|
||||
// .foregroundColor(Color.primary)
|
||||
// }
|
||||
NavigationLink(destination: AllReviewsScreen(reviewsVM: reviewsVM)) {
|
||||
Text(L("see_all_reviews"))
|
||||
.foregroundColor(Color.primary)
|
||||
}
|
||||
}
|
||||
|
||||
// user review
|
||||
ReviewView(
|
||||
review: Constants.reviewExample,
|
||||
onDeleteClick: {}
|
||||
)
|
||||
if let userReview = reviewsVM.userReview, !reviewsVM.isThereReviewPlannedToPublish {
|
||||
ReviewView(
|
||||
review: userReview,
|
||||
onDeleteClick: {
|
||||
reviewsVM.deleteReview()
|
||||
}
|
||||
)
|
||||
}
|
||||
// most recent recent review
|
||||
ReviewView(
|
||||
review: Constants.reviewExample,
|
||||
onDeleteClick: nil
|
||||
)
|
||||
if let mostRecentReview = reviewsVM.latestReview {
|
||||
ReviewView(
|
||||
review: mostRecentReview,
|
||||
onDeleteClick: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.sheet(isPresented: $showReviewSheet) {
|
||||
PostReviewView(
|
||||
postReviewVM: PostReviewViewModel(),
|
||||
placeId: placeId) {
|
||||
// TODO: cmon
|
||||
postReviewVM: PostReviewViewModel(
|
||||
reviewsRepository: ReviewsRepositoryImpl(
|
||||
reviewsPersistenceController: ReviewsPersistenceController.shared,
|
||||
reviewsService: ReviewsServiceImpl(userPreferences: UserPreferences.shared)
|
||||
)
|
||||
),
|
||||
placeId: placeId,
|
||||
onPostReviewSuccess: {
|
||||
showReviewSheet = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,91 @@
|
|||
import Combine
|
||||
|
||||
class ReviewsViewModel: ObservableObject {
|
||||
private let cancellables = Set<AnyCancellable>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let reviewsRepository: ReviewsRepository
|
||||
private let userPreferences: UserPreferences
|
||||
|
||||
init(reviewsRepository: ReviewsRepository, id: Int64) {
|
||||
self.reviewsRepository = reviewsRepository
|
||||
|
||||
observeReviews(id: id)
|
||||
|
||||
// TODO: cmon get isThereReviewPlannedToPublish from DB
|
||||
}
|
||||
private let placeId: Int64
|
||||
|
||||
@Published var reviews: [Review] = [Constants.reviewExample]
|
||||
@Published var messageToShowOnReviewsScreen = ""
|
||||
@Published var shouldShowMessageOnReviewsScreen = false
|
||||
|
||||
@Published var reviews: [Review] = []
|
||||
@Published var userReview: Review? = nil
|
||||
@Published var latestReview: Review? = nil
|
||||
@Published var isThereReviewPlannedToPublish = false
|
||||
|
||||
func observeReviews(id: Int64) {
|
||||
init(reviewsRepository: ReviewsRepository, userPreferences: UserPreferences, id: Int64) {
|
||||
self.reviewsRepository = reviewsRepository
|
||||
self.userPreferences = userPreferences
|
||||
self.placeId = id
|
||||
|
||||
observeReviews()
|
||||
observeIfThereIsReviewPlannedToPost()
|
||||
}
|
||||
|
||||
func observeReviews() {
|
||||
reviewsRepository.observeReviewsForPlace(id: placeId)
|
||||
|
||||
reviewsRepository.reviewsResource.sink { _ in } receiveValue: { reviews in
|
||||
self.reviews = reviews
|
||||
|
||||
self.getUserReview()
|
||||
self.getLatestReview()
|
||||
}
|
||||
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func getUserReview() {
|
||||
let userId = userPreferences.getUserId()
|
||||
let first = reviews.filter {
|
||||
if let user = $0.user {
|
||||
return String(user.id) == userId
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}.first
|
||||
|
||||
reviewsRepository.observeReviewsForPlace(id: id)
|
||||
if let userReview = first {
|
||||
self.userReview = userReview
|
||||
}
|
||||
}
|
||||
|
||||
private func getLatestReview() {
|
||||
if let latest = reviews.first {
|
||||
self.latestReview = latest
|
||||
}
|
||||
}
|
||||
|
||||
private func observeIfThereIsReviewPlannedToPost() {
|
||||
reviewsRepository.checkIfThereIsReviewPlannedToPublish(for: placeId)
|
||||
|
||||
reviewsRepository.isThereReviewPlannedToPublishResource.sink { _ in } receiveValue: { isThere in
|
||||
self.isThereReviewPlannedToPublish = isThere
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func deleteReview() {
|
||||
// TODO: cmon
|
||||
if let id = userReview?.id {
|
||||
reviewsRepository.deleteReview(id: id)
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
self.userReview = nil
|
||||
case .failure(let error):
|
||||
self.showMessageOnReviewsScreen(error.localizedDescription)
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
self.showMessageOnReviewsScreen(response.message)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
func showMessageOnReviewsScreen(_ message: String) {
|
||||
messageToShowOnReviewsScreen = message
|
||||
shouldShowMessageOnReviewsScreen = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,6 +126,7 @@ struct ProfileBar: View {
|
|||
HStack(alignment: .center) {
|
||||
LoadImageView(url: personalData.pfpUrl)
|
||||
.frame(width: 100, height: 100)
|
||||
.background(Color.surface)
|
||||
.clipShape(Circle())
|
||||
|
||||
HorizontalSpace(width: 16)
|
||||
|
|
|
@ -95,8 +95,7 @@ class ProfileViewModel: ObservableObject {
|
|||
profileRepository.updateProfile(
|
||||
fullName: fullName,
|
||||
country: countryCodeName!,
|
||||
// We shouldn't send email field if there's no change
|
||||
email: email == currentEmail ? nil : email,
|
||||
email: email,
|
||||
pfpUrl: pfpToUpload
|
||||
)
|
||||
.sink { completion in
|
||||
|
|
|
@ -44,6 +44,17 @@ class TabBarController: UITabBarController {
|
|||
)
|
||||
let authRepository = AuthRepositoryImpl(authService: AuthServiceImpl())
|
||||
|
||||
// monitoring network for sync
|
||||
let dataSyncer = DataSyncer(
|
||||
reviewsRepository: ReviewsRepositoryImpl(
|
||||
reviewsPersistenceController: ReviewsPersistenceController.shared,
|
||||
reviewsService: ReviewsServiceImpl(userPreferences: UserPreferences.shared)
|
||||
),
|
||||
placesRepository: placesRepository
|
||||
)
|
||||
dataSyncer.startMonitoring()
|
||||
|
||||
// creating shared viewModels()
|
||||
let homeVM = HomeViewModel(placesRepository: placesRepository)
|
||||
let categoriesVM = CategoriesViewModel(placesRepository: placesRepository)
|
||||
let favoritesVM = FavoritesViewModel(placesRepository: placesRepository)
|
||||
|
|
33
iphone/Maps/Tourism/Utils/ImageStoreUtils.swift
Normal file
33
iphone/Maps/Tourism/Utils/ImageStoreUtils.swift
Normal file
|
@ -0,0 +1,33 @@
|
|||
import UIKit
|
||||
|
||||
func saveMultipleImages(_ images: [UIImage], placeId: Int64) -> [URL] {
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
|
||||
return images.enumerated().compactMap { (index, image) in
|
||||
let fileName = "image_\(index)_placeId\(placeId).jpg"
|
||||
let fileURL = documentsDirectory.appendingPathComponent(fileName)
|
||||
|
||||
guard let data = image.jpegData(compressionQuality: 0.01) else { return nil }
|
||||
|
||||
do {
|
||||
try data.write(to: fileURL)
|
||||
return fileURL
|
||||
} catch {
|
||||
print("Error saving image \(fileName): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveMultipleImages(urls: [URL]) -> [UIImage] {
|
||||
return urls.compactMap { url in
|
||||
do {
|
||||
let imageData = try Data(contentsOf: url)
|
||||
return UIImage(data: imageData)
|
||||
} catch {
|
||||
print("Error retrieving image at \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue