aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2023-09-26 19:36:27 -0600
committerIván Ávalos <avalos@disroot.org>2023-09-26 19:36:27 -0600
commit3e72d9b02e864f4f79ae494c67418031b7625659 (patch)
tree3d6d7ae3997b9372d50932e0e489dced12e5b07c
parent8765d82d3ad055945c6221e4f46bc38d903bf58d (diff)
parent3022b877d0cf9b7546c80953237c7bca5a4afa50 (diff)
downloadetbsa-trackermap-mobile-3e72d9b02e864f4f79ae494c67418031b7625659.tar.gz
etbsa-trackermap-mobile-3e72d9b02e864f4f79ae494c67418031b7625659.tar.bz2
etbsa-trackermap-mobile-3e72d9b02e864f4f79ae494c67418031b7625659.zip
Merge branch 'main' of https://git.sr.ht/~avalos/trackermap-mobile
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/TrackerApp.kt4
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt43
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginFragment.kt23
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginViewModel.kt20
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/MarkerTransformations.kt19
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt5
-rw-r--r--androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt14
-rw-r--r--androidApp/src/main/res/drawable/angle_0.xml22
-rw-r--r--androidApp/src/main/res/drawable/angle_135.xml22
-rw-r--r--androidApp/src/main/res/drawable/angle_180.xml22
-rw-r--r--androidApp/src/main/res/drawable/angle_225.xml22
-rw-r--r--androidApp/src/main/res/drawable/angle_270.xml22
-rw-r--r--androidApp/src/main/res/drawable/angle_315.xml22
-rw-r--r--androidApp/src/main/res/drawable/angle_45.xml22
-rw-r--r--androidApp/src/main/res/drawable/angle_90.xml22
-rw-r--r--androidApp/src/main/res/layout/login.xml249
-rw-r--r--androidApp/src/main/res/layout/offline_banner.xml15
-rw-r--r--androidApp/src/main/res/layout/units_activity.xml17
-rw-r--r--androidApp/src/main/res/values-es-rMX/strings.xml1
-rw-r--r--androidApp/src/main/res/values/dimen.xml4
-rw-r--r--androidApp/src/main/res/values/strings.xml1
-rw-r--r--iosApp/iosApp.xcodeproj/project.pbxproj29
-rw-r--r--iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme2
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle0.imageset/0 2.svg60
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle0.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle135.imageset/135.svg60
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle135.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle180.imageset/180.svg60
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle180.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle225.imageset/225.svg60
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle225.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle270.imageset/270.svg60
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle270.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle315.imageset/315.svg60
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle315.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle45.imageset/45.svg60
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle45.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle90.imageset/90.svg59
-rw-r--r--iosApp/iosApp/Assets.xcassets/Angle90.imageset/Contents.json21
-rw-r--r--iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift49
-rw-r--r--iosApp/iosApp/Map/MapViewController.swift48
-rw-r--r--iosApp/iosApp/Map/UnitMapView.swift4
-rw-r--r--iosApp/iosApp/Session/RootView.swift38
-rw-r--r--iosApp/iosApp/Session/RootViewModel.swift36
-rw-r--r--iosApp/iosApp/Shared/Constants.swift19
-rw-r--r--iosApp/iosApp/Shared/MarkerTransformations.swift19
-rw-r--r--iosApp/iosApp/Shared/OfflineBanner.swift27
-rw-r--r--iosApp/iosApp/Units/UnitsViewModel.swift14
-rw-r--r--iosApp/iosApp/en.lproj/Localizable.strings1
-rw-r--r--iosApp/iosApp/es-419.lproj/Localizable.strings1
-rw-r--r--iosApp/iosApp/iOSApp.swift9
-rw-r--r--shared/build.gradle.kts5
-rw-r--r--shared/src/androidMain/AndroidManifest.xml4
-rw-r--r--shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt55
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt2
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/SessionManager.kt2
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/UnitInformation.kt12
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt8
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/SessionController.kt18
-rw-r--r--shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt6
-rw-r--r--shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt25
61 files changed, 1443 insertions, 228 deletions
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/TrackerApp.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/TrackerApp.kt
index aa92c91..1bb921d 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/TrackerApp.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/TrackerApp.kt
@@ -28,6 +28,7 @@ import mx.trackermap.TrackerMap.android.units.UnitsViewModel
import mx.trackermap.TrackerMap.client.apis.*
import mx.trackermap.TrackerMap.client.infrastructure.SessionManager
import mx.trackermap.TrackerMap.controllers.*
+import mx.trackermap.TrackerMap.controllers.NetworkController
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.viewmodel.dsl.viewModel
@@ -56,7 +57,8 @@ open class TrackerApp : Application() {
factory { ReportsApi(get()) }
factory { GeofencesApi(get()) }
- factory { SessionController(get(), get()) }
+ factory { NetworkController(applicationContext) }
+ factory { SessionController(get(), get(), get()) }
factory { UnitsController(get(), get()) }
factory { GeofencesController(get()) }
factory { ReportController(get(), get()) }
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt
index 4915c49..2be9bb4 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt
@@ -40,6 +40,7 @@ import mx.trackermap.TrackerMap.client.models.Geofence
import mx.trackermap.TrackerMap.client.models.MapLayer
import mx.trackermap.TrackerMap.client.models.Marker
import mx.trackermap.TrackerMap.utils.MapCalculus
+import kotlin.math.atan2
typealias SetupCallback = () -> Unit
typealias MarkerCallback = (Int?) -> Unit
@@ -186,11 +187,14 @@ open class MapFragment : GlobeMapFragment() {
val colorReport = ContextCompat.getColor(requireContext(), R.color.colorReport)
val colorLabel = ContextCompat.getColor(requireContext(), R.color.colorMarkerLabel)
val colorLabelOutline = ContextCompat.getColor(requireContext(), R.color.colorMarkerLabelOutline)
- val vectorWidth = context?.resources?.getDimensionPixelSize(R.dimen.report_label_width)?.toFloat()
+ val vectorWidth = requireContext().resources.getDimensionPixelSize(R.dimen.report_label_width).toFloat()
+
+ val markerSize = requireContext().resources.getDimensionPixelSize(R.dimen.marker_size).toDouble()
+ val vertexSize = requireContext().resources.getDimensionPixelSize(R.dimen.vertex_size).toDouble()
val vectorInfo = VectorInfo()
- vectorInfo.setColor(colorReport)
- vectorInfo.setLineWidth(vectorWidth ?: 20f)
+ vectorInfo.color = colorReport
+ vectorInfo.lineWidth = vectorWidth
val labelInfo = LabelInfo()
labelInfo.typeface = Typeface.DEFAULT_BOLD
@@ -210,17 +214,17 @@ open class MapFragment : GlobeMapFragment() {
when (i) {
0 -> getIcon(Marker.Type.REPORT_START)
markers.size - 1 -> getIcon(Marker.Type.REPORT_END)
- else -> getIcon(Marker.Type.REPORT_POSITION)
+ else -> getIconForDirection(points[i], points[i + 1])
}
} else getIcon(marker.type)
screenMarker.size = if (isReport) {
// For reports, position, start and end, size must be different
when (i) {
- 0 -> Point2d(144.0, 144.0)
- markers.size - 1 -> Point2d(144.0, 144.0)
- else -> Point2d(82.0, 82.0)
+ 0 -> Point2d(markerSize, markerSize)
+ markers.size - 1 -> Point2d(markerSize, markerSize)
+ else -> Point2d(vertexSize, vertexSize)
}
- } else Point2d(144.0, 144.0)
+ } else Point2d(markerSize, markerSize)
screenMarker.userObject = marker.id
screenMarker.selectable = true
@@ -292,11 +296,11 @@ open class MapFragment : GlobeMapFragment() {
val colorFill = ContextCompat.getColor(requireContext(), R.color.colorGeofence)
val colorLabel = ContextCompat.getColor(requireContext(), R.color.colorGeofenceLabel)
val colorLabelOutline = ContextCompat.getColor(requireContext(), R.color.colorMarkerLabelOutline)
- val vectorWidth = context?.resources?.getDimensionPixelSize(R.dimen.geofence_label_width)?.toFloat()
+ val vectorWidth = requireContext().resources.getDimensionPixelSize(R.dimen.geofence_label_width).toFloat()
val vectorInfo = VectorInfo()
- vectorInfo.setColor(colorFill)
- vectorInfo.setLineWidth(vectorWidth ?: 4f)
+ vectorInfo.color = colorFill
+ vectorInfo.lineWidth = vectorWidth
val labelInfo = LabelInfo()
labelInfo.typeface = Typeface.DEFAULT_BOLD
@@ -334,7 +338,7 @@ open class MapFragment : GlobeMapFragment() {
}
}
}
- } catch (e: SFException) {}
+ } catch (_: SFException) {}
}
}
@@ -427,9 +431,22 @@ open class MapFragment : GlobeMapFragment() {
}
private fun getIcon(markerType: Marker.Type): Bitmap {
+ val markerSize = requireContext().resources.getDimensionPixelSize(R.dimen.marker_size)
return ResourcesCompat.getDrawable(
requireActivity().resources,
MarkerTransformations.markerTypeToResourceId(markerType),
- requireActivity().theme)!!.toBitmap(144, 144)
+ requireActivity().theme)!!.toBitmap(markerSize, markerSize)
+ }
+
+ private fun getIconForDirection(a: Point2d, b: Point2d): Bitmap {
+ val vertexSize = requireContext().resources.getDimensionPixelSize(R.dimen.vertex_size)
+ val vectorX = b.x - a.x
+ val vectorY = b.y - a.y
+ val angleRad = atan2(vectorY, vectorX)
+ return ResourcesCompat.getDrawable(
+ requireActivity().resources,
+ MarkerTransformations.angleToResourceId(angleRad),
+ requireActivity().theme
+ )!!.toBitmap(vertexSize, vertexSize)
}
} \ No newline at end of file
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginFragment.kt
index 6a30789..8879349 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginFragment.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginFragment.kt
@@ -67,7 +67,6 @@ class LoginFragment : Fragment() {
getString(R.string.default_server_url)
) ?: getString(R.string.default_server_url)
)
- loginViewModel.restoreSession()
}
override fun onStart() {
@@ -100,6 +99,26 @@ class LoginFragment : Fragment() {
}
private fun setupObservers() {
+ loginViewModel.networkAvailable.observe(viewLifecycleOwner) { available ->
+ Log.d("LoginFragment", "available = $available, session = ${loginViewModel.hasSession}")
+
+ binding.offlineBanner.root.visibility =
+ if (available == true) View.GONE else View.VISIBLE
+
+ binding.infoLoading.root.visibility = when (available) {
+ null -> View.VISIBLE
+ true -> if (loginViewModel.hasSession) {
+ loginViewModel.restoreSession()
+ View.VISIBLE
+ } else View.GONE
+ false -> if (loginViewModel.hasSession) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+ }
+
loginViewModel.loginState.observe(viewLifecycleOwner) { result ->
Log.d("LoginFragment", result.toString())
when (result) {
@@ -164,4 +183,4 @@ class LoginFragment : Fragment() {
const val PREFERENCE_SERVER_URL = "server_url"
const val PREFERENCE_TOKEN = "token"
}
-} \ No newline at end of file
+}
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginViewModel.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginViewModel.kt
index bcee2ec..434ac44 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginViewModel.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/session/LoginViewModel.kt
@@ -17,32 +17,46 @@
*/
package mx.trackermap.TrackerMap.android.session
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zhuinden.eventemitter.EventEmitter
import com.zhuinden.eventemitter.EventSource
import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import mx.trackermap.TrackerMap.client.models.SessionBody
+import mx.trackermap.TrackerMap.controllers.NetworkController
import mx.trackermap.TrackerMap.controllers.SessionController
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@DelicateCoroutinesApi
class LoginViewModel : ViewModel(), KoinComponent {
-
+ private val networkController: NetworkController by inject()
private val sessionController: SessionController by inject()
+ private val _networkAvailable = MutableLiveData<Boolean?>(null)
+ val networkAvailable: LiveData<Boolean?> = _networkAvailable
private val loginStateEmitter = EventEmitter<SessionController.LoginState>()
val loginState: EventSource<SessionController.LoginState> = loginStateEmitter
+ val hasSession: Boolean get() = sessionController.hasSession
init {
viewModelScope.launch {
+ setupNetworkObserver()
+ }
+ viewModelScope.launch {
setupLoginStateObserver()
}
}
+ private suspend fun setupNetworkObserver() {
+ networkController.networkAvailable.collect {
+ _networkAvailable.postValue(it)
+ }
+ }
+
private suspend fun setupLoginStateObserver() {
sessionController.loginStateFlow.collect {
it?.let {
@@ -52,7 +66,7 @@ class LoginViewModel : ViewModel(), KoinComponent {
}
fun restoreSession() {
- sessionController.restoreSession()
+ sessionController.getSession()
}
fun login(email: String, password: String, url: String, token: String?) {
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/MarkerTransformations.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/MarkerTransformations.kt
index 4dd1ea7..e99b8e6 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/MarkerTransformations.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/MarkerTransformations.kt
@@ -19,6 +19,7 @@ package mx.trackermap.TrackerMap.android.shared
import mx.trackermap.TrackerMap.android.R
import mx.trackermap.TrackerMap.client.models.Marker
+import kotlin.math.PI
object MarkerTransformations {
fun markerTypeToResourceId(markerType: Marker.Type): Int {
@@ -52,7 +53,7 @@ object MarkerTransformations {
}
}
- fun markerTypeToStringId(markerType: Marker.Type): Int {
+ private fun markerTypeToStringId(markerType: Marker.Type): Int {
return when (markerType) {
Marker.Type.ANIMAL -> R.string.unit_category_animal
Marker.Type.BACKHOE -> R.string.unit_category_backhoe
@@ -87,4 +88,20 @@ object MarkerTransformations {
fun categoryToStringId(category: String?): Int {
return markerTypeToStringId(Marker.categoryToMarkerType(category))
}
+
+ private const val STEP = PI / 8
+
+ @OptIn(ExperimentalStdlibApi::class)
+ fun angleToResourceId(rad: Double): Int = when (rad) {
+ in 0.0 ..< STEP -> R.drawable.angle_0
+ in STEP ..< STEP * 3 -> R.drawable.angle_45
+ in STEP * 3 ..< STEP * 5 -> R.drawable.angle_90
+ in STEP * 5 ..< STEP * 7 -> R.drawable.angle_135
+ in STEP * 7 ..< STEP * 9 -> R.drawable.angle_180
+ in STEP * 9 ..< STEP * 11 -> R.drawable.angle_225
+ in STEP * 11 ..< STEP * 13 -> R.drawable.angle_270
+ in STEP * 13 ..< STEP * 15 -> R.drawable.angle_315
+ in STEP * 15 ..< STEP * 16 -> R.drawable.angle_0
+ else -> angleToResourceId(PI * 2 + rad)
+ }
} \ No newline at end of file
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt
index 8a161c3..443a205 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt
@@ -28,7 +28,6 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.TooltipCompat
-import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.commit
import kotlinx.coroutines.DelicateCoroutinesApi
import mx.trackermap.TrackerMap.android.R
@@ -150,6 +149,10 @@ class UnitsActivity : AppCompatActivity() {
}
private fun setupObservers() {
+ unitsViewModel.networkAvailable.observe(this) { available ->
+ binding.offlineBanner.root.visibility =
+ if (available == true) View.GONE else View.VISIBLE
+ }
unitsViewModel.unitsDisplayMode.observe(this) { displayMode ->
binding.displayModeToggle.setImageResource(
when (displayMode) {
diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt
index b5ed78d..05962e9 100644
--- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt
+++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsViewModel.kt
@@ -27,6 +27,7 @@ import mx.trackermap.TrackerMap.client.models.Geofence
import mx.trackermap.TrackerMap.client.models.MapLayer
import mx.trackermap.TrackerMap.client.models.UnitInformation
import mx.trackermap.TrackerMap.controllers.GeofencesController
+import mx.trackermap.TrackerMap.controllers.NetworkController
import mx.trackermap.TrackerMap.controllers.UnitsController
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -48,9 +49,11 @@ class UnitsViewModel(
val animated: Boolean,
)
+ private val networkController: NetworkController by inject()
private val unitsController: UnitsController by inject()
private val geofenceController: GeofencesController by inject()
+ private var _networkAvailable = MutableLiveData<Boolean?>()
private var _searchQuery = savedStateHandle.getLiveData("searchQuery", "")
private var _unitsDisplayMode = MutableLiveData(UnitsDisplayMode.MAP)
private var _units = MutableLiveData<List<UnitInformation>>()
@@ -61,6 +64,7 @@ class UnitsViewModel(
private var _geofences = MutableLiveData<Map<Int, Geofence>>()
private val _camera = MutableLiveData<Camera?>()
+ val networkAvailable: LiveData<Boolean?> get() = _networkAvailable
val searchQuery: LiveData<String> get() = _searchQuery
val unitsDisplayMode: LiveData<UnitsDisplayMode> get() = _unitsDisplayMode
val units: LiveData<List<UnitInformation>> get() = _units
@@ -75,6 +79,9 @@ class UnitsViewModel(
Log.d("UnitsViewModel", "Initializing Units View Model")
unitsController.fetchUnits(viewModelScope)
viewModelScope.launch {
+ setupNetworkObserver()
+ }
+ viewModelScope.launch {
setupUnitsObserver()
}
viewModelScope.launch {
@@ -86,6 +93,13 @@ class UnitsViewModel(
}
}
+ private suspend fun setupNetworkObserver() {
+ networkController.networkAvailable.collect { available ->
+ Log.d("UnitsViewModel", "Collecting network state")
+ _networkAvailable.value = available
+ }
+ }
+
private suspend fun setupUnitsObserver() {
(unitsController.displayedUnitsFlow as StateFlow<List<UnitInformation>>).collect { units ->
Log.d("UnitsViewModel", "Collecting units")
diff --git a/androidApp/src/main/res/drawable/angle_0.xml b/androidApp/src/main/res/drawable/angle_0.xml
new file mode 100644
index 0000000..53957d9
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_0.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M3.1751,6.106A2.9308,2.9308 0,0 1,0.2443 3.1752,2.9308 2.9308,0 0,1 3.1751,0.2444 2.9308,2.9308 0,0 1,6.1059 3.1752,2.9308 2.9308,0 0,1 3.1751,6.106Z"
+ android:strokeWidth="1.00012"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M6.3501,3.176C6.3501,1.4254 4.9249,0.0002 3.1743,0.0002c-1.7506,0 -3.1738,1.4252 -3.1738,3.1758 -0,1.7506 1.4232,3.1738 3.1738,3.1738C4.9249,6.3498 6.3501,4.9266 6.3501,3.176ZM5.8618,3.176c-0,1.4866 -1.2009,2.6855 -2.6875,2.6855 -1.4866,0 -2.6855,-1.1989 -2.6855,-2.6855 -0,-1.4866 1.1989,-2.6875 2.6855,-2.6875 1.4866,0 2.6875,1.2009 2.6875,2.6875z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M5.8618,3.1757 L2.9355,1.4857V2.5831L0.2443,2.5826c0,0 -0,0.3951 -0,0.5926 0,0.1972 0,0.5917 0,0.5917l2.6912,0.0005v1.0974z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/drawable/angle_135.xml b/androidApp/src/main/res/drawable/angle_135.xml
new file mode 100644
index 0000000..cd1b88e
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_135.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M5.2479,1.1026A2.9308,2.9308 90,0 1,5.2479 5.2474,2.9308 2.9308,90 0,1 1.1032,5.2474 2.9308,2.9308 0,0 1,1.1032 1.1026,2.9308 2.9308,0 0,1 5.2479,1.1026Z"
+ android:strokeWidth="1.00012"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M0.9311,0.9294C-0.3068,2.1673 -0.3068,4.1828 0.9311,5.4206c1.2379,1.2379 3.252,1.2365 4.4899,-0.0014 1.2379,-1.2379 1.2379,-3.2506 0,-4.4885C4.183,-0.3071 2.1689,-0.3085 0.9311,0.9294ZM1.2763,1.2747c1.0512,-1.0512 2.7481,-1.0498 3.7993,0.0014 1.0512,1.0512 1.0512,2.7467 -0,3.7979 -1.0512,1.0512 -2.7481,1.0526 -3.7993,0.0014 -1.0512,-1.0512 -1.0512,-2.7495 0,-3.8007z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M1.2761,1.2749 L2.1503,4.5391 2.9263,3.7631 4.8289,5.6664c0,0 0.2793,-0.2793 0.419,-0.419C5.3874,5.1079 5.6663,4.829 5.6663,4.829L3.7637,2.9257 4.5396,2.1498Z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/drawable/angle_180.xml b/androidApp/src/main/res/drawable/angle_180.xml
new file mode 100644
index 0000000..f3fca08
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_180.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M3.1755,0.244A2.9308,2.9308 0,0 1,6.1063 3.1748,2.9308 2.9308,0 0,1 3.1755,6.1056 2.9308,2.9308 0,0 1,0.2447 3.1748,2.9308 2.9308,0 0,1 3.1755,0.244Z"
+ android:strokeWidth="1.00012"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M0.0005,3.174C0.0005,4.9246 1.4257,6.3498 3.1763,6.3498c1.7506,-0 3.1738,-1.4252 3.1738,-3.1758 0,-1.7506 -1.4232,-3.1738 -3.1738,-3.1738C1.4257,0.0002 0.0005,1.4234 0.0005,3.174ZM0.4888,3.174c0,-1.4866 1.2009,-2.6855 2.6875,-2.6855 1.4866,-0 2.6855,1.1989 2.6855,2.6855 0,1.4866 -1.1989,2.6875 -2.6855,2.6875 -1.4866,-0 -2.6875,-1.2009 -2.6875,-2.6875z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M0.4888,3.1744 L3.4151,4.8643V3.7669l2.6912,0.0005c0,0 0,-0.3951 0,-0.5926 0,-0.1972 0,-0.5917 0,-0.5917L3.4151,2.5827V1.4853Z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/drawable/angle_225.xml b/androidApp/src/main/res/drawable/angle_225.xml
new file mode 100644
index 0000000..bd7c512
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_225.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M1.1036,1.1031A2.9308,2.9308 90,0 1,5.2484 1.1031,2.9308 2.9308,90 0,1 5.2484,5.2478 2.9308,2.9308 0,0 1,1.1036 5.2478,2.9308 2.9308,0 0,1 1.1036,1.1031Z"
+ android:strokeWidth="1.00012"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M0.9304,5.4199C2.1682,6.6578 4.1837,6.6578 5.4216,5.4199c1.2379,-1.2379 1.2365,-3.252 -0.0014,-4.4899 -1.2379,-1.2379 -3.2506,-1.2379 -4.4885,-0C-0.3061,2.1679 -0.3075,4.1821 0.9304,5.4199ZM1.2756,5.0747c-1.0512,-1.0512 -1.0498,-2.7481 0.0014,-3.7993 1.0512,-1.0512 2.7467,-1.0512 3.7979,0 1.0512,1.0512 1.0526,2.7481 0.0014,3.7993 -1.0512,1.0512 -2.7495,1.0512 -3.8007,-0z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M1.2759,5.0749 L4.54,4.2007 3.7641,3.4247 5.6674,1.5221c0,0 -0.2793,-0.2793 -0.419,-0.419C5.1089,0.9636 4.83,0.6847 4.83,0.6847L2.9267,2.5873 2.1508,1.8114Z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/drawable/angle_270.xml b/androidApp/src/main/res/drawable/angle_270.xml
new file mode 100644
index 0000000..19db1cf
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_270.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M0.2455,3.1753A2.9308,2.9308 0,0 1,3.1763 0.2445,2.9308 2.9308,0 0,1 6.107,3.1753 2.9308,2.9308 0,0 1,3.1763 6.1061,2.9308 2.9308,0 0,1 0.2455,3.1753Z"
+ android:strokeWidth="1.00012"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M3.1755,6.3503C4.9261,6.3503 6.3513,4.9251 6.3513,3.1745c-0,-1.7506 -1.4252,-3.1738 -3.1758,-3.1738 -1.7506,-0 -3.1738,1.4232 -3.1738,3.1738C0.0017,4.9251 1.4249,6.3503 3.1755,6.3503ZM3.1755,5.862c-1.4866,-0 -2.6855,-1.2009 -2.6855,-2.6875 -0,-1.4866 1.1989,-2.6855 2.6855,-2.6855 1.4866,-0 2.6875,1.1989 2.6875,2.6855 -0,1.4866 -1.2009,2.6875 -2.6875,2.6875z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="m3.1758,5.862 l1.6899,-2.9263 -1.0974,0 0.0005,-2.6912c0,0 -0.3951,0 -0.5926,-0 -0.1972,0 -0.5917,0 -0.5917,0l-0.0005,2.6912 -1.0974,0z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/drawable/angle_315.xml b/androidApp/src/main/res/drawable/angle_315.xml
new file mode 100644
index 0000000..3186db0
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_315.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M1.1038,5.2479A2.9308,2.9308 90,0 1,1.1038 1.1031,2.9308 2.9308,0 0,1 5.2486,1.1031 2.9308,2.9308 0,0 1,5.2486 5.2479,2.9308 2.9308,90 0,1 1.1038,5.2479Z"
+ android:strokeWidth="1.00012"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M5.4207,5.4211C6.6586,4.1832 6.6586,2.1677 5.4207,0.9299c-1.2379,-1.2379 -3.252,-1.2365 -4.4899,0.0014 -1.2379,1.2379 -1.2379,3.2506 0,4.4885C2.1687,6.6576 4.1828,6.659 5.4207,5.4211ZM5.0754,5.0758c-1.0512,1.0512 -2.7481,1.0498 -3.7993,-0.0014 -1.0512,-1.0512 -1.0512,-2.7467 -0,-3.7979 1.0512,-1.0512 2.7481,-1.0526 3.7993,-0.0014 1.0512,1.0512 1.0512,2.7495 0,3.8007z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M5.0757,5.0756 L4.2014,1.8114 3.4255,2.5874 1.5228,0.6841c0,0 -0.2793,0.2793 -0.419,0.419C0.9644,1.2426 0.6855,1.5215 0.6855,1.5215L2.5881,3.4248 1.8121,4.2007Z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/drawable/angle_45.xml b/androidApp/src/main/res/drawable/angle_45.xml
new file mode 100644
index 0000000..9d8b5a9
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_45.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M5.2467,5.2476A2.9308,2.9308 90,0 1,1.1019 5.2476,2.9308 2.9308,0 0,1 1.1019,1.1029 2.9308,2.9308 0,0 1,5.2467 1.1029,2.9308 2.9308,90 0,1 5.2467,5.2476Z"
+ android:strokeWidth="1.00012"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M5.4199,0.9308C4.1821,-0.3071 2.1666,-0.3071 0.9287,0.9308c-1.2379,1.2379 -1.2365,3.252 0.0014,4.4899 1.2379,1.2379 3.2506,1.2379 4.4885,-0C6.6564,4.1828 6.6578,2.1686 5.4199,0.9308ZM5.0747,1.276c1.0512,1.0512 1.0498,2.7481 -0.0014,3.7993 -1.0512,1.0512 -2.7467,1.0512 -3.7979,0 -1.0512,-1.0512 -1.0526,-2.7481 -0.0014,-3.7993 1.0512,-1.0512 2.7495,-1.0512 3.8007,-0z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M5.0744,1.2758 L1.8103,2.1501 2.5862,2.926 0.8061,4.7067 1.2251,5.1257 1.6435,5.5441 3.4236,3.7634 4.1995,4.5393Z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/drawable/angle_90.xml b/androidApp/src/main/res/drawable/angle_90.xml
new file mode 100644
index 0000000..44dae03
--- /dev/null
+++ b/androidApp/src/main/res/drawable/angle_90.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="6.35"
+ android:viewportHeight="6.35">
+ <path
+ android:pathData="M6.1058,3.175A2.9308,2.9308 0,0 1,3.175 6.1058,2.9308 2.9308,0 0,1 0.2442,3.175 2.9308,2.9308 0,0 1,3.175 0.2442,2.9308 2.9308,0 0,1 6.1058,3.175Z"
+ android:strokeWidth="1.00012499"
+ android:fillColor="#008000"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M3.1758,0C1.4252,0 0,1.4252 0,3.1758c0,1.7506 1.4252,3.1738 3.1758,3.1738 1.7506,0 3.1738,-1.4232 3.1738,-3.1738C6.3496,1.4252 4.9264,0 3.1758,0ZM3.1758,0.4883c1.4866,0 2.6855,1.2009 2.6855,2.6875 0,1.4866 -1.1989,2.6855 -2.6855,2.6855 -1.4866,0 -2.6875,-1.1989 -2.6875,-2.6855 0,-1.4866 1.2009,-2.6875 2.6875,-2.6875z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+ <path
+ android:pathData="M3.1755,0.4883 L1.4855,3.4146H2.5829V5.9667H3.1755,3.7671V3.4146h1.0974z"
+ android:strokeWidth="0"
+ android:fillColor="#1a1a1a"
+ android:strokeColor="#1a1a1a"
+ android:strokeLineCap="square"/>
+</vector>
diff --git a/androidApp/src/main/res/layout/login.xml b/androidApp/src/main/res/layout/login.xml
index 1de3f12..ec3925c 100644
--- a/androidApp/src/main/res/layout/login.xml
+++ b/androidApp/src/main/res/layout/login.xml
@@ -1,132 +1,151 @@
<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:name="mx.trackermap.TrackerMap.android.session.LoginFragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <androidx.constraintlayout.widget.ConstraintLayout
+<androidx.constraintlayout.widget.ConstraintLayout
+ android:name="mx.trackermap.TrackerMap.android.session.LoginFragment"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <androidx.appcompat.widget.AppCompatImageView
- android:id="@+id/bannerImage"
- android:layout_width="0dp"
- android:layout_height="@dimen/about_logo_height"
- app:layout_constraintWidth_percent="0.5"
- app:layout_constraintDimensionRatio="1"
+ <include
+ android:id="@+id/offlineBanner"
+ layout="@layout/offline_banner"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- android:layout_marginHorizontal="@dimen/card_margin"
- app:srcCompat="@drawable/about_logo" />
-
- <androidx.cardview.widget.CardView
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:cardCornerRadius="@dimen/card_border_radius"
- app:cardElevation="@dimen/card_elevation"
- app:cardUseCompatPadding="true"
- app:layout_constraintTop_toBottomOf="@id/bannerImage"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- android:layout_marginHorizontal="@dimen/card_margin"
- android:layout_marginBottom="16dp">
+ android:visibility="gone"
+ tools:visibility="visible" />
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toBottomOf="@id/offlineBanner"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/bannerImage"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/about_logo_height"
+ app:layout_constraintWidth_percent="0.5"
+ app:layout_constraintDimensionRatio="1"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginHorizontal="@dimen/card_margin"
+ app:srcCompat="@drawable/about_logo" />
+
+ <androidx.cardview.widget.CardView
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:paddingVertical="@dimen/card_large_padding"
- android:paddingHorizontal="@dimen/card_padding">
-
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/usernameInputLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
+ app:cardCornerRadius="@dimen/card_border_radius"
+ app:cardElevation="@dimen/card_elevation"
+ app:cardUseCompatPadding="true"
+ app:layout_constraintTop_toBottomOf="@id/bannerImage"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginHorizontal="@dimen/card_margin"
+ android:layout_marginBottom="16dp">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:hint="@string/login_username"
- app:layout_constraintBottom_toTopOf="@+id/passwordInputLayout"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent">
-
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/usernameEditText"
+ android:paddingVertical="@dimen/card_large_padding"
+ android:paddingHorizontal="@dimen/card_padding">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/usernameInputLayout"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:inputType="textEmailAddress"/>
-
- </com.google.android.material.textfield.TextInputLayout>
-
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/passwordInputLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:hint="@string/login_password"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/usernameInputLayout"
- app:layout_constraintBottom_toTopOf="@id/urlInputLayout"
- android:layout_marginTop="@dimen/fields_spacing">
-
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/passwordEditText"
+ android:hint="@string/login_username"
+ app:layout_constraintBottom_toTopOf="@+id/passwordInputLayout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/usernameEditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textEmailAddress"/>
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/passwordInputLayout"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:inputType="textPassword"/>
-
- </com.google.android.material.textfield.TextInputLayout>
-
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/urlInputLayout"
- style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:hint="@string/login_url"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/passwordInputLayout"
- app:layout_constraintBottom_toTopOf="@id/signinButton"
- android:layout_marginTop="@dimen/fields_spacing"
- android:visibility="gone">
-
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/urlEditText"
+ android:hint="@string/login_password"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/usernameInputLayout"
+ app:layout_constraintBottom_toTopOf="@id/urlInputLayout"
+ android:layout_marginTop="@dimen/fields_spacing">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/passwordEditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textPassword"/>
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/urlInputLayout"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:inputType="textUri"/>
-
- </com.google.android.material.textfield.TextInputLayout>
-
- <com.google.android.material.button.MaterialButton
- android:id="@+id/signinButton"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_constraintWidth_percent="0.5"
- app:layout_constraintTop_toBottomOf="@id/urlInputLayout"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="0.5"
- android:layout_marginTop="@dimen/fields_large_spacing"
- android:text="@string/login_login"/>
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- </androidx.cardview.widget.CardView>
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- <include
- android:id="@+id/infoLoading"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- layout="@layout/loading_indicator"
- android:visibility="gone"/>
-
-</FrameLayout> \ No newline at end of file
+ android:hint="@string/login_url"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/passwordInputLayout"
+ app:layout_constraintBottom_toTopOf="@id/signinButton"
+ android:layout_marginTop="@dimen/fields_spacing"
+ android:visibility="gone">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/urlEditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textUri"/>
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/signinButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintWidth_percent="0.5"
+ app:layout_constraintTop_toBottomOf="@id/urlInputLayout"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ android:layout_marginTop="@dimen/fields_large_spacing"
+ android:text="@string/login_login"/>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </androidx.cardview.widget.CardView>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <include
+ android:id="@+id/infoLoading"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ layout="@layout/loading_indicator"
+ android:visibility="gone"/>
+
+ </FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/androidApp/src/main/res/layout/offline_banner.xml b/androidApp/src/main/res/layout/offline_banner.xml
new file mode 100644
index 0000000..6710d08
--- /dev/null
+++ b/androidApp/src/main/res/layout/offline_banner.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="10dp"
+ android:background="@color/colorAccent">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:text="@string/login_no_internet"
+ android:textColor="@android:color/white" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/androidApp/src/main/res/layout/units_activity.xml b/androidApp/src/main/res/layout/units_activity.xml
index 60a77db..8f46516 100644
--- a/androidApp/src/main/res/layout/units_activity.xml
+++ b/androidApp/src/main/res/layout/units_activity.xml
@@ -15,7 +15,16 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <include
+ android:id="@+id/offlineBanner"
+ layout="@layout/offline_banner"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:visibility="gone"
+ tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/displayModeToggle"
@@ -28,7 +37,7 @@
app:elevation="@dimen/fab_elevation"
app:fabSize="mini"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/offlineBanner"
tools:ignore="ContentDescription" />
<com.google.android.material.card.MaterialCardView
@@ -83,7 +92,7 @@
app:elevation="@dimen/fab_elevation"
app:fabSize="mini"
app:layout_constraintEnd_toStartOf="@id/userButton"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toBottomOf="@id/offlineBanner" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/userButton"
@@ -98,6 +107,6 @@
app:elevation="@dimen/fab_elevation"
app:fabSize="mini"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
+ app:layout_constraintTop_toBottomOf="@id/offlineBanner" />
</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/androidApp/src/main/res/values-es-rMX/strings.xml b/androidApp/src/main/res/values-es-rMX/strings.xml
index 53e74a2..2b9994a 100644
--- a/androidApp/src/main/res/values-es-rMX/strings.xml
+++ b/androidApp/src/main/res/values-es-rMX/strings.xml
@@ -19,6 +19,7 @@
<string name="login_password_missing">Falta la contraseña</string>
<string name="login_url_missing">Falta la URL del servidor</string>
<string name="login_login_failed">Falló el inicio de sesión</string>
+ <string name="login_no_internet">No hay conexión a internet</string>
<!-- UnitsActivity -->
<string name="toggle_list">Cambiar a lista de dispositivos</string>
diff --git a/androidApp/src/main/res/values/dimen.xml b/androidApp/src/main/res/values/dimen.xml
index 8c8c5f5..4afaf23 100644
--- a/androidApp/src/main/res/values/dimen.xml
+++ b/androidApp/src/main/res/values/dimen.xml
@@ -29,10 +29,12 @@
<dimen name="nav_height">64dp</dimen>
<!-- Map -->
+ <dimen name="marker_size">55dp</dimen>
+ <dimen name="vertex_size">24dp</dimen>
<dimen name="marker_label_text_size">11sp</dimen>
<dimen name="geofence_label_text_size">11sp</dimen>
<dimen name="geofence_label_width">4dp</dimen>
- <dimen name="report_label_width">10dp</dimen>
+ <dimen name="report_label_width">12dp</dimen>
<dimen name="attribution_text_size">11sp</dimen>
<!-- Reports bottom sheet -->
diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml
index 3177ad6..3dacd7a 100644
--- a/androidApp/src/main/res/values/strings.xml
+++ b/androidApp/src/main/res/values/strings.xml
@@ -33,6 +33,7 @@
<string name="login_password_missing">Password is missing</string>
<string name="login_url_missing">Server URL is missing</string>
<string name="login_login_failed">Login failed</string>
+ <string name="login_no_internet">No internet connection</string>
<!-- UnitsActivity -->
<string name="toggle_list">Switch to device list</string>
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index 031c33e..471e3b0 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 52;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -30,6 +30,7 @@
E36A5A8627B4BFC40070DED5 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = E36A5A8527B4BFC40070DED5 /* FirebaseMessaging */; };
E36DF77B27AB740C003C561C /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36DF77927AB740C003C561C /* MapViewController.swift */; };
E36DF77C27AB740C003C561C /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E36DF77A27AB740C003C561C /* MapViewController.xib */; };
+ E37F5CA92ABD26DE00EA3234 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E37F5CA82ABD26DE00EA3234 /* Constants.swift */; };
E38F241527A242870069FC45 /* Inject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241427A242870069FC45 /* Inject.swift */; };
E38F241727A242C70069FC45 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241627A242C70069FC45 /* Resolver.swift */; };
E38F241C27A26DD70069FC45 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241B27A26DD70069FC45 /* RootViewModel.swift */; };
@@ -51,6 +52,7 @@
E3C651E727CB5426002F6F4C /* Tabler in Frameworks */ = {isa = PBXBuildFile; productRef = E3C651E627CB5426002F6F4C /* Tabler */; };
E3C651EA27CB61EE002F6F4C /* GEOSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E3C651E927CB61EE002F6F4C /* GEOSwift */; };
E3E77EE6279E6CE400150070 /* FlowCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E77EE5279E6CE400150070 /* FlowCollector.swift */; };
+ E3F5F2792AB8080E008A47A7 /* OfflineBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3F5F2782AB8080E008A47A7 /* OfflineBanner.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -65,14 +67,14 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
- E3B5741D27F6A3C70018AFCF /* Embed App Extensions */ = {
+ E3B5741D27F6A3C70018AFCF /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
);
- name = "Embed App Extensions";
+ name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
@@ -103,6 +105,7 @@
E36A5A8927B4C8BB0070DED5 /* iosApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iosApp.entitlements; sourceTree = "<group>"; };
E36DF77927AB740C003C561C /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; };
E36DF77A27AB740C003C561C /* MapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MapViewController.xib; sourceTree = "<group>"; };
+ E37F5CA82ABD26DE00EA3234 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
E38F241427A242870069FC45 /* Inject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inject.swift; sourceTree = "<group>"; };
E38F241627A242C70069FC45 /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
E38F241B27A26DD70069FC45 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
@@ -121,6 +124,7 @@
E3B5740727F68F5F0018AFCF /* XlsxFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XlsxFile.swift; sourceTree = "<group>"; };
E3B5740927F69F750018AFCF /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
E3E77EE5279E6CE400150070 /* FlowCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCollector.swift; sourceTree = "<group>"; };
+ E3F5F2782AB8080E008A47A7 /* OfflineBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineBanner.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -287,6 +291,8 @@
E31D721427CF159900CDA320 /* SidewaysScroller.swift */,
E3B5740727F68F5F0018AFCF /* XlsxFile.swift */,
E3B5740927F69F750018AFCF /* ShareViewController.swift */,
+ E3F5F2782AB8080E008A47A7 /* OfflineBanner.swift */,
+ E37F5CA82ABD26DE00EA3234 /* Constants.swift */,
);
path = Shared;
sourceTree = "<group>";
@@ -303,7 +309,7 @@
7555FF78242A565900829871 /* Frameworks */,
7555FF79242A565900829871 /* Resources */,
E33A236F27A7545500DD647F /* Embed Frameworks */,
- E3B5741D27F6A3C70018AFCF /* Embed App Extensions */,
+ E3B5741D27F6A3C70018AFCF /* Embed Foundation Extensions */,
);
buildRules = (
);
@@ -326,8 +332,9 @@
7555FF73242A565900829871 /* Project object */ = {
isa = PBXProject;
attributes = {
+ BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1320;
- LastUpgradeCheck = 1130;
+ LastUpgradeCheck = 1430;
ORGANIZATIONNAME = orgName;
TargetAttributes = {
7555FF7A242A565900829871 = {
@@ -420,11 +427,13 @@
E3385A4F27B0D8A10025311C /* UnitCommandsViewModel.swift in Sources */,
E392BE1727B77B7E002698F3 /* AppDelegate.swift in Sources */,
E36DF77B27AB740C003C561C /* MapViewController.swift in Sources */,
+ E3F5F2792AB8080E008A47A7 /* OfflineBanner.swift in Sources */,
E33A237327A7581A00DD647F /* Utils.swift in Sources */,
E3E77EE6279E6CE400150070 /* FlowCollector.swift in Sources */,
E33A236527A530F300DD647F /* SmallLabelStyle.swift in Sources */,
E39ABC4327A4E88C00965D05 /* UnitsViewModel.swift in Sources */,
E360251B27BCA8A600958B21 /* AccountViewModel.swift in Sources */,
+ E37F5CA92ABD26DE00EA3234 /* Constants.swift in Sources */,
E34A2F4827A7878200AD8AEB /* HyperlinkText.swift in Sources */,
E3B5740827F68F5F0018AFCF /* XlsxFile.swift in Sources */,
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
@@ -456,6 +465,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -517,6 +527,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -577,7 +588,7 @@
CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = 358YRZ9P3L;
ENABLE_PREVIEWS = YES;
@@ -593,7 +604,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.4;
+ MARKETING_VERSION = 1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
@@ -618,7 +629,7 @@
CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = 358YRZ9P3L;
ENABLE_PREVIEWS = YES;
@@ -634,7 +645,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.4;
+ MARKETING_VERSION = 1.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-framework",
diff --git a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme
index 5b29c0a..9ece2b5 100644
--- a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme
+++ b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
- LastUpgradeVersion = "1320"
+ LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
diff --git a/iosApp/iosApp/Assets.xcassets/Angle0.imageset/0 2.svg b/iosApp/iosApp/Assets.xcassets/Angle0.imageset/0 2.svg
new file mode 100644
index 0000000..efe814f
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle0.imageset/0 2.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="0.svg"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="15.12"
+ inkscape:cx="13.85582"
+ inkscape:cy="17.195767"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012;stroke-dasharray:none"
+ transform="rotate(90,3.1749477,3.17515)">
+ <path
+ style="color:#000000;fill:#008000;stroke-width:1.00012;stroke-linecap:square;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 5.8618164,3.1756595 2.9355056,1.4857149 V 2.5830814 L 0.24432832,2.5826243 c 0,0 -2e-8,0.395052 -2e-8,0.592578 0,0.1972212 0,0.5916636 0,0.5916636 l 2.6911773,4.572e-4 v 1.0973665 z"
+ sodipodi:nodetypes="ccccscccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle0.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle0.imageset/Contents.json
new file mode 100644
index 0000000..be69468
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle0.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "0 2.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Angle135.imageset/135.svg b/iosApp/iosApp/Assets.xcassets/Angle135.imageset/135.svg
new file mode 100644
index 0000000..a7c625b
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle135.imageset/135.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="135.svg"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="21.382909"
+ inkscape:cx="8.6283863"
+ inkscape:cy="10.896553"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012;stroke-dasharray:none"
+ transform="rotate(-45,3.175293,3.1743164)">
+ <path
+ style="color:#000000;fill:#008000;stroke-width:1.00012;stroke-linecap:square;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 1.2760955,1.2748867 2.1503383,4.5390721 2.9262936,3.7631168 4.82892,5.6663897 c 0,0 0.279344,-0.279344 0.419016,-0.4190159 C 5.3873924,5.1079173 5.6663053,4.8290044 5.6663053,4.8290044 L 3.763679,2.9257315 4.5396343,2.1497762 Z"
+ sodipodi:nodetypes="ccccscccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle135.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle135.imageset/Contents.json
new file mode 100644
index 0000000..1e01d4c
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle135.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "135.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Angle180.imageset/180.svg b/iosApp/iosApp/Assets.xcassets/Angle180.imageset/180.svg
new file mode 100644
index 0000000..4df8b75
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle180.imageset/180.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="180.svg"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="21.382909"
+ inkscape:cx="8.6283863"
+ inkscape:cy="10.896553"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012;stroke-dasharray:none"
+ transform="rotate(-90,3.17515,3.1746617)">
+ <path
+ style="color:#000000;fill:#008000;stroke-width:1.00012;stroke-linecap:square;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 0.4887696,3.1743545 3.4150803,4.8642991 V 3.7669326 l 2.6911772,4.571e-4 c 0,0 0,-0.395052 0,-0.592578 0,-0.1972212 0,-0.5916636 0,-0.5916636 L 3.4150803,2.5826909 V 1.4853244 Z"
+ sodipodi:nodetypes="ccccscccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle180.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle180.imageset/Contents.json
new file mode 100644
index 0000000..cca2175
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle180.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "180.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Angle225.imageset/225.svg b/iosApp/iosApp/Assets.xcassets/Angle225.imageset/225.svg
new file mode 100644
index 0000000..35d4080
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle225.imageset/225.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="225.svg"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="21.382909"
+ inkscape:cx="8.6283863"
+ inkscape:cy="10.896553"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012;stroke-dasharray:none"
+ transform="rotate(-135,3.175579,3.175007)">
+ <path
+ style="color:#000000;fill:#008000;stroke-width:1.00012;stroke-linecap:square;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 1.2758633,5.074895 4.5400487,4.2006522 3.7640934,3.4246969 5.6673663,1.5220705 c 0,0 -0.279344,-0.279344 -0.4190159,-0.419016 C 5.1088939,0.96359809 4.829981,0.68468519 4.829981,0.68468519 L 2.9267081,2.5873115 2.1507528,1.8113562 Z"
+ sodipodi:nodetypes="ccccscccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle225.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle225.imageset/Contents.json
new file mode 100644
index 0000000..1b49b8a
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle225.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "225.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Angle270.imageset/270.svg b/iosApp/iosApp/Assets.xcassets/Angle270.imageset/270.svg
new file mode 100644
index 0000000..23562a0
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle270.imageset/270.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="270.svg"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="21.382909"
+ inkscape:cx="8.6283863"
+ inkscape:cy="10.896553"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012;stroke-dasharray:none"
+ transform="rotate(-180,3.1756382,3.17515)">
+ <path
+ style="color:#000000;fill:#008000;stroke-width:1.00012;stroke-linecap:square;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="m 3.1758193,5.8620185 1.6899446,-2.9263106 -1.0973665,0 4.571e-4,-2.6911772 c 0,0 -0.395052,0 -0.592578,-7e-8 -0.1972212,7e-8 -0.5916636,7e-8 -0.5916636,7e-8 l -4.572e-4,2.6911771 -1.0973665,0 z"
+ sodipodi:nodetypes="ccccscccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle270.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle270.imageset/Contents.json
new file mode 100644
index 0000000..9eab5a3
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle270.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "270.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Angle315.imageset/315.svg b/iosApp/iosApp/Assets.xcassets/Angle315.imageset/315.svg
new file mode 100644
index 0000000..a501605
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle315.imageset/315.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="315.svg"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="21.382909"
+ inkscape:cx="8.6283863"
+ inkscape:cy="11.972178"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012;stroke-dasharray:none"
+ transform="rotate(135,3.1754952,3.1754952)">
+ <path
+ style="color:#000000;fill:#008000;stroke-width:1.00012;stroke-linecap:square;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 5.0756692,5.0756154 4.2014264,1.81143 3.4254711,2.5873853 1.5228447,0.68411246 c 0,0 -0.279344,0.27934395 -0.419016,0.41901584 C 0.96437228,1.2425848 0.68545938,1.5214977 0.68545938,1.5214977 L 2.5880857,3.4247706 1.8121304,4.2007259 Z"
+ sodipodi:nodetypes="ccccscccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle315.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle315.imageset/Contents.json
new file mode 100644
index 0000000..fed8257
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle315.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "315.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Angle45.imageset/45.svg b/iosApp/iosApp/Assets.xcassets/Angle45.imageset/45.svg
new file mode 100644
index 0000000..1ce060e
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle45.imageset/45.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="45.svg"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="15.12"
+ inkscape:cx="13.062169"
+ inkscape:cy="10.780423"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012;stroke-dasharray:none"
+ transform="rotate(45,3.1743164,3.1743164)">
+ <path
+ style="color:#000000;fill:#008000;stroke-width:1.00012;stroke-linecap:square;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 5.0744368,1.2758094 1.8102513,2.1500524 2.5862066,2.9260077 0.80607031,4.7066972 1.2250863,5.1257132 1.6434556,5.5440825 3.4235919,3.7633929 4.1995472,4.5393483 Z"
+ sodipodi:nodetypes="ccccccccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle45.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle45.imageset/Contents.json
new file mode 100644
index 0000000..a785b47
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle45.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "45.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Angle90.imageset/90.svg b/iosApp/iosApp/Assets.xcassets/Angle90.imageset/90.svg
new file mode 100644
index 0000000..de994b5
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle90.imageset/90.svg
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 6.3499999 6.35"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+ sodipodi:docname="90.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ inkscape:zoom="15.12"
+ inkscape:cx="17.956349"
+ inkscape:cy="11.177249"
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <g
+ id="path1"
+ style="stroke-width:1.00012499;stroke-dasharray:none">
+ <path
+ style="color:#000000;fill:#008000;stroke-linecap:square;stroke-width:1.00012499;stroke-dasharray:none"
+ d="M 6.1057692,3.175 A 2.9307692,2.9307692 0 0 1 3.175,6.1057692 2.9307692,2.9307692 0 0 1 0.24423075,3.175 2.9307692,2.9307692 0 0 1 3.175,0.24423075 2.9307692,2.9307692 0 0 1 6.1057692,3.175 Z"
+ id="path6" />
+ <path
+ style="color:#000000;fill:#1a1a1a;stroke-linecap:square;stroke-width:0;stroke-dasharray:none"
+ d="M 3.1757813,0 C 1.4251716,0 0,1.4251716 0,3.1757813 c 0,1.7506096 1.4251716,3.1738281 3.1757813,3.1738281 1.7506096,0 3.1738281,-1.4232185 3.1738281,-3.1738281 C 6.3496094,1.4251716 4.9263909,0 3.1757813,0 Z m 0,0.48828125 c 1.4866254,0 2.6855468,1.20087455 2.6855468,2.68750005 0,1.4866254 -1.1989214,2.6855468 -2.6855468,2.6855468 -1.4866255,0 -2.68750005,-1.1989214 -2.68750005,-2.6855468 0,-1.4866255 1.20087455,-2.68750005 2.68750005,-2.68750005 z"
+ id="path7" />
+ </g>
+ <path
+ id="path5"
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#1a1a1a;stroke-width:0;stroke-linecap:square;stroke-dasharray:none"
+ d="M 3.1754573,0.48828127 1.4855127,3.414592 H 2.5828792 V 5.9667127 H 3.1754573 3.7671208 V 3.414592 h 1.0973665 z"
+ sodipodi:nodetypes="ccccscccc" />
+ </g>
+</svg>
diff --git a/iosApp/iosApp/Assets.xcassets/Angle90.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/Angle90.imageset/Contents.json
new file mode 100644
index 0000000..c84e36d
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Angle90.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "90.svg",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift b/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift
index 7e2ba81..4e0d39e 100644
--- a/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift
+++ b/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift
@@ -19,6 +19,7 @@ import Foundation
import Combine
import shared
+@MainActor
class UnitReportsViewModel: ObservableObject {
@Inject var reportController: ReportController
@Inject var geofencesController: GeofencesController
@@ -38,25 +39,27 @@ class UnitReportsViewModel: ObservableObject {
}
@Published var periodType: ReportDates.PeriodTypes = .today {
didSet {
- switch periodType {
- case .today:
- reportPeriod = ReportDates.ReportPeriodToday()
- case .last24:
- reportPeriod = ReportDates.ReportPeriodLast24()
- case .yesterday:
- reportPeriod = ReportDates.ReportPeriodYesterday()
- case .thisWeek:
- reportPeriod = ReportDates.ReportPeriodThisWeek()
- case .last7:
- reportPeriod = ReportDates.ReportPeriodLast7()
- case .thisMonth:
- reportPeriod = ReportDates.ReportPeriodThisMonth()
- case .last30:
- reportPeriod = ReportDates.ReportPeriodLast30()
- case .custom:
- reportPeriod = ReportDates.ReportPeriodCustom(from: nil, to: nil)
- default:
- reportPeriod = ReportDates.ReportPeriodToday()
+ Task { @MainActor in
+ switch periodType {
+ case .today:
+ reportPeriod = ReportDates.ReportPeriodToday()
+ case .last24:
+ reportPeriod = ReportDates.ReportPeriodLast24()
+ case .yesterday:
+ reportPeriod = ReportDates.ReportPeriodYesterday()
+ case .thisWeek:
+ reportPeriod = ReportDates.ReportPeriodThisWeek()
+ case .last7:
+ reportPeriod = ReportDates.ReportPeriodLast7()
+ case .thisMonth:
+ reportPeriod = ReportDates.ReportPeriodThisMonth()
+ case .last30:
+ reportPeriod = ReportDates.ReportPeriodLast30()
+ case .custom:
+ reportPeriod = ReportDates.ReportPeriodCustom(from: nil, to: nil)
+ default:
+ reportPeriod = ReportDates.ReportPeriodToday()
+ }
}
}
}
@@ -136,11 +139,15 @@ class UnitReportsViewModel: ObservableObject {
}
func setReport (report: ReportController.Report) {
- self.report = report
+ Task { @MainActor in
+ self.report = report
+ }
}
private func setGeofences(geofences: [Int: Geofence]) {
- self.geofences = geofences
+ Task { @MainActor in
+ self.geofences = geofences
+ }
}
func fetchReport(xlsx: Bool = false) {
diff --git a/iosApp/iosApp/Map/MapViewController.swift b/iosApp/iosApp/Map/MapViewController.swift
index c7bc91d..f424389 100644
--- a/iosApp/iosApp/Map/MapViewController.swift
+++ b/iosApp/iosApp/Map/MapViewController.swift
@@ -224,14 +224,14 @@ class OurMaplyViewController: MaplyViewController {
Float(marker.latitude))
}
- let fontSize = 11.0
+ let fontSize = Constants.markerLabelTextSize
let colorReport = UIColor(red: 0.0, green: 0.5, blue: 0.0, alpha: 1.0)
let colorLabel = UIColor.darkGray
let colorLabelOutline = UIColor.white
let vectorDesc: [AnyHashable : Any] = [
kMaplyColor: colorReport,
- kMaplyVecWidth: 12.0,
+ kMaplyVecWidth: Constants.reportLineWidth,
kMaplyWideVecImpl: kMaplyWideVecImplPerf
]
@@ -248,26 +248,26 @@ class OurMaplyViewController: MaplyViewController {
screenMarker.layoutImportance = .greatestFiniteMagnitude
screenMarker.loc = MaplyCoordinateMakeWithDegrees(Float(marker.longitude),
Float(marker.latitude))
- var type: Marker.Type_ = .default_
+ var image: UIImage
if isReport {
// For reports, position, start and end icons must be different
switch i {
- case markers.startIndex: type = .reportStart
- case markers.endIndex - 1: type = .reportEnd
- default: type = .reportPosition
+ case markers.startIndex: image = getIcon(markerType: .reportStart)
+ case markers.endIndex - 1: image = getIcon(markerType: .reportEnd)
+ default: image = getIcon(forDirection: points[i], to: points[i + 1])
}
} else {
- type = marker.type
+ image = getIcon(markerType: marker.type)
}
- screenMarker.image = getIcon(markerType: type)
+ screenMarker.image = image
- var size = 50.0
+ var size = Constants.markerSize
if isReport {
// For reports, position, start and end sizes must be different
switch i {
- case markers.startIndex: size = 40.0
- case markers.endIndex - 1: size = 40.0
- default: size = 22.0
+ case markers.startIndex: size = Constants.markerSize
+ case markers.endIndex - 1: size = Constants.markerSize
+ default: size = Constants.vertexsize
}
}
screenMarker.size = CGSize(width: size, height: size)
@@ -311,14 +311,14 @@ class OurMaplyViewController: MaplyViewController {
"features": [
[
"type": "Feature",
- "properties": [],
+ "properties": [] as [Any],
"geometry": [
"type": "LineString",
"coordinates": markers.map({ marker in
[marker.longitude, marker.latitude]
})
- ]
- ]
+ ] as [String : Any]
+ ] as [String : Any]
]
]
if let vector = MaplyVectorObject(fromGeoJSONDictionary: geoJSON) {
@@ -342,14 +342,14 @@ class OurMaplyViewController: MaplyViewController {
func display(geofences: [Geofence]) {
clear(geofences: true)
- let fontSize = 11.0
+ let fontSize = Constants.geofenceLabelTextSize
let colorFill = UIColor(red: 0.10, green: 0.46, blue: 0.82, alpha: 1.00)
let colorLabel = UIColor(red: 0.10, green: 0.46, blue: 0.82, alpha: 1.00)
let colorLabelOutline = UIColor.white
let vectorDesc: [AnyHashable : Any] = [
kMaplyColor: colorFill,
- kMaplyVecWidth: 12.0,
+ kMaplyVecWidth: Constants.geofenceLineWidth,
kMaplyWideVecImpl: kMaplyWideVecImplPerf
]
@@ -375,7 +375,7 @@ class OurMaplyViewController: MaplyViewController {
"features": [
[
"type": "Feature",
- "properties": [],
+ "properties": [] as [Any],
"geometry": [
"type": "Polygon",
"coordinates": [
@@ -383,8 +383,8 @@ class OurMaplyViewController: MaplyViewController {
[coordinate.y, coordinate.x]
}
]
- ]
- ]
+ ] as [String : Any]
+ ] as [String : Any]
]
]
if let vector = MaplyVectorObject(fromGeoJSONDictionary: geoJSON) {
@@ -439,4 +439,12 @@ class OurMaplyViewController: MaplyViewController {
return UIImage(named: MarkerTransformations
.markerTypeToImageName(markerType: markerType))!
}
+
+ private func getIcon(forDirection a: MaplyCoordinate, to b: MaplyCoordinate) -> UIImage {
+ let vectorX = b.x - a.x
+ let vectorY = b.y - a.y
+ let angleRad = atan2(vectorY, vectorX)
+ return UIImage(named: MarkerTransformations
+ .angleToImageName(rad: angleRad))!
+ }
}
diff --git a/iosApp/iosApp/Map/UnitMapView.swift b/iosApp/iosApp/Map/UnitMapView.swift
index 5a1e2f0..8444e36 100644
--- a/iosApp/iosApp/Map/UnitMapView.swift
+++ b/iosApp/iosApp/Map/UnitMapView.swift
@@ -27,7 +27,9 @@ struct UnitMapView: View {
markers: $unitsViewModel.markers,
geofences: $unitsViewModel.flatGeofences,
selected: $unitsViewModel.selectedMarker,
- markerCallback: unitsViewModel.selectUnitWith)
+ markerCallback: { p1, p2 in
+ unitsViewModel.selectUnitWith(position: p1, switchToMap: p2)
+ })
if let unit = unitsViewModel.selectedUnit {
VStack {
DeviceRow(unit: unit, callback: { action in
diff --git a/iosApp/iosApp/Session/RootView.swift b/iosApp/iosApp/Session/RootView.swift
index 960187a..10498b2 100644
--- a/iosApp/iosApp/Session/RootView.swift
+++ b/iosApp/iosApp/Session/RootView.swift
@@ -35,16 +35,34 @@ struct RootView: View {
var body: some View {
Group {
- switch rootViewModel.loginState {
- case is SessionController.LoginStateLoading:
- LoadingView()
- case is SessionController.LoginStateSuccess:
- UnitsView()
- default:
- LoginContentView(username: $username,
- password: $password,
- server: $server,
- onLogin: rootViewModel.login)
+ VStack {
+ if (rootViewModel.networkAvailable != true) {
+ OfflineBanner()
+ }
+
+ if rootViewModel.showLoadingView {
+ LoadingView()
+ .frame(minHeight: 0, maxHeight: .infinity)
+ } else {
+ switch rootViewModel.loginState {
+ case is SessionController.LoginStateSuccess:
+ UnitsView()
+ case is SessionController.LoginStateLoading:
+ LoadingView()
+ .frame(minHeight: 0, maxHeight: .infinity)
+ default:
+ LoginContentView(username: $username,
+ password: $password,
+ server: $server,
+ onLogin: { username, password, server in
+ rootViewModel.login(
+ username: username,
+ password: password,
+ url: server
+ )
+ })
+ }
+ }
}
}.environmentObject(rootViewModel)
}
diff --git a/iosApp/iosApp/Session/RootViewModel.swift b/iosApp/iosApp/Session/RootViewModel.swift
index ec103ba..16f21f3 100644
--- a/iosApp/iosApp/Session/RootViewModel.swift
+++ b/iosApp/iosApp/Session/RootViewModel.swift
@@ -20,14 +20,42 @@ import shared
@MainActor
class RootViewModel: ObservableObject {
+ @Inject private var networkController: NetworkController
@Inject private var sessionController: SessionController
+ @Published var networkAvailable: Bool? = nil
@Published var loginState: SessionController.LoginState = SessionController.LoginStateNothing()
+ @Published var showLoadingView: Bool = false
+
+ var hasSession: Bool{
+ get { return sessionController.hasSession }
+ }
init() {
- let collector = Collector<SessionController.LoginState?>(callback: setLoginState)
- sessionController.loginStateFlow.collect(collector: collector) { _ in }
- restoreSession()
+ let networkCollector = Collector<Bool?>(callback: setNetworkState)
+ networkController.networkAvailable.collect(collector: networkCollector) { _ in }
+ let sessionCollector = Collector<SessionController.LoginState?>(callback: setLoginState)
+ sessionController.loginStateFlow.collect(collector: sessionCollector) { _ in }
+ }
+
+ func setNetworkState(state: Bool?) {
+ print("Network state is: \(state?.description ?? "")")
+ Task { @MainActor in
+ self.networkAvailable = state
+
+ // Wait for internet to restore session
+ if (state == true && sessionController.hasSession) {
+ showLoadingView = false
+ restoreSession()
+ return
+ }
+
+ if (state == nil) {
+ showLoadingView = true
+ } else {
+ showLoadingView = hasSession
+ }
+ }
}
func setLoginState(state: SessionController.LoginState?) {
@@ -38,7 +66,7 @@ class RootViewModel: ObservableObject {
}
func restoreSession() {
- sessionController.restoreSession()
+ sessionController.getSession()
}
private func getFcmToken() -> String? {
diff --git a/iosApp/iosApp/Shared/Constants.swift b/iosApp/iosApp/Shared/Constants.swift
new file mode 100644
index 0000000..de82b0b
--- /dev/null
+++ b/iosApp/iosApp/Shared/Constants.swift
@@ -0,0 +1,19 @@
+//
+// Constants.swift
+// iosApp
+//
+// Created by Ivan Avalos on 21/09/23.
+// Copyright © 2023 orgName. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+struct Constants {
+ static var markerSize = 14.0 * UIScreen.main.scale
+ static var vertexsize = 8.0 * UIScreen.main.scale
+ static var markerLabelTextSize = UIFont.smallSystemFontSize
+ static var geofenceLabelTextSize = UIFont.smallSystemFontSize
+ static var geofenceLineWidth = 4.0 * UIScreen.main.scale
+ static var reportLineWidth = 4.0 * UIScreen.main.scale
+}
diff --git a/iosApp/iosApp/Shared/MarkerTransformations.swift b/iosApp/iosApp/Shared/MarkerTransformations.swift
index 7291a58..57aa36e 100644
--- a/iosApp/iosApp/Shared/MarkerTransformations.swift
+++ b/iosApp/iosApp/Shared/MarkerTransformations.swift
@@ -51,4 +51,23 @@ class MarkerTransformations {
}
return imageName
}
+
+ static let STEP = Float.pi / 8
+
+ static func angleToImageName(rad: Float) -> String {
+ var imageName: String
+ switch rad {
+ case 0.0 ..< STEP: imageName = "Angle0"
+ case STEP ..< STEP * 3: imageName = "Angle45"
+ case STEP * 3 ..< STEP * 5: imageName = "Angle90"
+ case STEP * 5 ..< STEP * 7: imageName = "Angle135"
+ case STEP * 7 ..< STEP * 9: imageName = "Angle180"
+ case STEP * 9 ..< STEP * 11: imageName = "Angle225"
+ case STEP * 11 ..< STEP * 13: imageName = "Angle270"
+ case STEP * 13 ..< STEP * 15: imageName = "Angle315"
+ case STEP * 15 ..< STEP * 16: imageName = "Angle0"
+ default: imageName = angleToImageName(rad: Float.pi * 2 + rad)
+ }
+ return imageName
+ }
}
diff --git a/iosApp/iosApp/Shared/OfflineBanner.swift b/iosApp/iosApp/Shared/OfflineBanner.swift
new file mode 100644
index 0000000..f29ab7d
--- /dev/null
+++ b/iosApp/iosApp/Shared/OfflineBanner.swift
@@ -0,0 +1,27 @@
+//
+// OfflineBanner.swift
+// iosApp
+//
+// Created by Ivan Avalos on 17/09/23.
+// Copyright © 2023 orgName. All rights reserved.
+//
+
+import SwiftUI
+
+struct OfflineBanner: View {
+ var body: some View {
+ Group {
+ Text("offline")
+ .foregroundColor(.white)
+ .padding(5)
+ }
+ .frame(minWidth: 0, maxWidth: .infinity)
+ .background(Color.red)
+ }
+}
+
+struct OfflineBanner_Previews: PreviewProvider {
+ static var previews: some View {
+ OfflineBanner()
+ }
+}
diff --git a/iosApp/iosApp/Units/UnitsViewModel.swift b/iosApp/iosApp/Units/UnitsViewModel.swift
index 7fcc80e..52e0c39 100644
--- a/iosApp/iosApp/Units/UnitsViewModel.swift
+++ b/iosApp/iosApp/Units/UnitsViewModel.swift
@@ -21,6 +21,7 @@ import shared
@MainActor
class UnitsViewModel: ObservableObject {
+ @Inject var networkController: NetworkController
@Inject var unitsController: UnitsController
@Inject var geofenceController: GeofencesController
@@ -34,6 +35,7 @@ class UnitsViewModel: ObservableObject {
var detailsUnit: UnitInformation? = nil
var detailsAction = DeviceRow.Action.details
+ @Published var networkAvailable: Bool? = nil
@Published var searchQuery = "" {
didSet {
unitsDisplayMode = .list
@@ -83,6 +85,9 @@ class UnitsViewModel: ObservableObject {
}
private func setupObservers() {
+ let networkCollector = Collector<Bool?>(callback: setNetworkState)
+ networkController.networkAvailable.collect(collector: networkCollector) { _ in }
+
let unitsCollector = Collector<[UnitInformation]>(callback: setUnits)
unitsController.displayedUnitsFlow.collect(collector: unitsCollector) { _ in }
@@ -90,12 +95,19 @@ class UnitsViewModel: ObservableObject {
geofenceController.geofencesFlow.collect(collector: geofencesCollector) { _ in }
}
+ private func setNetworkState(state: Bool?) {
+ print("Network state is: \(state?.description ?? "")")
+ Task { @MainActor in
+ self.networkAvailable = state
+ }
+ }
+
private func setUnits(units: [UnitInformation]) {
print("Positions")
Task { @MainActor in
self.units = units
+ updateSelectedUnit()
}
- updateSelectedUnit()
}
private func setGeofences(geofences: [Int: Geofence]) {
diff --git a/iosApp/iosApp/en.lproj/Localizable.strings b/iosApp/iosApp/en.lproj/Localizable.strings
index 0959a1f..58d1ba5 100644
--- a/iosApp/iosApp/en.lproj/Localizable.strings
+++ b/iosApp/iosApp/en.lproj/Localizable.strings
@@ -22,6 +22,7 @@
"loading" = "Loading";
"done" = "Done";
+"offline" = "No internet access";
"username" = "Username";
"password" = "Password";
diff --git a/iosApp/iosApp/es-419.lproj/Localizable.strings b/iosApp/iosApp/es-419.lproj/Localizable.strings
index 52267cc..8fc5d45 100644
--- a/iosApp/iosApp/es-419.lproj/Localizable.strings
+++ b/iosApp/iosApp/es-419.lproj/Localizable.strings
@@ -22,6 +22,7 @@
"loading" = "Cargando";
"done" = "Hecho";
+"offline" = "No hay acceso a internet";
"username" = "Usuario";
"password" = "Contraseña";
diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift
index 763e0e2..46848ae 100644
--- a/iosApp/iosApp/iOSApp.swift
+++ b/iosApp/iosApp/iOSApp.swift
@@ -53,8 +53,15 @@ struct iOSApp: App {
Resolver.shared.add(CommandsApi.self) { resolver in
return CommandsApi(sessionManager: resolver.resolve())
}
+ Resolver.shared.add(NetworkController.self) { resolver in
+ return NetworkController()
+ }
Resolver.shared.add(SessionController.self) { resolver in
- return SessionController(sessionApi: resolver.resolve(), usersApi: resolver.resolve())
+ return SessionController(
+ sessionManager: resolver.resolve(),
+ sessionApi: resolver.resolve(),
+ usersApi: resolver.resolve()
+ )
}
Resolver.shared.add(UnitsController.self) { resolver in
return UnitsController(devicesApi: resolver.resolve(), positionsApi: resolver.resolve())
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index 67acdb8..6517f7f 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -19,6 +19,11 @@ kotlin {
).forEach {
it.binaries.framework {
baseName = "shared"
+
+ // Fix builds in Xcode 15
+ if (System.getenv("XCODE_VERSION_MAJOR") == "1500") {
+ linkerOpts += "-ld64"
+ }
}
}
diff --git a/shared/src/androidMain/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml
index 568741e..f0f34af 100644
--- a/shared/src/androidMain/AndroidManifest.xml
+++ b/shared/src/androidMain/AndroidManifest.xml
@@ -1,2 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-<manifest /> \ No newline at end of file
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+</manifest> \ No newline at end of file
diff --git a/shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt b/shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt
new file mode 100644
index 0000000..eecd7de
--- /dev/null
+++ b/shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt
@@ -0,0 +1,55 @@
+package mx.trackermap.TrackerMap.controllers
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import mx.trackermap.TrackerMap.Injectable
+
+actual class NetworkController(context: Context): Injectable {
+ private val networkRequest = NetworkRequest.Builder().build()
+ private val connectivityManager: ConnectivityManager
+ private val _networkAvailable = MutableStateFlow<Boolean?>(null)
+ actual val networkAvailable = _networkAvailable.asStateFlow()
+
+ private val networkCallback = object: ConnectivityManager.NetworkCallback() {
+ override fun onCapabilitiesChanged(
+ network: Network,
+ networkCapabilities: NetworkCapabilities
+ ) {
+ super.onCapabilitiesChanged(network, networkCapabilities)
+ _networkAvailable.value = checkNetworkAccess(networkCapabilities)
+ }
+
+ override fun onLost(network: Network) {
+ super.onLost(network)
+ _networkAvailable.value = false
+ }
+ }
+
+ init {
+ connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ _networkAvailable.value = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ checkNetworkAccess(connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork))
+ } else {
+ connectivityManager.activeNetworkInfo?.isConnected == true
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
+ } else {
+ connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
+ }
+ }
+
+ private fun checkNetworkAccess(capabilities: NetworkCapabilities?) =
+ capabilities != null
+ && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ } else true
+} \ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt
index 937b2dd..8238f7e 100644
--- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/ApiClient.kt
@@ -168,7 +168,7 @@ open class ApiClient(
}
}
- if (sessionManager.token.isNotEmpty()) {
+ if (sessionManager.hasSession) {
request.headers["Cookie"] = sessionManager.token
}
val response: HttpResponse = client.request(request)
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/SessionManager.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/SessionManager.kt
index caf2da1..71ae5d0 100644
--- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/SessionManager.kt
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/infrastructure/SessionManager.kt
@@ -28,6 +28,8 @@ class SessionManager(
settings[ACCESS_TOKEN_KEY] = token
}
+ val hasSession: Boolean get() = settings.hasKey(ACCESS_TOKEN_KEY)
+
fun clearSession() {
settings.remove(ACCESS_TOKEN_KEY)
settings.remove(SERVER_URL_KEY)
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/UnitInformation.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/UnitInformation.kt
index 6afa350..33e85ee 100644
--- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/UnitInformation.kt
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/client/models/UnitInformation.kt
@@ -53,5 +53,15 @@ data class UnitInformation(
} ?: EngineStop.UNKNOWN
} else EngineStop.UNKNOWN
- fun getHourmeter() = position?.attributes?.get("hours")?.longOrNull
+ fun getHourmeter() = position?.attributes?.let { attrs ->
+ if ("io16" in attrs) {
+ // Minutes
+ attrs["io16"]?.longOrNull?.let { it * 60 * 1000 }
+ } else if ("hours" in attrs) {
+ // Milliseconds
+ attrs["hours"]?.longOrNull
+ } else {
+ null
+ }
+ }
} \ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt
new file mode 100644
index 0000000..08dcc87
--- /dev/null
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt
@@ -0,0 +1,8 @@
+package mx.trackermap.TrackerMap.controllers
+
+import kotlinx.coroutines.flow.StateFlow
+import mx.trackermap.TrackerMap.Injectable
+
+expect class NetworkController: Injectable {
+ val networkAvailable: StateFlow<Boolean?>
+} \ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/SessionController.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/SessionController.kt
index a63bba2..5cfdf96 100644
--- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/SessionController.kt
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/SessionController.kt
@@ -25,13 +25,15 @@ import kotlinx.serialization.json.JsonPrimitive
import mx.trackermap.TrackerMap.Injectable
import mx.trackermap.TrackerMap.client.apis.SessionApi
import mx.trackermap.TrackerMap.client.apis.UsersApi
+import mx.trackermap.TrackerMap.client.infrastructure.SessionManager
import mx.trackermap.TrackerMap.client.models.SessionBody
import mx.trackermap.TrackerMap.client.models.User
@DelicateCoroutinesApi
class SessionController(
+ private val sessionManager: SessionManager,
private val sessionApi: SessionApi,
- private val usersApi: UsersApi
+ private val usersApi: UsersApi,
): Injectable {
sealed class LoginState {
object Nothing: LoginState()
@@ -46,6 +48,7 @@ class SessionController(
val loginStateFlow = MutableStateFlow<LoginState?>(null)
val userFlow = MutableStateFlow<User?>(null)
+ val hasSession: Boolean get() = sessionManager.hasSession
fun getSession() {
loginStateFlow.value = LoginState.Loading
@@ -59,19 +62,6 @@ class SessionController(
}
}
- fun restoreSession() {
- loginStateFlow.value = LoginState.Loading
- GlobalScope.launch {
- try {
- userFlow.value = sessionApi.sessionGet()
- loginStateFlow.value = LoginState.Success
- } catch (e: Exception) {
- e.printStackTrace()
- loginStateFlow.value = LoginState.Nothing
- }
- }
- }
-
fun login(body: SessionBody) {
val url = body.url.trim()
val email = body.email.trim()
diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt
index 5298df3..9f3a142 100644
--- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt
+++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt
@@ -22,6 +22,8 @@ import kotlinx.datetime.*
@DelicateCoroutinesApi
class ReportDates {
+
+ // Don't remove! Used by iOS
enum class PeriodTypes {
TODAY,
LAST_24,
@@ -35,7 +37,7 @@ class ReportDates {
sealed class ReportPeriod {
val timezone = TimeZone.currentSystemDefault()
- val clock = Clock.System
+ private val clock = Clock.System
val instant = clock.now()
val dateTime = instant.toLocalDateTime(timezone)
val date = dateTime.date
@@ -47,7 +49,7 @@ class ReportDates {
return formatDateTime(from) to formatDateTime(to)
}
- fun formatDateTime(dateTime: LocalDateTime) =
+ private fun formatDateTime(dateTime: LocalDateTime) =
dateTime.toInstant(timezone).toString()
class Today : ReportPeriod() {
diff --git a/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt b/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt
new file mode 100644
index 0000000..d112c11
--- /dev/null
+++ b/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/controllers/NetworkController.kt
@@ -0,0 +1,25 @@
+package mx.trackermap.TrackerMap.controllers
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import mx.trackermap.TrackerMap.Injectable
+import platform.Network.*
+import platform.darwin.*
+
+actual class NetworkController: Injectable {
+ private val monitor = nw_path_monitor_create()
+ private val queue: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND.toLong(), 0u)
+ private val _networkAvailable = MutableStateFlow<Boolean?>(null)
+ actual val networkAvailable = _networkAvailable.asStateFlow()
+
+ private val updateHandler: nw_path_monitor_update_handler_t = { path: nw_path_t ->
+ val status = nw_path_get_status(path)
+ _networkAvailable.value = status in arrayOf(nw_path_status_satisfied, nw_path_status_satisfiable)
+ }
+
+ init {
+ nw_path_monitor_set_update_handler(monitor, updateHandler)
+ nw_path_monitor_set_queue(monitor, queue)
+ nw_path_monitor_start(monitor)
+ }
+} \ No newline at end of file