From 7ac67dcec4c9e1912484b1b8e6e6dff15682ff0a Mon Sep 17 00:00:00 2001 From: Dalafiari Samuel Date: Wed, 4 Sep 2024 22:24:34 +0100 Subject: [PATCH] added platform specific download implementations --- composeApp/build.gradle.kts | 9 + .../src/androidMain/AndroidManifest.xml | 3 +- .../androidMain/kotlin/Platform.android.kt | 3 + .../kotlin/di/PlatformModule.android.kt | 14 ++ .../download/PlatformDownloadImage.android.kt | 35 ++++ composeApp/src/commonMain/kotlin/Platform.kt | 9 + composeApp/src/commonMain/kotlin/di/Koin.kt | 3 +- .../commonMain/kotlin/di/PlatformModule.kt | 5 + .../ui/download/PlatformDownloadImage.kt | 7 + .../kotlin/ui/screen/PhotoDetail.kt | 154 ++++++++++-------- .../ui/screen/PhotoDetailScreenEntryPoint.kt | 93 ++++++++++- .../kotlin/ui/state/ImageDownloadState.kt | 8 + .../ui/viewmodel/PhotoDetailViewModel.kt | 16 ++ .../desktopMain/kotlin/Platform.desktop.kt | 3 + .../kotlin/di/PlatformModule.desktop.kt | 11 ++ .../download/PlatformDownloadImage.desktop.kt | 44 +++++ .../src/nativeMain/kotlin/Platform.native.kt | 3 + .../kotlin/di/PlatformModule.native.kt | 14 ++ .../download/PlatformDownloadImage.native.kt | 76 +++++++++ gradle/libs.versions.toml | 2 + .../UserInterfaceState.xcuserstate | Bin 16011 -> 16727 bytes iosApp/iosApp/Info.plist | 90 +++++----- 22 files changed, 480 insertions(+), 122 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/Platform.android.kt create mode 100644 composeApp/src/androidMain/kotlin/di/PlatformModule.android.kt create mode 100644 composeApp/src/androidMain/kotlin/ui/download/PlatformDownloadImage.android.kt create mode 100644 composeApp/src/commonMain/kotlin/Platform.kt create mode 100644 composeApp/src/commonMain/kotlin/di/PlatformModule.kt create mode 100644 composeApp/src/commonMain/kotlin/ui/download/PlatformDownloadImage.kt create mode 100644 composeApp/src/commonMain/kotlin/ui/state/ImageDownloadState.kt create mode 100644 composeApp/src/desktopMain/kotlin/Platform.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/di/PlatformModule.desktop.kt create mode 100644 composeApp/src/desktopMain/kotlin/ui/download/PlatformDownloadImage.desktop.kt create mode 100644 composeApp/src/nativeMain/kotlin/Platform.native.kt create mode 100644 composeApp/src/nativeMain/kotlin/di/PlatformModule.native.kt create mode 100644 composeApp/src/nativeMain/kotlin/ui/download/PlatformDownloadImage.native.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 24ec7fa..c7f78b0 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -20,6 +20,14 @@ kotlin { } } + targets.configureEach { + compilations.configureEach { + compileTaskProvider.get().compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } + jvm("desktop") listOf( @@ -77,6 +85,7 @@ kotlin { implementation(libs.material3.window.size.multiplatform) api(libs.datastore.preferences) api(libs.datastore) + implementation(libs.calf.permissions) } commonTest.dependencies { implementation(libs.koin.test) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 3eb4b49..835a6d0 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,7 +1,8 @@ - + + Unit = {}, + onDownloadImageClicked: (String?) -> Unit = {}, ) { val scrollableState = rememberScrollState() @@ -62,94 +64,102 @@ fun PhotoDetail( modifier = Modifier .background(MaterialTheme.colors.background) .fillMaxSize() - .verticalScroll(scrollableState) - .padding(horizontal = 12.dp, vertical = 10.dp + getEdgeToEdgeTopPadding()) + .padding(top = 10.dp + getEdgeToEdgeTopPadding(), start = 12.dp, end = 12.dp) .then(modifier) ) { - NavBar(modifier = Modifier.fillMaxWidth(), onBackPressed = onBackPressed) - - when { - state.isLoading -> { - LinearProgressIndicator( - color = appWhite, - modifier = Modifier - .fillMaxWidth() - .height(2.dp) - ) - } + NavBar( + modifier = Modifier + .defaultMinSize(minWidth = 25.dp) + .fillMaxWidth(), + onBackPressed = onBackPressed + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollableState) + ) { + when { + state.isLoading -> { + LinearProgressIndicator( + color = appWhite, + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + ) + } - state.error != null -> { - ErrorComponent( - modifier = Modifier - .fillMaxSize() - .testTag("error_view"), - message = state.error.message ?: stringResource( - Res.string.an_unknown_error_occurred + state.error != null -> { + ErrorComponent( + modifier = Modifier + .fillMaxSize() + .testTag("error_view"), + message = state.error.message ?: stringResource( + Res.string.an_unknown_error_occurred + ) ) - ) - } + } - else -> { + else -> { + + if (state.photo != null) { + Spacer(modifier = Modifier.padding(top = 10.dp)) + ArtistCard(unsplashUser = state.photo.user) + } - if (state.photo != null) { Spacer(modifier = Modifier.padding(top = 10.dp)) - ArtistCard(unsplashUser = state.photo.user) - } - Spacer(modifier = Modifier.padding(top = 10.dp)) - - PhotoLargeDisplay( - modifier = Modifier - .fillMaxWidth() - .zIndex(2f), - imageUrl = state.photo?.urls?.full.orEmpty(), - imageColor = state.photo?.color, - imageSize = Size( - (state.photo?.width ?: 1).toFloat(), - (state.photo?.height ?: 1).toFloat() + PhotoLargeDisplay( + modifier = Modifier + .fillMaxWidth() + .zIndex(2f), + imageUrl = state.photo?.urls?.full.orEmpty(), + imageColor = state.photo?.color, + imageSize = Size( + (state.photo?.width ?: 1).toFloat(), + (state.photo?.height ?: 1).toFloat() + ) ) - ) - if (state.photo?.description != null || state.photo?.alternateDescription != null) { - Spacer(modifier = Modifier.padding(top = 16.dp)) + if (state.photo?.description != null || state.photo?.alternateDescription != null) { + Spacer(modifier = Modifier.padding(top = 16.dp)) + + val content = state.photo.description ?: state.photo.alternateDescription + + Text( + text = content.orEmpty().capitalize(locale), + color = appWhite, + fontSize = 13.sp, + fontStyle = FontStyle.Normal, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + } - val content = state.photo.description ?: state.photo.alternateDescription + Spacer(modifier = Modifier.padding(top = 20.dp)) - Text( - text = content.orEmpty().capitalize(locale), - color = appWhite, - fontSize = 15.sp, - fontStyle = FontStyle.Normal, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Start, - letterSpacing = 0.5.sp, + OutlinedButton( modifier = Modifier - .align(Alignment.CenterHorizontally) - ) - } - - Spacer(modifier = Modifier.padding(top = 20.dp)) - - OutlinedButton( - modifier = Modifier - .height(height = 45.dp) - .width(width = 200.dp) - .align(Alignment.CenterHorizontally), - elevation = ButtonDefaults.elevation(0.dp), - shape = RoundedCornerShape(10.dp), - onClick = { - //TODO: implement OS specific download action + .height(height = 45.dp) + .width(width = 200.dp) + .align(Alignment.CenterHorizontally), + elevation = ButtonDefaults.elevation(0.dp), + shape = RoundedCornerShape(10.dp), + onClick = { onDownloadImageClicked(state.photo?.urls?.raw) } + ) { + Icon( + painterResource(Res.drawable.round_downloading), + tint = appWhite, + contentDescription = null, + ) + Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) + Text(text = stringResource(Res.string.download_image), fontSize = 12.sp) } - ) { - Icon( - painterResource(Res.drawable.round_downloading), - tint = appWhite, - contentDescription = null, - ) - Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) - Text(text = stringResource(Res.string.download_image), fontSize = 12.sp) + Spacer(modifier = Modifier.padding(top = 30.dp)) } } } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/PhotoDetailScreenEntryPoint.kt b/composeApp/src/commonMain/kotlin/ui/screen/PhotoDetailScreenEntryPoint.kt index ac05aa3..76af813 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/PhotoDetailScreenEntryPoint.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/PhotoDetailScreenEntryPoint.kt @@ -1,15 +1,32 @@ package ui.screen +import Platform +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavController +import com.mohamedrejeb.calf.permissions.ExperimentalPermissionsApi +import com.mohamedrejeb.calf.permissions.Permission +import com.mohamedrejeb.calf.permissions.PermissionStatus +import com.mohamedrejeb.calf.permissions.rememberPermissionState +import getPlatform import org.koin.compose.viewmodel.koinViewModel import org.koin.core.annotation.KoinExperimentalAPI +import ui.theme.appDark import ui.viewmodel.PhotoDetailViewModel -@OptIn(KoinExperimentalAPI::class) +@OptIn(KoinExperimentalAPI::class, ExperimentalPermissionsApi::class) @Composable fun PhotoDetailScreenEntryPoint( navController: NavController, @@ -20,11 +37,79 @@ fun PhotoDetailScreenEntryPoint( viewModel.getSelectedPhotoById(photoId) } + val writeStorage = rememberPermissionState( + Permission.WriteStorage + ) + + val gallery = rememberPermissionState( + Permission.Gallery + ) + + var showDialog = viewModel.isDownloading + PhotoDetail( modifier = Modifier.fillMaxSize(), state = viewModel.uiState, - onBackPressed = { - navController.popBackStack() + onBackPressed = { navController.popBackStack() } + ) { + when (getPlatform()) { + Platform.Android -> { + + when (val status = writeStorage.status) { + is PermissionStatus.Denied -> { + if (status.shouldShowRationale) { + writeStorage.launchPermissionRequest() + } else { + writeStorage.openAppSettings() + } + } + + is PermissionStatus.Granted -> { + if (it != null) { + viewModel.startDownload(it) + } + } + } + } + + Platform.Apple -> { + when (val status = gallery.status) { + is PermissionStatus.Denied -> { + if (status.shouldShowRationale) { + gallery.launchPermissionRequest() + } else { + gallery.openAppSettings() + } + } + + is PermissionStatus.Granted -> { + if (it != null) { + viewModel.startDownload(it) + } + } + } + } + + Platform.Desktop -> { + if (it != null) { + viewModel.startDownload(it) + } + } } - ) + } + + if (showDialog) { + Dialog( + onDismissRequest = { showDialog = false }, + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) + ) { + Column( + modifier = Modifier.size(100.dp).background(appDark, RoundedCornerShape(8.dp)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator(strokeWidth = 2.dp) + } + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/state/ImageDownloadState.kt b/composeApp/src/commonMain/kotlin/ui/state/ImageDownloadState.kt new file mode 100644 index 0000000..7e71fce --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ui/state/ImageDownloadState.kt @@ -0,0 +1,8 @@ +package ui.state + +sealed class ImageDownloadState { + data object Idle : ImageDownloadState() + data object Loading : ImageDownloadState() + data object Successful : ImageDownloadState() + data class Failure(val exception: Throwable) : ImageDownloadState() +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ui/viewmodel/PhotoDetailViewModel.kt b/composeApp/src/commonMain/kotlin/ui/viewmodel/PhotoDetailViewModel.kt index 006625d..5a6635d 100644 --- a/composeApp/src/commonMain/kotlin/ui/viewmodel/PhotoDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ui/viewmodel/PhotoDetailViewModel.kt @@ -1,5 +1,6 @@ package ui.viewmodel +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -9,15 +10,24 @@ import data.repository.ImageRepository import data.repository.Resource import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent +import ui.download.PlatformDownloadImage +import ui.state.ImageDownloadState import ui.state.PhotoDetailState class PhotoDetailViewModel( private val repository: ImageRepository, + private val platformDownloadImage: PlatformDownloadImage, ) : ViewModel(), KoinComponent { var uiState by mutableStateOf(PhotoDetailState()) + var imageDownloadState by mutableStateOf(ImageDownloadState.Idle) + + val isDownloading by derivedStateOf { imageDownloadState is ImageDownloadState.Loading } + + val isDownloadError by derivedStateOf { imageDownloadState is ImageDownloadState.Failure } + fun getSelectedPhotoById(photoId: String) { uiState.intentPhotoId = photoId @@ -42,4 +52,10 @@ class PhotoDetailViewModel( } } + fun startDownload(photoUrl: String) { + viewModelScope.launch { + imageDownloadState = ImageDownloadState.Loading + imageDownloadState = platformDownloadImage.downloadImage(photoUrl) + } + } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/Platform.desktop.kt b/composeApp/src/desktopMain/kotlin/Platform.desktop.kt new file mode 100644 index 0000000..200c202 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/Platform.desktop.kt @@ -0,0 +1,3 @@ +actual fun getPlatform(): Platform { + return Platform.Desktop +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/di/PlatformModule.desktop.kt b/composeApp/src/desktopMain/kotlin/di/PlatformModule.desktop.kt new file mode 100644 index 0000000..c604a33 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/di/PlatformModule.desktop.kt @@ -0,0 +1,11 @@ +package di + +import org.koin.core.module.Module +import org.koin.dsl.module +import ui.download.PlatformDownloadImage + +actual fun platformModule(): Module { + return module { + single { PlatformDownloadImage(get()) } + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/ui/download/PlatformDownloadImage.desktop.kt b/composeApp/src/desktopMain/kotlin/ui/download/PlatformDownloadImage.desktop.kt new file mode 100644 index 0000000..35876e7 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/ui/download/PlatformDownloadImage.desktop.kt @@ -0,0 +1,44 @@ +package ui.download + +import io.ktor.client.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.lang.Exception +import java.nio.file.Paths +import ui.state.ImageDownloadState + +actual class PlatformDownloadImage(private val client: HttpClient) { + actual suspend fun downloadImage(imageLink: String): ImageDownloadState { + return try { + val fileName = "${Url(imageLink).pathSegments[1]}.jpeg" + + val response = client.get(imageLink) { + onDownload { bytesSentTotal, contentLength -> + println("Downloaded $bytesSentTotal of $contentLength") + } + } + val body = response.bodyAsChannel() + val result = body.toByteArray() + saveImageToFile(result, fileName) + ImageDownloadState.Successful + } catch (e: Exception) { + ImageDownloadState.Failure(e) + } + } + + private suspend fun saveImageToFile(imageBytes: ByteArray, fileName: String) { + withContext(Dispatchers.IO) { + val userHome = System.getProperty("user.home") + val desktopPath = Paths.get(userHome, "Downloads", fileName).toString() + val file = File(desktopPath) + file.writeBytes(imageBytes) + println("Image saved to $desktopPath") + } + } +} \ No newline at end of file diff --git a/composeApp/src/nativeMain/kotlin/Platform.native.kt b/composeApp/src/nativeMain/kotlin/Platform.native.kt new file mode 100644 index 0000000..9c91c7b --- /dev/null +++ b/composeApp/src/nativeMain/kotlin/Platform.native.kt @@ -0,0 +1,3 @@ +actual fun getPlatform(): Platform { + return Platform.Apple +} \ No newline at end of file diff --git a/composeApp/src/nativeMain/kotlin/di/PlatformModule.native.kt b/composeApp/src/nativeMain/kotlin/di/PlatformModule.native.kt new file mode 100644 index 0000000..e78afc0 --- /dev/null +++ b/composeApp/src/nativeMain/kotlin/di/PlatformModule.native.kt @@ -0,0 +1,14 @@ +package di + +import org.koin.core.module.Module +import org.koin.dsl.module +import ui.download.PlatformDownloadImage + + +actual fun platformModule(): Module { + return module { + single { + PlatformDownloadImage() + } + } +} \ No newline at end of file diff --git a/composeApp/src/nativeMain/kotlin/ui/download/PlatformDownloadImage.native.kt b/composeApp/src/nativeMain/kotlin/ui/download/PlatformDownloadImage.native.kt new file mode 100644 index 0000000..da71253 --- /dev/null +++ b/composeApp/src/nativeMain/kotlin/ui/download/PlatformDownloadImage.native.kt @@ -0,0 +1,76 @@ +package ui.download + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import okio.IOException +import platform.Foundation.NSData +import platform.Foundation.NSError +import platform.Foundation.NSURL +import platform.Foundation.NSURLErrorDomain +import platform.Foundation.NSURLResponse +import platform.Foundation.NSURLSession +import platform.Foundation.NSURLSessionConfiguration +import platform.Foundation.dataTaskWithURL +import platform.UIKit.UIImage +import platform.UIKit.UIImageWriteToSavedPhotosAlbum +import ui.state.ImageDownloadState +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@OptIn(ExperimentalForeignApi::class) +actual class PlatformDownloadImage { + + actual suspend fun downloadImage(imageLink: String): ImageDownloadState { + return withContext(Dispatchers.IO) { + try { + val nsUrl = NSURL(string = imageLink) + + val (data, _) = fetchData(nsUrl) + + if (data == null) { + println("No data received") + return@withContext ImageDownloadState.Failure(Exception("No data was received")) + } + + val image = UIImage(data = data) + + saveImageToPhotos(image) + println("Image saved successfully") + ImageDownloadState.Successful + } catch (e: Exception) { + ImageDownloadState.Failure(e) + } + } + } + + private suspend fun fetchData(url: NSURL): Pair = + suspendCancellableCoroutine { continuation -> + val configuration = NSURLSessionConfiguration.defaultSessionConfiguration + configuration.timeoutIntervalForRequest = 30.0 + val session = NSURLSession.sharedSession + val task = session.dataTaskWithURL(url) { data, response, error -> + if (error != null) { + continuation.resumeWithException(error.toKotlinException()) + } else { + continuation.resume(Pair(data, response)) + } + } + task.resume() + } + + private suspend fun saveImageToPhotos(image: UIImage) = + suspendCancellableCoroutine { continuation -> + UIImageWriteToSavedPhotosAlbum(image, null, null, null) + continuation.resume(Unit) + } + + private fun NSError.toKotlinException(): Exception { + return when (domain) { + NSURLErrorDomain -> IOException(localizedDescription) + else -> Exception(localizedDescription) + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 89efd3e..a32eb0e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ androidx-core-ktx = "1.13.1" androidx-espresso-core = "3.5.1" androidx-material = "1.12.0" androidx-test-junit = "1.1.5" +calfPermissions = "0.5.5" compose-plugin = "1.6.10" junit = "4.13.2" kotlin = "2.0.0" @@ -35,6 +36,7 @@ kotlinxAtomic = "0.24.0" [libraries] +calf-permissions = { module = "com.mohamedrejeb.calf:calf-permissions", version.ref = "calfPermissions" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { group = "junit", name = "junit", version.ref = "junit" } diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/samueldalafiari.xcuserdatad/UserInterfaceState.xcuserstate b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/samueldalafiari.xcuserdatad/UserInterfaceState.xcuserstate index fb4541d48e6f14c1bc2b170d82e5c166159d9f10..d14ad99513e0ca9e28f6e5291566ab4760bac3dd 100644 GIT binary patch delta 9078 zcma)h2Ygf27ymo=-lWiGHEo)tS#8p$Ns~0IDSOXKnPsoi5g=09(!qw@2Z94sAjlSI zi^xV90xE(m8H!K@6h&MhLqHTnW&Lkjia7qi|DSx4H20l*-nrkizV9$=%Gqf!Cl_9t zo7XS+3OAp7ja$Gi<6h@ha;vyExINrn?n7=Lx1T$}9pnyiA907dBivE$822&v33r_P zj62Pp<9^^Sa+kOtxy#%Y?pN+McZd4}8bTyQfe6GP0V&8p1!{-~12~}>G>5j(88YD+ z=mxzY8+t>37yyHUkPCS*9G-GrEO-LBFEg z=nnb~-9^8nKhOgVSb!6;5u1WI2`6JSPQeyz#i`ha?bw0Sabw&Bx4pI zJP;4UWw;zy;IVid9*-yBiFgv8jHh58zkui9xp*F4f|ugucnw~Q*WvYeGv12d!|&tI z@M(MopT(c!FYr11CH@MZ$6w>`@D+Ru2Yg3^_}_Bwv#s$VGC6 zTqnPh+vKi5Dv$}H1#&@*AXcCd#0iuFl|U_s7ia`pfliKZHpP4~(cN%`FcU7u9HW z`dTS{SSsObCY4~UicIX^qqMl7BEP&$hH8?M&9x$mu!uFEiCC;vk&!Z}vDqE9SA^j! z=918|hH9{e4V1R_D-LxS;cauM_2l4Q-FKm@4ZTt$GqugY%%b7NX(fe$vQS!NT&p6k zf*Z$8=3eENb8m7RxVKo!A7F|86?cuL^B>%OmdsKxf*YDbE0(-npesvUKg-c6Fb!se zrbl*%&Y@G0*FznJCgmwk%t<&Y(>9m0S8{VG`{++oLuaF!Nf&a9!)uGEvyxjvU7ljEZVP8$&kS6{t>xBn z>zSb&xlP<=X71ZGo%(17ZA2T>CbTJSMw`lEb6Wo{gS)D#Z@y% z&vReXPP8-4tmeMq%-nbM87ie+LXt#R(*c?7J9H=rloywpS_g)4;eYkpuW~p3wf!cS z#NDD@SzvbyX^pzUHMOw+#;vI2?$REA-TF@e&c2$v$KB^1a1Xghz|o$x7tN-5m zH6Xx1*r$+7qWx%px|)6NVOp&N>x}7IT;LB~k@yqpt$}EW#X$c(~iivL8tp(_-p(TsU z)^r#xpd)DD6tsi(Y;Ov5giZr93s@!wh6ntmeKN!8%cZ_$U_#;%QI;Z=XFE{>~VU=>%* zjXMQ{nIS`9DCEGfkVCF)Fp3sZE=SfSRGic}^qgFc^PwQLQm(>&ZXElL)6}pHBVZ&1 z28K&ec6n)_Xap^$MLDuoAz6|sB#kk~7Q*O3?fdrb7%24jU_#YU1jV5aF=jjlN<&34 z4k!yPi*YupV5-K#Fc=R*`*q7s%gZhE4`Sb?{z6uE#`-BoOX;Y(qfdg#eY$1$>6KLl zlR_xAM;{8aIQu%_S%Oc8=U@iRgy-P}coANrW9c|Lo=%_>=_ERtPN7q2a2?EM!N^`P z2j+6y*!O(SOeqVZ@M}5^F@(f){#K{8H*5V>ps$2#%F{q?$iiySuVNst3BMW4(`w%V zndEWs!zOr}i>rdo@D_cJ&ZvSdu$9iFuY``LW*T89yw5TNcEP*w9(|s^Kwqqe-S7eI zp)b+d^kr%cmDp^dE1F(GILPog1Ro7%7*tdQ3i2kD`^&n^WN&yLX1w%#mo8hj_kaJZ zC0~r8v+6K80!RBYO!{QDuYx1oxElDF?f>N8`>&5z#d)f`m(0HU{QCo5|1Fo!GPN#P z`E>fiN%)K#&W=z6r|7G6P8FPnGjuMUM~#CD14aJY!1R|5Ehx^f7|p^p%U?92d}JLs zUxI!m4akm}_WWF{@<|eY1K)>H`4+yTuh9ihRE8uf9li?JLTM^B{)q)uno1e`cNX4& zn=CSJu`Ij+x8V+5MpvkNDtPQXWqLqM0( z<#feg33vkuSavLTwRCm~al!s=EBi4ChaW zx}NNO(#^_G!IPLrF}3ksl_S&S7mrTMEh#DVr;Q(7=qM>IF7cO^PjImISDqtF3F%^W zrtsf&)voqXNYAP$GSD|!V?stwicBaeG*P7uuAv|NCzi;9tbfDuExPtUu|!VfdV=L< zy6$gSq6~&5YJ?i2CUiaBKsWvq%OC@j=Kl})+SkIj>3_rbIImDw)RVJUqi(1>>OtS8 zTjtX`m#`nQ~;xNt0am1c#^b6nnxXx7*Xmp3taKqbXDB z)D1%g)hHL`p?tcN?xOF}-F553AF~a{P*5Q3#)RQU1v6{8GQas7|yf ze7TsuPu)4P_&%AeT@(%q*)UiHt+zb7YDqO};aWx#H3ZjCyuI8vTN zGtf-BkM5@j{uwF%uRvi{n&sxfe*{VDzp?WL$9Od^e{a_kJ4kS z&`MMZaj2SpOi!|+{ROr5&+-p1@5?xF$e>rX9hj9np}3+vOdtmI3XB+8d!=6II8j}U1S@aX5B-~Y?~V@&`+1Xi{5863%y5cD$#CwiqR}>@I%J3 z&_1-Een!tep*rXgI{HtVgpP4$^a(x9Sk@V8d}5!eHNWWT5{+umnSX6PTf6o1x~+dZ zfi29n(0O!;v#&v4qYLO8^ey@heUE-X7wI|rCH;z?r(e?x^c(u^R^ z5cMvu#p4wE6WwE7C7U~j?uSg0lpy_~E><3*N32)ZHimGEufiNR?&*ccC}M;$XJ5IJ zTI&~tIEJ%tz!A6sZipjs6c%AImS8EC;b<(UKhn$e3cX6N(VysbdV}7iKhsBO@9jq0WV@xZi6s_@JB6&|EK$U|-7GW|G&7lZ}oLk$ly`f3%N!HeiT z9-^u7zi0(sR!b}D)vv(SEco$CT#2iAh~XiY2SqjP!EfNz^kp96cu+DX)l-=ms&HCE z2BjxhgEw$eyz$?3=s)Nbev65|{cq8KGpBlkw&5MMaD+uuZNv+4gbW0 zg$HXDzJYJ@kjjH2?5%iw8{Z8J+zETj#=M1phtoXRS-tx<- z^5Ci_m=M^*gPRA>)5O+GqBtoL{Ttr9hpK)bEbl1a`E29wP+JX80~b%CGqccE;A_PwyX2BB!z)YbVN@KB!MInBQcRAl1vz489X%N zp)n6lcxcK)Gaj1r(1M4SJhWOzEOj6gdmYHc6$Ux{B*0!gw66!bN9}V#HfhOFCarjA z^B85)mZ41Ak@n#?4{dp9SNF;Iuv8|hx4sMM#;hk{{c<5L~x|1F}bmHNeFxrDj zAJRX3yc*JvzRE*qc04jLe7wy1V0uEiqv2A$)*O=0d_abgT$0B_7ap>B=vqw*h(GLu zZaj2<+6N6tAp>?a1GdDH7|C!hVL!*PpB*eu;apC})!{rgjC0Sw3<={LoI--E&B1M2 zT}dbp*-vUZnNFT(MUOm3W{{aY^yZ-t4}Gi23*<%e5)b`&7{Ei$f6%pmv$g+0#e)Bz ze6E%pkvU;sF%0|vPrmkquGMXNjgd04fGi}7co@jTARdN1=4E6l7pxltvA#-{Gv-BB z@G$tlnHi}LUth(;(5I-`e=)QFJ4vkf@>;ToB-c*qr2k_|lMg<88b!MAxRsO`gK zE7`^#0=rcd7FLn1>>)7R_sKi#@LV)StjE>04e~AtQ5G zkptv#IIa$oLxdHn01wadFshmyAxFtE9twFVVQrfjc&I+f^1kNlr2CgcXa? zmEEldk2$;X8V|*FC-{n-XD4XS23%F-D>jmN zZ1Dy1ZCEEOYGW$NcRZB-rITsAM1CZf2QZywYycrB8o{%XLXoFsgYcA&TqV~+Zcl8= zRl=Sx`!j-DJEW*re1rTPR?O;MMJ2h#!`M)vr%fhm zeYwHhP&QGYj9+9E^kul3P0!ci4ft&~JKv6XusQi1f>^0k5G7Hwp}3A!!$ejI)7fCW zF&m0ECoM^9(w0@Lj%*M-k;?+Bpo3tTAXhL=FkLW5utHE76jTe|5WFc^D_Aes zD0pA+fncv-pWuMtkl?W3sNiG41;H;up-?S!2wMxg2?q)DgayLk!jZya;TU0=utGRa zxJ0-~xL5dz@Qms&i5MD@6OkM7Y(z!G z)QDLTb0QW;Y>W6XB6uL;P{fglV-cT3T#mTaK+-_gKpusnqN38Hnne{v9f_)mx*T;= zBo`S(Mp2R|MPwD(MA@RjqJXGSR3sWBDic+R7K^Gx)uJ~PK&-1{UN$1 zdLViv1~C>3#1Z0#;wZ6LEESu@7ICWBE>07>#2#^aP@ExdEN&`pE^aCADxN5wEnX;I zFWxV{B7P(>N;*hJN}iPzO2$bhNM=f2l+2R6BAFwZCs{4oEZHV`N3v7$uH+-hamfiu zjpQ@Qmy+|63zBao-%D;u9!jI78flW$Ep04qDs3)pDeWNbB+Zm|k#?2lNJmP?NXw;T zrQ@Z+8Pa*u`O*c_MbahGWzyx+_0o;f&C<7}Tcz8jJEXg$2c(}%Z%FUU8ptfNR9Pcg zb6HDSYgq?bCt0Sfi)@&zOg34@%cjd_$ex$2ksXj-mi-y6ind2Lj_w=XKYC#F;OL>z z!=m$|3!;Zd2czdjS4QuO{y6$_^wsE}qHje19E|=Y`nKFGcgi#5jpa?{&E+lSUF3b` zgXKfz!{mALV)+>PMEPR*a`|rgQTfO6ov+^(Gm*kh_SLHv+f0f^lffz|ld`wb| zImQx`8Z$nIk69SAEM`m0dohP%4#ymg6~!uI6Jss0>9HMSyT?w94aTmE-4VMd_E_vE zvB6JcPsW~${U-Ke>^+4|(OA({(OuC~k*(;X=%*N<7^E1Y$Wi1f#wx}uCMqT?rYfjn zn&LUdOvMX|mlU%VYZZqTKgCJnJaPTwCdVy}dpGWE+&6LG$6bv3G4AKMKjQAiJ&1dx z1SM7qlyap~sa9%~2}+|fNoiJQDu*k}g35`?$;zops(fDgqH>n)kYKdx@YPo8qYJ=)M)dAH()kmr$sv6Y= z)eovms>`bDs++1?s$W%qs2ixG)fTlw-Cpfik5C8Hqtv6-#p*HYGIfRe6?JfqdY*c| zdVzY8dWpJPy-vMBy-EF+dW(9G`a|`8^+ELsb&dLh`djt)>Wk`M)VI~Ysee~LiWkL8 z;$`vj_@VKm;z!39$B&7BHGWt8zW9UjAH^SuuZh1Ne>47;My`p~#A#HTc#T%0*Cc3+ znr507npT=Nns%BFnogP?ntnme0L>uH5KWGzP*bES(UfX}8eTJ9Gea{^Gheenvq-Z- zvq7^-^Ok0dW{>8O=CJ0d=3~t%&1ub9%@>+4HTSg!ZEJ0&HcQ)0+e2HfeMP%ayIi|c zTcus4U9H`$eM`GV`=0g#?OyFZ?Mdx9?Ro75?YG*8x(GJr(&!R(!BkyaT_;_RZjA0F z-E7?}x;eUey7{^Vx)r+Bx;46Wx(&Kby7zRub$fIl>W=G9=xTJI>Aux{ue+%GQFlXk zPxnCgNDq36Ua42>HF}-ipttB#^>%%l-lg~Go9VOlL-ix|75b@qs-LERPCrxss(!9M zq<>AnP`_BeBdGsOf7c*0=nM%4qaoRlVz3%o7&;od7`hs|8+sas8vKS4hJay|q0~@r z7;6}Bm}r=8m~B{WSZSy-tTL=NY&2{(ylvQO*lyTk_}FmXaKZ4c;d{eH!;gk5hHHlF zhKC7U0!knW!h{A1kqM#%NrEh)WkONH(uAW4cM^liiTx61CN51}llXq(r->&M&m~?; zyq0*^NQ?rb(AdDJFs2z@MvpPwm|<*eY-((7Y-wz5Y-?<9>}bp}<{I;j!;JysC}WXv zjIrD}&N$IH#TYd5#%0D`#zV$SCb7w2YHsRb8e!s1A=5h3R@3{Y4@`Sa`%DK+hfJq} zrqiagrZ1B0NllWPCACOum9#KvRnpp|^+_9(wj^y!+L5#?=}OXXNe_}9B||b!j!YIM zOOxfvvB`%i7Mo?}7;~IiZPuC%W}`XT+|=B{ z+{)a>+|Jz5+}Zq$Im_JL+|!(GE-?pRHE%VaHvf@gNNJanm%^tkPkA$CUCPFk%_%!m zK1|u4axmqilp`s}Qa(>PpYl!0_bHcCuBF^a`Pm}2xGXI!Z7m%voh(_FZkC>wY)hVH zl%>#8WSL}{W|?7m!7|G-&obY#(6Yp`%(B~Z$*Qs@TGOm;tnI8>)&bT+Yms$Q&`Pb- ztTU`HSzorkYMpCcY29euXFX^=Y&~W@Zar!J%zD;(&U)VZjrET8ck7?l`__l4kcv|U zsS&A>siIU#sx9@I)S}cmsoPS|r9QOjY|U*0Y46%Wc)R)wZ>^^|r&dYqsF;wg-03j_eWkNV~``vFq#xyWQ@!XV{zAo7>yjJK8ht zUF==$W9@6~N9;BB3-({^x9txckq({1;IKQ~4zDA_(ah1p(c010(bF;1QRW!ynBbV~ z2s)-YW;kAO%yPWqnCn>WSnF8t*yz~o*y7mcc*n8R@t$M1V^7fWmE&HTA+1fCKW$dp z=CtE!*PW5h7-yVQ?bJAvoDQeU>2+o}n>d>}TR2-evz$Ghy`BA>gPl3fJZFKEcdl~2 z?R>|%)A^oruXCUCpz|Z=8Rr+yOU~=gpPj!te{()?aW3o#QsIh3k^*n(MmjXV)FqUDuzk`|c>W%B^wh+zD=zJK61VH*q(2w{o|2XS%c8 z-QB(1z1NQ2bOYT)x6p0$Bf5irL%*Xx&|e^hIM9L-Y+wfmB!d?n0UtDi#_$}pfOKdB z9iSt0g&xos`ojPi2x9^;7D{0pjE4y@5hlT8co_mP1*XAl2*SIt5Ej8=SOOKW4%Wj4 z*a%gy9jaj$d;t4kKOBHVa13hT3|xY1a1-vpJ@^HF#|j*Uqp=dlU=@zVYOKL=Sc?tV zifuRrd$1QjkDKCVIM5uo!tHQZ+zt1^eenQ15a;3%xDXfPvA7gZ#BboK_)RYk>Mntj36(P0y2^m zl43HROdu1<6!I#0jl4}}kax&zvXCqyi^&pFNmi1LWD_|^J|l<7VRD2VCCA9;ZU2wL(}N1bL)$Iwzbfxb*%rBmr_8l>;iIdlPCNSD$|x|*(` z8|W6glfF+spkL50=_&dZ{hFSp-_SGkEImih(+l)ldXa`*qL=A+^h$6nX%633Z6g~X z%;58Q61?7EzNl|Q7fWVd7Rh|UL!yd^Oq9iROv9Ya5&SVCv1-0}BEapSszI05kx@#u zE+Nt8bo*+;8Z?yj9Fs4-itPu?0som76z-`W)w;f{ga zk74a-(MG1=JS}L_<&k8ih*Ho9Hbxiz|K$+J$y=weRB^ zKg(5pk8Ak>h#(9kU;sDQZd0z^me3BeU^u+Q6^LCPo~IJ&7Wb6v@yGKB%i4&16aCD3fV|4$1Sf_t3o1 zv$@P&j>=d{aJ0k~ScDcYWuCgOmZI`OT{F9kDajd9;BQ%6oHM=>EkzZmv`^Q}+S|nR zmr(6r{V2=PX5?CnR-lz=6U1S3_YtGVHOV*0DW^LFO_C7nszG7c<+L@%5V7JIlL69Z}*GKjW zc914`26f39mp?LpfqQL8bqE(68Gle__YMo;gEC%TcyGs&mfxqA?}e zo&Cc~dKUG_&l_G+Q;YCdXmL6Enzdz~dNj_UOUSi?<8Th0M;Fky=pvteJJy~hbYLA> zrxoZj`VL*;*Q?0PUSOTsTl~723(gVTC-Wze`hM|h!zo07Cjim=W z%2QnEPxP=HJzyDZ;MeF8AOOHf1_UVQl9;op0W^eg{+|RQgRA5l?Y&qY%V*u$G}epx z+3-^!134&olPHJ=CB%RVVnL0nf&s;dhJ#op%VD{!NAM>_ldd}SuoCnT4+hqg^=19p zz>{EtMBY{m7O?i~n9KE?KP=y0+^ZH&K0Z66#rcJKY$zL&EpHL@S`veIqD{(9@IwDK zy?eIHFYt#bQ3;))OVFycl5UU@?50eH?!n2*l*XC7tDevtdP7sL2=|bj(SA-Gcd@^K z=aR90hS)HcT{qt>=+~=jX0INdDo@}Dz`W4Q z_h2rYz$R8e8O&#s*kooJlv^}(%t#*Z&i=x@lHvUe@(ca7LHCcAAFdmADSEh+Q=~2d zK_#q2s!CV}%V7lzFvf&RSOu$L4SR*X%3ce;9m@inIG~$h%K#4Im@)aeL&lf*M|YFU zE5z&bcX#jj!}_vQ_npVKRI({`pl*e2y*a48I<~2Rt*EpHcJTH)pKh|s;cn-wr0%J_UI1@Ef$d&JF~!9kp0>I9B}K&@@<;RQ_>Lw1 zk?d`@>ED(5C7h~PDpyV(o5N=OyGqZ(xmuM5xJ=o*Y-XKAS$ds2mqRN34z9rWY!-Wm z&HgWy)`_xSSN~94{iD0JQVae=YX2cF#)ZZBijjKk}AjHR5%OCR&N@-dG$)bh9!=J}=^>sk3@ z9^(YgV@ScK(3j_kirVi$x%4r`upKAy&|wF5vSn;}1$NEG>!g+hPX91ZS%D0(~PV4u|4h- z^05OK)cSuHRPcr5NHxyDnYA>;Jva?F{!PQ~_UJHP=5ZG87j)|5pntHf-W~Wy4#$IV zc6|wC*$e@;E7)6Mvu&^T}78F(h1#rCkh>?8K^GETKBn9m6o$^D%ioD)CAx)3i% zu2pyuUW}LErMMhd;7YuV?PL4d0rn|7$Ub9-*x{;MhBByMI^mtTnlF>^`|M~r-o=gu zXGyh<@E)|d0`J8ivCr8TPbxm%hYyD6wI3hApR(iZ1gojUlaUo4=5v>^lfjEpParW} z*!0gu6RyFho@(?mZ*-c&C1YPQ6K}!(qs2LV9=Vn-We$0r5INMB@OMv~yuwa1M{P2| z*FtXofUmP}*tt44Ej-ELTlnr%L;l2vyvNS)Av9DYL(jnILwaV|pR@ zf=nNH90VdFQOLE9h)Eb}KpK*85qsT647G@0U$#U)~M2Fb@ti zBP}?H5Xpv@lXUhobJS0+HEEArl_Z6a;o<%J*_}$#fpjFD*gXNF1aSRr+J*Gx36XRq z-AD%MPI{0`(v$Qey-6ST3;UJbXTP!E*&pmr_JBQPf2|=|bqSFSgd!a;(PomvC;zA} zBMLz3QlbEIo)ddPk0PED$tVHP0AUqm8hJ~A1_Cq;cDKhqGqbjmE8zS3 z{(@vKCkAfXGT%L(0f}_L=}v0c64Xc1zIb@de+BwKc3EOOcF}!2o4Ozx-m-E}{9%JH^`fgW|wY5o)tYK3HQ2f)4^knr=vYC7k!gdR(B3sEevYqT8 zJIVW`n(PuFS^%X0F#@Orh!sFBfJT5g0ki_>){xzgQU0i|?2!G)DuBKoWkWs6&RUc^ zpA4b=g#hvOD1Y^@m8ahA^Wyp6~HGzssL#MJR?A(HB?&{5^AUm2~7-zq;Y*no~sW@ z+uFsz3)IVFLU|X@K8^`}=6_q&(x$Wp_nJ1N%>`&8!1EO}owgL9sQ@iPD_bpXM>~cL z+QVD{n(<0ZJCSJuaO(y^i7U=VGiXm9E!v&-pqT>j9K>6k!M^)J7{a zxZCb%SVIRQ86CtK{m^CXI;Do@@Y^9zN8|p1ir)rAwTs$0;@Q3nemR=N>d8<2ba)-k zc_EzJahxenbzFnZLqG=#=_sTsr$qvED5oz8(2+{y}f*^uT*Gbi6=0zCFt+Csr@! zc*at1$Vp+-_cjla=yQ-+eOf2bVaDo z`nylw<663ouFv9qj^^7sQDL3{{emsiTBmQMoA|-glcS6}yejENx*3)B z?V4G4Uq3_@-4+^Rs{jMa>2?7I^6hxbwB~_YpR4JT$G%4Jgwwme&2Gx;{u=rr-9z`% zkLbtr6S|M?rw8b#0%Qv?Sb!V>h6pfJfLsCm0t^#?dpCRyveHAyip=yVJw`vL$LR@L z!(;;F^9mO!zz6|e6rezWk%9;c1t_X|{h3?{qu}B4f!o zepVSE0za*MjjSV|@N>wEfwy01vN>nT=5sej%6HO3J61^pQTQpPjjwmRaBbqBJ6D0J>61Nez6X%O3i)V@#ikFL5idT!*ir0&` zi+75v#UF@26z>&(CB6|x!wg}ru;egzm?!MHur^^`!uo|}hYbiL=7&a$tUfBGw zg<(6w_J*AcyAXCU{ABpq2vLM2;+crR{D{hk_apX39E~_0Q4{fH#8(lgB_Iix#7g2M zI*CDIlq5>pOEM(gC7F@|k{2b#k_nP2l39|KlGT#6lJ$~}lFgDT$u`Lj$@`LBlHHOo zB&Q@_OTLkum7JG+E4d{3PV&9vn&i6VW~3~#X=K;PtjI}`??zTf9*_K4ilwT6G*Q}E z+DY13+Etn%&64`1Bcx-cf^>>>p0r%LOu9n4O1e$@v2>sGfb^jBko1W3nDmVFob-b9 zqV%%#iu9`V2kApuw9F_=mUWSJlMRsN$cD=NvJtWZS)pu{>^0duS-EVPY=vx<>@(RF zc|*BY{+zsvJWF0GA1|LM4@{N^T1*vf#{^@wCMEc_R*Qq z{^-K!vgpOppGBXFJ|BHO`eyX)=sVHBM?X?hWt_5;vX8PrS*RSPELN5%$12AuCnzT= zUsf{Z9OYbPnR0=0k#dQ$Tv@4Hu3V{Htz4@-sJt2z5tAI#J*G5fK}>bb>6l+*9;vX3 zs>CXpO09}h=~RJuRf5W-N>Qb$8mXRDHB+@vwN$lM4OUH2%~H)#%~h4D7OR%3Dpbo< zRjNIzk5q?LhgC;a->5FCE~~DnuBvXU?x=oJ-BbM!p)#?w_AFB7NKUVKkA5b4uA5vddUr}FG|De91zNP+A{hRuc1~fz?(u8T08kI(^ ziPP9L4o#9KS<^(*RMT9Ou4%97sp+lhtLdl7(@fI5tYMm0G_Pu2*G$z+(@fXQ(9F^- z*92B-R%_O3)@wFuHfyRh+cY~g?`w8xzSi82)5bN6%ZYmdhJH-cI{4Wwe|z; zhuW(;jjoBVwJt+9ROiL%)D=w=0Ui*yybWx5r*wYv4Xjk?Xcy}F~i8r>JVQ@S&{ zbGi$$CLz^#k?U`W*dGySH>TRzYu>j{$czh0~m-wWC$}f zG(;F84e^EqgUMhvSPgcA)8I0=4IaZF!wkbN!}WxigtiH#2`dt+61FGoOxTsMJ7G`4 z$%L~B7ZWZgTuHc^a4+G{K*Ga>M@BFzp>dRPyzzD88^)=|X~yZsGUGzyVq>|n(zx8X!B}P7X53+X-*~`y)cCpagz=>D z8{=8ydE>XnOU9eVUrgB4&=g^cG|5aVliCz#(wX8-9@BHCKs!^mDaSO_zuiMJAeOuU=;Ge2*{W@;9jv&_TH!_6bi1?GL`6Xq|>r_86# zXUyl!*UfhW=AX^KnD3kavLFkwh%F5)F&3xAWpP_P7M~@}(#Z0xrHQ4frMV^3(#z7v zl4a>{8Dtr38Dhz`}cIzJN$JYJUPp!wTr>$qL z7p#}8SFBg9KUi;Af3rTcAsewZutnIUHn}a3Y|F3>v<Z@2Ha*VwPxuiJ0g@7RB~|7!o;{=ojofgK`8oI~$0IE;=&ht**ZIGhfb zBgNr$q&hMjMUJ3jo8z?Oku%ZR%9-PQ+4+WZn)7YvOy@jjxwF!_+_}=Z+PT)b)4AKZ z*ZGO_p!2ZvnDe;v=OlTOKFOS9OL8V9C#5AdPHK|WG^u&gsHC!_>ZDJSjwfACx{`D| z=}(u`C3opvCYRY|bGck8u7KB->T2cc=IZI{?aFcua^<*&y8NyZm*9HORpwggTH>m3 zEqASQt#xg1ZFX&SedapiI_5g=s&Rek`pR|Mb;fnx^{wlY>rrxia;xOrOesB6#-uDxS(&mm<VpWFW*<-EA*}PZT0Q;?e!h?UGiP=UGv>2@ZI*^_1*K`_x<5}n2J(kQe#u& zQgx~Esm4@usx8%-nw;uM^`$mT?U_0<_4U-U)J>`TQ$J7rF7-j0G%X=5CoL~+a@wnD YGt=g!mDL?ZV_f%3uhsq5U(*)+58!C~=>Px# diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index 412e378..5f0c5ab 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -1,50 +1,52 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSceneManifest - UIApplicationSupportsMultipleScenes - + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSPhotoLibraryUsageDescription + We need access to your photo library to save images. + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + - UILaunchScreen - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - -