Skip to content

Commit

Permalink
Add: Image share option for dive plans
Browse files Browse the repository at this point in the history
Right now this serves as an initial starting point, a sort of MVP implementation. It would be good to offer more information in shared images in the future; like graphs, additional general information (max depth, average depth) and more. Also the way in which compose is used to render images is experimental at best.

Implements: #1
  • Loading branch information
Rolf-Smit committed Aug 23, 2024
1 parent deedbac commit a0830cd
Show file tree
Hide file tree
Showing 26 changed files with 1,039 additions and 235 deletions.
2 changes: 2 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ kotlin {
implementation(libs.kotlinInject.runtimeKmp)
implementation(libs.navigation.compose)
implementation(libs.jetbrains.lifecycle.viewmodel)
implementation(libs.jetbrains.lifecycle.runtime)
implementation(libs.jetbrains.kotlinx.datetime)

implementation(compose.runtime)
implementation(compose.foundation)
Expand Down
10 changes: 10 additions & 0 deletions composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@
<meta-data android:name="org.neotech.app.abysner.ContextProvider" android:value="androidx.startup" />
</provider>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ actual fun applyPlatformSpecificThemeConfiguration(colorScheme: ColorScheme, isD
// }
// }
}

actual fun platform(): Platform = Platform.ANDROID
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Abysner - Dive planner
* Copyright (C) 2024 Neotech
*
* Abysner is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

package org.neotech.app.abysner.presentation.utilities

import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.content.FileProvider
import org.neotech.app.abysner.applicationContext
import org.neotech.app.abysner.currentActivity
import java.io.File
import java.io.FileOutputStream

actual fun shareImageBitmap(image: ImageBitmap) {
val bitmap = image.asAndroidBitmap()

val file = File(applicationContext.cacheDir, "share/shared-image.png")
file.parentFile?.mkdirs()
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}

val uri: Uri = FileProvider.getUriForFile(
applicationContext,
"${applicationContext.packageName}.provider",
file
)

val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/png"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

currentActivity.get()!!.startActivity(Intent.createChooser(intent, "Share Image"))
}
15 changes: 15 additions & 0 deletions composeApp/src/androidMain/res/xml/file_provider_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!--
~ Abysner - Dive planner
~ Copyright (C) 2024 Neotech
~
~ Abysner is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License version 3,
~ as published by the Free Software Foundation.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see https://www.gnu.org/licenses/.
-->

<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="share" />
</paths>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="#ffffff" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92c0,-1.61 -1.31,-2.92 -2.92,-2.92zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20.02c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>

</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="512" android:viewportWidth="512" android:width="24dp">

<path android:pathData="M336,192h40a40,40 0,0 1,40 40v192a40,40 0,0 1,-40 40H136a40,40 0,0 1,-40 -40V232a40,40 0,0 1,40 -40h40M336,128l-80,-80 -80,80M256,321V48" android:strokeColor="#ffffff" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="32"/>

</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.neotech.app.abysner.presentation.utilities.DestinationDefinition
import org.neotech.app.abysner.presentation.utilities.NavHost
import org.neotech.app.abysner.presentation.utilities.composable
import org.neotech.app.abysner.presentation.theme.AbysnerTheme
import org.neotech.app.abysner.presentation.utilities.BitmapRenderRoot

enum class Destinations(override val destinationName: String) : DestinationDefinition {
PLANNER("planner"),
Expand Down Expand Up @@ -66,7 +67,8 @@ fun MainNavController(
}

val navController = rememberNavController()
AbysnerTheme {

BitmapRenderRoot {

NavHost(navController = navController, startDestination = startDestination) {
composable(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Abysner - Dive planner
* Copyright (C) 2024 Neotech
*
* Abysner is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

package org.neotech.app.abysner.presentation.preview

import org.neotech.app.abysner.domain.core.model.Configuration
import org.neotech.app.abysner.domain.core.model.Cylinder
import org.neotech.app.abysner.domain.core.model.Gas
import org.neotech.app.abysner.domain.diveplanning.DivePlanner
import org.neotech.app.abysner.domain.diveplanning.model.DivePlanSet
import org.neotech.app.abysner.domain.diveplanning.model.DiveProfileSection
import org.neotech.app.abysner.domain.gasplanning.GasPlanner

object PreviewData {
val divePlan: DivePlanSet
get() {
val divePlan = DivePlanner().apply {
configuration = Configuration()
}.getDecoPlan(
plan = listOf(
DiveProfileSection(16, 45, Cylinder(gas = Gas.Air, pressure = 232.0, waterVolume = 12.0)),
),
decoGases = listOf(Cylinder.aluminium80Cuft(Gas.Oxygen50)),
)

val gasPlan = GasPlanner().calculateGasPlan(
divePlan
)
return DivePlanSet(
base = divePlan,
deeperAndLonger = divePlan,
gasPlan = gasPlan
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* Abysner - Dive planner
* Copyright (C) 2024 Neotech
*
* Abysner is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

package org.neotech.app.abysner.presentation.screens

import abysner.composeapp.generated.resources.Res
import abysner.composeapp.generated.resources.abysner_logo
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.format
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.compose.resources.vectorResource
import org.neotech.app.abysner.domain.diveplanning.model.DivePlanSet
import org.neotech.app.abysner.domain.settings.model.SettingsModel
import org.neotech.app.abysner.domain.utilities.format
import org.neotech.app.abysner.presentation.component.BigNumberDisplay
import org.neotech.app.abysner.presentation.component.BigNumberSize
import org.neotech.app.abysner.presentation.component.appendBold
import org.neotech.app.abysner.presentation.preview.PreviewData
import org.neotech.app.abysner.presentation.screens.planner.decoplan.DecoPlanTable
import org.neotech.app.abysner.presentation.screens.planner.gasplan.CylindersTable
import org.neotech.app.abysner.presentation.screens.planner.gasplan.GasLimitsTable
import org.neotech.app.abysner.presentation.theme.AbysnerTheme
import org.neotech.app.abysner.presentation.theme.platform
import org.neotech.app.abysner.version.VersionInfo

@Composable
fun ShareImage(
divePlan: DivePlanSet,
settingsModel: SettingsModel,
) {
// Disable scaling and dark theme and dynamic color, the image should be the same for every device.
CompositionLocalProvider(LocalDensity provides Density(LocalDensity.current.density, 1f)) {
AbysnerTheme(darkTheme = false, dynamicColor = false) {
Card {

val backgroundImage = rememberVectorPainter(vectorResource(Res.drawable.abysner_logo))

Column(
modifier = Modifier
.drawBehind {
val vectorSize = backgroundImage.intrinsicSize.times(2f)
translate(
left = (size.width - vectorSize.width) / 2f,
top = (size.height - vectorSize.height) / 2f
) {
with(backgroundImage) {
draw(size = backgroundImage.intrinsicSize.times(2f), alpha = 0.05f)
}
}
}
.padding(vertical = 16.dp, horizontal = 16.dp)
.fillMaxWidth()
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
text = "Dive plan"
)
DecoPlanTable(
divePlan = divePlan.base,
settings = settingsModel
)

Row(
modifier = Modifier.padding(top = 16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
16.dp,
Alignment.CenterHorizontally
),
) {
BigNumberDisplay(
modifier = Modifier.width(96.dp),
size = BigNumberSize.SMALL,
value = "${divePlan.base.totalCns.format(0)}%",
label = "CNS"
)
BigNumberDisplay(
modifier = Modifier.width(96.dp),
size = BigNumberSize.SMALL,
value = divePlan.base.totalOtu.format(0),
label = "OTU"
)
}

// DecoPlanExtraInfo(
// modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp),
// divePlan = divePlan.base
// )

Text(
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp),
text = "Cylinders",
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold)
)
CylindersTable(divePlanSet = divePlan)

Text(
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp),
text = "Limits",
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold)
)
GasLimitsTable(divePlanSet = divePlan)

Text(
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp),
text = "Configuration",
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold)
)

val configuration = divePlan.configuration

Text(
text = buildAnnotatedString {
appendBold("Deco model: ")
append(configuration.algorithm.shortName)
append(" (${configuration.gf})")
},
style = MaterialTheme.typography.bodySmall
)
Text(
text = buildAnnotatedString {
appendBold("Salinity: ")
append(configuration.environment.salinity.humanReadableName)
append(" (${configuration.environment.salinity.density.format(0)} kg/m3)")
},
style = MaterialTheme.typography.bodySmall
)
Text(
text = buildAnnotatedString {
appendBold("Atmospheric pressure: ")
append(configuration.environment.atmosphericPressure.format(3))
append(" hPa")
append(" (${configuration.altitude.format(0)} meters)")
},
style = MaterialTheme.typography.bodySmall
)

val date =
Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
Text(
style = MaterialTheme.typography.bodySmall.copy(
fontStyle = FontStyle.Italic,
color = LocalTextStyle.current.color.copy(alpha = 0.8f)
),
modifier = Modifier.padding(top = 16.dp)
.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
text = "Created with Abysner for ${platform().humanReadable} ${VersionInfo.VERSION_NAME} (${VersionInfo.COMMIT_HASH})\non ${date.format(LocalDate.Formats.ISO)}"
)
}
}
}
}
}


@Preview
@Composable
private fun ShareImagePreview() {
ShareImage(
divePlan = PreviewData.divePlan,
settingsModel = SettingsModel(
showBasicDecoTable = true,
termsAndConditionsAccepted = true
)
)
}
Loading

0 comments on commit a0830cd

Please sign in to comment.