mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
allow to store the location in memories
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
acf0d25a5e
commit
351d82c6d7
44 changed files with 2273 additions and 553 deletions
|
|
@ -139,6 +139,9 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
<!-- Optional and requested only when "Location in Memories" is enabled. -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,16 @@ import io.flutter.plugin.common.MethodChannel
|
||||||
import eu.twonly.notifications.NotificationTapChannel
|
import eu.twonly.notifications.NotificationTapChannel
|
||||||
import eu.twonly.webxdc.WebxdcChannel
|
import eu.twonly.webxdc.WebxdcChannel
|
||||||
import eu.twonly.widget.WidgetRuntimeChannel
|
import eu.twonly.widget.WidgetRuntimeChannel
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
|
||||||
class MainActivity : FlutterFragmentActivity() {
|
class MainActivity : FlutterFragmentActivity() {
|
||||||
private val CHANNEL = "eu.twonly/photo_picker"
|
private val CHANNEL = "eu.twonly/photo_picker"
|
||||||
private var pendingResult: MethodChannel.Result? = null
|
private var pendingResult: MethodChannel.Result? = null
|
||||||
|
private var pendingLocationPermissionResult: MethodChannel.Result? = null
|
||||||
|
|
||||||
private lateinit var pickMultipleMedia: ActivityResultLauncher<PickVisualMediaRequest>
|
private lateinit var pickMultipleMedia: ActivityResultLauncher<PickVisualMediaRequest>
|
||||||
|
private lateinit var requestLocationPermissions: ActivityResultLauncher<Array<String>>
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
|
|
@ -43,6 +47,14 @@ class MainActivity : FlutterFragmentActivity() {
|
||||||
}
|
}
|
||||||
pendingResult = null
|
pendingResult = null
|
||||||
}
|
}
|
||||||
|
requestLocationPermissions = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
|
) {
|
||||||
|
pendingLocationPermissionResult?.success(
|
||||||
|
checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED,
|
||||||
|
)
|
||||||
|
pendingLocationPermissionResult = null
|
||||||
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +124,28 @@ class MainActivity : FlutterFragmentActivity() {
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MethodChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
"eu.twonly/location_metadata",
|
||||||
|
).setMethodCallHandler { call, result ->
|
||||||
|
if (call.method != "requestPrecisePermission") {
|
||||||
|
result.notImplemented()
|
||||||
|
return@setMethodCallHandler
|
||||||
|
}
|
||||||
|
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
result.success(true)
|
||||||
|
} else if (pendingLocationPermissionResult != null) {
|
||||||
|
result.error("request_in_progress", "A location permission request is already open.", null)
|
||||||
|
} else {
|
||||||
|
pendingLocationPermissionResult = result
|
||||||
|
requestLocationPermissions.launch(
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package eu.twonly.location
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.Location
|
||||||
|
import android.location.LocationListener
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import eu.twonly.MyApplication
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
|
/** One-shot precise location lookup called synchronously by Rust/JNI. */
|
||||||
|
object NativeLocation {
|
||||||
|
private const val TARGET_ACCURACY_METERS = 50f
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun current(timeoutMillis: Long): String? {
|
||||||
|
val context = MyApplication.instance
|
||||||
|
if (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val manager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return null
|
||||||
|
val finished = CountDownLatch(1)
|
||||||
|
val best = AtomicReference<Location?>(null)
|
||||||
|
val listener = object : LocationListener {
|
||||||
|
override fun onLocationChanged(location: Location) {
|
||||||
|
if (location.accuracy < 0) return
|
||||||
|
val previous = best.get()
|
||||||
|
if (previous == null || location.accuracy < previous.accuracy) best.set(location)
|
||||||
|
if (location.accuracy <= TARGET_ACCURACY_METERS) finished.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Android")
|
||||||
|
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
|
||||||
|
}
|
||||||
|
val main = Handler(Looper.getMainLooper())
|
||||||
|
main.post {
|
||||||
|
var requested = false
|
||||||
|
for (provider in listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)) {
|
||||||
|
if (!runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)) continue
|
||||||
|
runCatching {
|
||||||
|
manager.requestLocationUpdates(provider, 0L, 0f, listener, Looper.getMainLooper())
|
||||||
|
requested = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!requested) finished.countDown()
|
||||||
|
}
|
||||||
|
finished.await(timeoutMillis.coerceAtLeast(0), TimeUnit.MILLISECONDS)
|
||||||
|
main.post { runCatching { manager.removeUpdates(listener) } }
|
||||||
|
val location = best.get() ?: return null
|
||||||
|
return "[${location.latitude},${location.longitude},${location.accuracy}]"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -31,6 +31,7 @@
|
||||||
D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100042F70000100D1A002 /* NativeImageCodec.swift */; };
|
D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100042F70000100D1A002 /* NativeImageCodec.swift */; };
|
||||||
D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100062F70000100D1A003 /* NativeVideoCodec.swift */; };
|
D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100062F70000100D1A003 /* NativeVideoCodec.swift */; };
|
||||||
D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100082F70000100D1A004 /* NativeGallery.swift */; };
|
D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A100082F70000100D1A004 /* NativeGallery.swift */; };
|
||||||
|
D3A100092F70000100D1A005 /* NativeLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A1000A2F70000100D1A005 /* NativeLocation.swift */; };
|
||||||
D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */; };
|
D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */; };
|
||||||
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
|
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
@ -135,6 +136,7 @@
|
||||||
D3A100042F70000100D1A002 /* NativeImageCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeImageCodec.swift; sourceTree = "<group>"; };
|
D3A100042F70000100D1A002 /* NativeImageCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeImageCodec.swift; sourceTree = "<group>"; };
|
||||||
D3A100062F70000100D1A003 /* NativeVideoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeVideoCodec.swift; sourceTree = "<group>"; };
|
D3A100062F70000100D1A003 /* NativeVideoCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeVideoCodec.swift; sourceTree = "<group>"; };
|
||||||
D3A100082F70000100D1A004 /* NativeGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeGallery.swift; sourceTree = "<group>"; };
|
D3A100082F70000100D1A004 /* NativeGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeGallery.swift; sourceTree = "<group>"; };
|
||||||
|
D3A1000A2F70000100D1A005 /* NativeLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeLocation.swift; sourceTree = "<group>"; };
|
||||||
D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcHost.swift; sourceTree = "<group>"; };
|
D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcHost.swift; sourceTree = "<group>"; };
|
||||||
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
|
@ -305,6 +307,7 @@
|
||||||
D3A100042F70000100D1A002 /* NativeImageCodec.swift */,
|
D3A100042F70000100D1A002 /* NativeImageCodec.swift */,
|
||||||
D3A100062F70000100D1A003 /* NativeVideoCodec.swift */,
|
D3A100062F70000100D1A003 /* NativeVideoCodec.swift */,
|
||||||
D3A100082F70000100D1A004 /* NativeGallery.swift */,
|
D3A100082F70000100D1A004 /* NativeGallery.swift */,
|
||||||
|
D3A1000A2F70000100D1A005 /* NativeLocation.swift */,
|
||||||
D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */,
|
D3A1000C2F70000100D1A0FE /* WebxdcHost.swift */,
|
||||||
D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */,
|
D24E27CC2F38ABC10055D9D1 /* RunnerRelease.entitlements */,
|
||||||
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
|
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
|
||||||
|
|
@ -739,6 +742,7 @@
|
||||||
D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */,
|
D3A100032F70000100D1A002 /* NativeImageCodec.swift in Sources */,
|
||||||
D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */,
|
D3A100052F70000100D1A003 /* NativeVideoCodec.swift in Sources */,
|
||||||
D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */,
|
D3A100072F70000100D1A004 /* NativeGallery.swift in Sources */,
|
||||||
|
D3A100092F70000100D1A005 /* NativeLocation.swift in Sources */,
|
||||||
D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */,
|
D3A1000B2F70000100D1A0FE /* WebxdcHost.swift in Sources */,
|
||||||
D4A100022F81000100A10002 /* TwonlyWidgetShared.swift in Sources */,
|
D4A100022F81000100A10002 /* TwonlyWidgetShared.swift in Sources */,
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/app-check.git",
|
"location" : "https://github.com/google/app-check.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
|
"revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902",
|
||||||
"version" : "11.2.0"
|
"version" : "11.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -23,17 +23,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
|
"revision" : "346daa9f46316aa372b35b317e18224acc2e9063",
|
||||||
"version" : "12.14.0"
|
"version" : "12.18.0"
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "flutterfire",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/firebase/flutterfire",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81",
|
|
||||||
"version" : "4.6.0-firebase-core-swift"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -41,8 +32,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
|
"revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55",
|
||||||
"version" : "3.6.0"
|
"version" : "3.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -50,8 +41,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
|
"revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5",
|
||||||
"version" : "12.14.0"
|
"version" : "12.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -59,8 +50,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
"revision" : "ba3358d3c3dbae8ef230b58a46b97ad65e84e974",
|
||||||
"version" : "10.1.0"
|
"version" : "10.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -68,8 +59,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
|
"revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3",
|
||||||
"version" : "8.1.0"
|
"version" : "8.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -86,8 +77,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
"revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc",
|
||||||
"version" : "5.3.0"
|
"version" : "5.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -113,8 +104,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/firebase/nanopb.git",
|
"location" : "https://github.com/firebase/nanopb.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
"revision" : "3851d94a41890dea16dc3db34caf60e585cb4163",
|
||||||
"version" : "2.30910.0"
|
"version" : "2.30910.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -122,8 +113,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/google/promises.git",
|
"location" : "https://github.com/google/promises.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
"revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837",
|
||||||
"version" : "2.4.0"
|
"version" : "2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -131,8 +122,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/getsentry/sentry-cocoa",
|
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
|
"revision" : "f196cbb98e0388381d57908f2f5a9bdc385cde68",
|
||||||
"version" : "8.58.0"
|
"version" : "8.58.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,6 @@
|
||||||
"version" : "12.18.0"
|
"version" : "12.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "flutterfire",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/firebase/flutterfire",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "a10a4148e769fadb01b1ff8d6bb76e9137f35b81",
|
|
||||||
"version" : "4.6.0-firebase-core-swift"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
@ -131,8 +122,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/getsentry/sentry-cocoa",
|
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
|
"revision" : "f196cbb98e0388381d57908f2f5a9bdc385cde68",
|
||||||
"version" : "8.58.0"
|
"version" : "8.58.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import flutter_sharing_intent
|
||||||
RuntimeStorageChannel.register(with: engineBridge.pluginRegistry)
|
RuntimeStorageChannel.register(with: engineBridge.pluginRegistry)
|
||||||
NativeNotificationChannel.register(with: engineBridge.pluginRegistry)
|
NativeNotificationChannel.register(with: engineBridge.pluginRegistry)
|
||||||
WebxdcHostChannel.register(with: engineBridge.pluginRegistry)
|
WebxdcHostChannel.register(with: engineBridge.pluginRegistry)
|
||||||
|
NativeLocationPermission.register(with: engineBridge.pluginRegistry)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func userNotificationCenter(
|
override func userNotificationCenter(
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,12 @@
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>twonly will save photos or videos to your library.</string>
|
<string>twonly will save photos or videos to your library.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>This app does not use or store your location information.</string>
|
<string>If you enable location metadata, twonly uses your precise location to add it to Memories stored on this device and photos you export to your library.</string>
|
||||||
|
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
|
||||||
|
<dict>
|
||||||
|
<key>MemoryLocation</key>
|
||||||
|
<string>Your precise location is needed to attach an accurate capture location to this Memory.</string>
|
||||||
|
</dict>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
|
|
||||||
164
ios/Runner/NativeLocation.swift
Normal file
164
ios/Runner/NativeLocation.swift
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import CoreLocation
|
||||||
|
import Flutter
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
private let memoryLocationPurpose = "MemoryLocation"
|
||||||
|
|
||||||
|
/// Permission is requested only from the settings switch through this channel.
|
||||||
|
final class NativeLocationPermission: NSObject, CLLocationManagerDelegate {
|
||||||
|
private static var active: NativeLocationPermission?
|
||||||
|
private let manager = CLLocationManager()
|
||||||
|
private let completion: FlutterResult
|
||||||
|
|
||||||
|
init(completion: @escaping FlutterResult) {
|
||||||
|
self.completion = completion
|
||||||
|
super.init()
|
||||||
|
manager.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
static func register(with registry: FlutterPluginRegistry) {
|
||||||
|
guard let registrar = registry.registrar(forPlugin: "TwonlyLocationMetadata") else { return }
|
||||||
|
let channel = FlutterMethodChannel(
|
||||||
|
name: "eu.twonly/location_metadata",
|
||||||
|
binaryMessenger: registrar.messenger()
|
||||||
|
)
|
||||||
|
channel.setMethodCallHandler { call, result in
|
||||||
|
guard call.method == "requestPrecisePermission" else {
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard active == nil else {
|
||||||
|
result(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let request = NativeLocationPermission(completion: result)
|
||||||
|
active = request
|
||||||
|
request.begin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func begin() {
|
||||||
|
switch manager.authorizationStatus {
|
||||||
|
case .notDetermined:
|
||||||
|
manager.requestWhenInUseAuthorization()
|
||||||
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
|
ensureFullAccuracy()
|
||||||
|
default:
|
||||||
|
finish(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
switch manager.authorizationStatus {
|
||||||
|
case .authorizedAlways, .authorizedWhenInUse: ensureFullAccuracy()
|
||||||
|
case .denied, .restricted: finish(false)
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureFullAccuracy() {
|
||||||
|
guard manager.accuracyAuthorization != .fullAccuracy else {
|
||||||
|
finish(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: memoryLocationPurpose) {
|
||||||
|
[weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.finish(self.manager.accuracyAuthorization == .fullAccuracy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finish(_ granted: Bool) {
|
||||||
|
guard Self.active === self else { return }
|
||||||
|
Self.active = nil
|
||||||
|
completion(granted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class NativeLocationRequest: NSObject, CLLocationManagerDelegate {
|
||||||
|
private let manager = CLLocationManager()
|
||||||
|
private let completion: (CLLocation?) -> Void
|
||||||
|
private(set) var best: CLLocation?
|
||||||
|
private var finished = false
|
||||||
|
|
||||||
|
init(completion: @escaping (CLLocation?) -> Void) {
|
||||||
|
self.completion = completion
|
||||||
|
super.init()
|
||||||
|
manager.delegate = self
|
||||||
|
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||||
|
manager.distanceFilter = kCLDistanceFilterNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
guard CLLocationManager.locationServicesEnabled(),
|
||||||
|
[.authorizedAlways, .authorizedWhenInUse].contains(manager.authorizationStatus),
|
||||||
|
manager.accuracyAuthorization == .fullAccuracy
|
||||||
|
else {
|
||||||
|
stop(returning: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.startUpdatingLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
|
for location in locations where location.horizontalAccuracy >= 0 {
|
||||||
|
if best == nil || location.horizontalAccuracy < best!.horizontalAccuracy {
|
||||||
|
best = location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let best, best.horizontalAccuracy <= 50 {
|
||||||
|
stop(returning: best)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
if (error as? CLError)?.code != .locationUnknown {
|
||||||
|
stop(returning: best)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop(returning location: CLLocation?) {
|
||||||
|
guard !finished else { return }
|
||||||
|
finished = true
|
||||||
|
manager.stopUpdatingLocation()
|
||||||
|
completion(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeNativeLocationRequest: NativeLocationRequest?
|
||||||
|
|
||||||
|
/// Synchronous C ABI used from Rust's blocking location worker.
|
||||||
|
@_cdecl("twonly_current_location")
|
||||||
|
func twonlyCurrentLocation(
|
||||||
|
_ timeoutMillis: Int64,
|
||||||
|
_ latitude: UnsafeMutablePointer<Double>?,
|
||||||
|
_ longitude: UnsafeMutablePointer<Double>?,
|
||||||
|
_ accuracy: UnsafeMutablePointer<Double>?
|
||||||
|
) -> Bool {
|
||||||
|
guard let latitude, let longitude, let accuracy else { return false }
|
||||||
|
let finished = DispatchSemaphore(value: 0)
|
||||||
|
var found: CLLocation?
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let request = NativeLocationRequest { location in
|
||||||
|
found = location
|
||||||
|
activeNativeLocationRequest = nil
|
||||||
|
finished.signal()
|
||||||
|
}
|
||||||
|
activeNativeLocationRequest = request
|
||||||
|
request.start()
|
||||||
|
}
|
||||||
|
if finished.wait(timeout: .now() + .milliseconds(Int(max(0, timeoutMillis)))) == .timedOut {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
found = activeNativeLocationRequest?.best
|
||||||
|
activeNativeLocationRequest?.stop(returning: found)
|
||||||
|
activeNativeLocationRequest = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let found else { return false }
|
||||||
|
latitude.pointee = found.coordinate.latitude
|
||||||
|
longitude.pointee = found.coordinate.longitude
|
||||||
|
accuracy.pointee = found.horizontalAccuracy
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -471,6 +471,13 @@ class RustApi {
|
||||||
isDraftMedia: isDraftMedia,
|
isDraftMedia: isDraftMedia,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Starts the optional location window for a Memory row created by the
|
||||||
|
/// local save button rather than the outgoing-media initializer.
|
||||||
|
static Future<void> initializeMemoryLocation({required String mediaId}) =>
|
||||||
|
RustLib.instance.api.crateBridgeApiRustApiInitializeMemoryLocation(
|
||||||
|
mediaId: mediaId,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<String> insertAndSendAdditionalData({
|
static Future<String> insertAndSendAdditionalData({
|
||||||
required String groupId,
|
required String groupId,
|
||||||
required String messageType,
|
required String messageType,
|
||||||
|
|
@ -568,6 +575,11 @@ class RustApi {
|
||||||
.api
|
.api
|
||||||
.crateBridgeApiRustApiPrerenderMedia(mediaId: mediaId);
|
.crateBridgeApiRustApiPrerenderMedia(mediaId: mediaId);
|
||||||
|
|
||||||
|
/// Begins the optional precise-location lookup when the capture camera is
|
||||||
|
/// opened, so a later photo usually has a fix immediately available.
|
||||||
|
static Future<void> prewarmMemoryLocation() =>
|
||||||
|
RustLib.instance.api.crateBridgeApiRustApiPrewarmMemoryLocation();
|
||||||
|
|
||||||
/// Deletes temporary media whose messages are finished with it.
|
/// Deletes temporary media whose messages are finished with it.
|
||||||
static Future<void> purgeMediaTempFolder() =>
|
static Future<void> purgeMediaTempFolder() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiPurgeMediaTempFolder();
|
RustLib.instance.api.crateBridgeApiRustApiPurgeMediaTempFolder();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -131,6 +131,10 @@ class UserConfig {
|
||||||
List<String>? preSelectedEmojies;
|
List<String>? preSelectedEmojies;
|
||||||
Map<String, List<String>>? autoDownloadOptions;
|
Map<String, List<String>>? autoDownloadOptions;
|
||||||
bool storeMediaFilesInGallery;
|
bool storeMediaFilesInGallery;
|
||||||
|
|
||||||
|
/// Adds the capture location to locally stored Memories and gallery
|
||||||
|
/// exports. Disabled by default and never added to outgoing media.
|
||||||
|
bool storeLocationInMemories;
|
||||||
bool autoStoreAllSendUnlimitedMediaFiles;
|
bool autoStoreAllSendUnlimitedMediaFiles;
|
||||||
bool typingIndicators;
|
bool typingIndicators;
|
||||||
bool showRestoreFlame;
|
bool showRestoreFlame;
|
||||||
|
|
@ -202,6 +206,7 @@ class UserConfig {
|
||||||
this.preSelectedEmojies,
|
this.preSelectedEmojies,
|
||||||
this.autoDownloadOptions,
|
this.autoDownloadOptions,
|
||||||
required this.storeMediaFilesInGallery,
|
required this.storeMediaFilesInGallery,
|
||||||
|
required this.storeLocationInMemories,
|
||||||
required this.autoStoreAllSendUnlimitedMediaFiles,
|
required this.autoStoreAllSendUnlimitedMediaFiles,
|
||||||
required this.typingIndicators,
|
required this.typingIndicators,
|
||||||
required this.showRestoreFlame,
|
required this.showRestoreFlame,
|
||||||
|
|
@ -267,6 +272,7 @@ class UserConfig {
|
||||||
preSelectedEmojies.hashCode ^
|
preSelectedEmojies.hashCode ^
|
||||||
autoDownloadOptions.hashCode ^
|
autoDownloadOptions.hashCode ^
|
||||||
storeMediaFilesInGallery.hashCode ^
|
storeMediaFilesInGallery.hashCode ^
|
||||||
|
storeLocationInMemories.hashCode ^
|
||||||
autoStoreAllSendUnlimitedMediaFiles.hashCode ^
|
autoStoreAllSendUnlimitedMediaFiles.hashCode ^
|
||||||
typingIndicators.hashCode ^
|
typingIndicators.hashCode ^
|
||||||
showRestoreFlame.hashCode ^
|
showRestoreFlame.hashCode ^
|
||||||
|
|
@ -336,6 +342,7 @@ class UserConfig {
|
||||||
preSelectedEmojies == other.preSelectedEmojies &&
|
preSelectedEmojies == other.preSelectedEmojies &&
|
||||||
autoDownloadOptions == other.autoDownloadOptions &&
|
autoDownloadOptions == other.autoDownloadOptions &&
|
||||||
storeMediaFilesInGallery == other.storeMediaFilesInGallery &&
|
storeMediaFilesInGallery == other.storeMediaFilesInGallery &&
|
||||||
|
storeLocationInMemories == other.storeLocationInMemories &&
|
||||||
autoStoreAllSendUnlimitedMediaFiles ==
|
autoStoreAllSendUnlimitedMediaFiles ==
|
||||||
other.autoStoreAllSendUnlimitedMediaFiles &&
|
other.autoStoreAllSendUnlimitedMediaFiles &&
|
||||||
typingIndicators == other.typingIndicators &&
|
typingIndicators == other.typingIndicators &&
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,14 @@ class MediaFiles extends Table {
|
||||||
|
|
||||||
IntColumn get sizeInBytes => integer().nullable()();
|
IntColumn get sizeInBytes => integer().nullable()();
|
||||||
|
|
||||||
|
RealColumn get locationLatitude => real().nullable()();
|
||||||
|
RealColumn get locationLongitude => real().nullable()();
|
||||||
|
RealColumn get locationAccuracy => real().nullable()();
|
||||||
|
TextColumn get locationStatus => text().nullable()();
|
||||||
|
IntColumn get locationDeadlineAt => integer().nullable()();
|
||||||
|
BoolColumn get galleryExportPending =>
|
||||||
|
boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
TextColumn get createdAtMonth => text().nullable()();
|
TextColumn get createdAtMonth => text().nullable()();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3536,6 +3536,75 @@ class $MediaFilesTable extends MediaFiles
|
||||||
type: DriftSqlType.int,
|
type: DriftSqlType.int,
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _locationLatitudeMeta = const VerificationMeta(
|
||||||
|
'locationLatitude',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<double> locationLatitude = GeneratedColumn<double>(
|
||||||
|
'location_latitude',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.double,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const VerificationMeta _locationLongitudeMeta = const VerificationMeta(
|
||||||
|
'locationLongitude',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<double> locationLongitude =
|
||||||
|
GeneratedColumn<double>(
|
||||||
|
'location_longitude',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.double,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const VerificationMeta _locationAccuracyMeta = const VerificationMeta(
|
||||||
|
'locationAccuracy',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<double> locationAccuracy = GeneratedColumn<double>(
|
||||||
|
'location_accuracy',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.double,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const VerificationMeta _locationStatusMeta = const VerificationMeta(
|
||||||
|
'locationStatus',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> locationStatus = GeneratedColumn<String>(
|
||||||
|
'location_status',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const VerificationMeta _locationDeadlineAtMeta =
|
||||||
|
const VerificationMeta('locationDeadlineAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> locationDeadlineAt = GeneratedColumn<int>(
|
||||||
|
'location_deadline_at',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const VerificationMeta _galleryExportPendingMeta =
|
||||||
|
const VerificationMeta('galleryExportPending');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> galleryExportPending = GeneratedColumn<bool>(
|
||||||
|
'gallery_export_pending',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("gallery_export_pending" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const Constant(false),
|
||||||
|
);
|
||||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||||
'createdAt',
|
'createdAt',
|
||||||
);
|
);
|
||||||
|
|
@ -3586,6 +3655,12 @@ class $MediaFilesTable extends MediaFiles
|
||||||
storedFileHash,
|
storedFileHash,
|
||||||
hasThumbnail,
|
hasThumbnail,
|
||||||
sizeInBytes,
|
sizeInBytes,
|
||||||
|
locationLatitude,
|
||||||
|
locationLongitude,
|
||||||
|
locationAccuracy,
|
||||||
|
locationStatus,
|
||||||
|
locationDeadlineAt,
|
||||||
|
galleryExportPending,
|
||||||
createdAt,
|
createdAt,
|
||||||
createdAtMonth,
|
createdAtMonth,
|
||||||
];
|
];
|
||||||
|
|
@ -3768,6 +3843,60 @@ class $MediaFilesTable extends MediaFiles
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('location_latitude')) {
|
||||||
|
context.handle(
|
||||||
|
_locationLatitudeMeta,
|
||||||
|
locationLatitude.isAcceptableOrUnknown(
|
||||||
|
data['location_latitude']!,
|
||||||
|
_locationLatitudeMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('location_longitude')) {
|
||||||
|
context.handle(
|
||||||
|
_locationLongitudeMeta,
|
||||||
|
locationLongitude.isAcceptableOrUnknown(
|
||||||
|
data['location_longitude']!,
|
||||||
|
_locationLongitudeMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('location_accuracy')) {
|
||||||
|
context.handle(
|
||||||
|
_locationAccuracyMeta,
|
||||||
|
locationAccuracy.isAcceptableOrUnknown(
|
||||||
|
data['location_accuracy']!,
|
||||||
|
_locationAccuracyMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('location_status')) {
|
||||||
|
context.handle(
|
||||||
|
_locationStatusMeta,
|
||||||
|
locationStatus.isAcceptableOrUnknown(
|
||||||
|
data['location_status']!,
|
||||||
|
_locationStatusMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('location_deadline_at')) {
|
||||||
|
context.handle(
|
||||||
|
_locationDeadlineAtMeta,
|
||||||
|
locationDeadlineAt.isAcceptableOrUnknown(
|
||||||
|
data['location_deadline_at']!,
|
||||||
|
_locationDeadlineAtMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('gallery_export_pending')) {
|
||||||
|
context.handle(
|
||||||
|
_galleryExportPendingMeta,
|
||||||
|
galleryExportPending.isAcceptableOrUnknown(
|
||||||
|
data['gallery_export_pending']!,
|
||||||
|
_galleryExportPendingMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (data.containsKey('created_at')) {
|
if (data.containsKey('created_at')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_createdAtMeta,
|
_createdAtMeta,
|
||||||
|
|
@ -3903,6 +4032,30 @@ class $MediaFilesTable extends MediaFiles
|
||||||
DriftSqlType.int,
|
DriftSqlType.int,
|
||||||
data['${effectivePrefix}size_in_bytes'],
|
data['${effectivePrefix}size_in_bytes'],
|
||||||
),
|
),
|
||||||
|
locationLatitude: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.double,
|
||||||
|
data['${effectivePrefix}location_latitude'],
|
||||||
|
),
|
||||||
|
locationLongitude: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.double,
|
||||||
|
data['${effectivePrefix}location_longitude'],
|
||||||
|
),
|
||||||
|
locationAccuracy: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.double,
|
||||||
|
data['${effectivePrefix}location_accuracy'],
|
||||||
|
),
|
||||||
|
locationStatus: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}location_status'],
|
||||||
|
),
|
||||||
|
locationDeadlineAt: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}location_deadline_at'],
|
||||||
|
),
|
||||||
|
galleryExportPending: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}gallery_export_pending'],
|
||||||
|
)!,
|
||||||
createdAt: attachedDatabase.typeMapping.read(
|
createdAt: attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.dateTime,
|
DriftSqlType.dateTime,
|
||||||
data['${effectivePrefix}created_at'],
|
data['${effectivePrefix}created_at'],
|
||||||
|
|
@ -3972,6 +4125,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
final Uint8List? storedFileHash;
|
final Uint8List? storedFileHash;
|
||||||
final bool hasThumbnail;
|
final bool hasThumbnail;
|
||||||
final int? sizeInBytes;
|
final int? sizeInBytes;
|
||||||
|
final double? locationLatitude;
|
||||||
|
final double? locationLongitude;
|
||||||
|
final double? locationAccuracy;
|
||||||
|
final String? locationStatus;
|
||||||
|
final int? locationDeadlineAt;
|
||||||
|
final bool galleryExportPending;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final String? createdAtMonth;
|
final String? createdAtMonth;
|
||||||
const MediaFile({
|
const MediaFile({
|
||||||
|
|
@ -4000,6 +4159,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
this.storedFileHash,
|
this.storedFileHash,
|
||||||
required this.hasThumbnail,
|
required this.hasThumbnail,
|
||||||
this.sizeInBytes,
|
this.sizeInBytes,
|
||||||
|
this.locationLatitude,
|
||||||
|
this.locationLongitude,
|
||||||
|
this.locationAccuracy,
|
||||||
|
this.locationStatus,
|
||||||
|
this.locationDeadlineAt,
|
||||||
|
required this.galleryExportPending,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.createdAtMonth,
|
this.createdAtMonth,
|
||||||
});
|
});
|
||||||
|
|
@ -4079,6 +4244,22 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
if (!nullToAbsent || sizeInBytes != null) {
|
if (!nullToAbsent || sizeInBytes != null) {
|
||||||
map['size_in_bytes'] = Variable<int>(sizeInBytes);
|
map['size_in_bytes'] = Variable<int>(sizeInBytes);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || locationLatitude != null) {
|
||||||
|
map['location_latitude'] = Variable<double>(locationLatitude);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || locationLongitude != null) {
|
||||||
|
map['location_longitude'] = Variable<double>(locationLongitude);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || locationAccuracy != null) {
|
||||||
|
map['location_accuracy'] = Variable<double>(locationAccuracy);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || locationStatus != null) {
|
||||||
|
map['location_status'] = Variable<String>(locationStatus);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || locationDeadlineAt != null) {
|
||||||
|
map['location_deadline_at'] = Variable<int>(locationDeadlineAt);
|
||||||
|
}
|
||||||
|
map['gallery_export_pending'] = Variable<bool>(galleryExportPending);
|
||||||
map['created_at'] = Variable<DateTime>(createdAt);
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
if (!nullToAbsent || createdAtMonth != null) {
|
if (!nullToAbsent || createdAtMonth != null) {
|
||||||
map['created_at_month'] = Variable<String>(createdAtMonth);
|
map['created_at_month'] = Variable<String>(createdAtMonth);
|
||||||
|
|
@ -4144,6 +4325,22 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
sizeInBytes: sizeInBytes == null && nullToAbsent
|
sizeInBytes: sizeInBytes == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
: Value(sizeInBytes),
|
: Value(sizeInBytes),
|
||||||
|
locationLatitude: locationLatitude == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(locationLatitude),
|
||||||
|
locationLongitude: locationLongitude == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(locationLongitude),
|
||||||
|
locationAccuracy: locationAccuracy == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(locationAccuracy),
|
||||||
|
locationStatus: locationStatus == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(locationStatus),
|
||||||
|
locationDeadlineAt: locationDeadlineAt == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(locationDeadlineAt),
|
||||||
|
galleryExportPending: Value(galleryExportPending),
|
||||||
createdAt: Value(createdAt),
|
createdAt: Value(createdAt),
|
||||||
createdAtMonth: createdAtMonth == null && nullToAbsent
|
createdAtMonth: createdAtMonth == null && nullToAbsent
|
||||||
? const Value.absent()
|
? const Value.absent()
|
||||||
|
|
@ -4198,6 +4395,16 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
|
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
|
||||||
hasThumbnail: serializer.fromJson<bool>(json['hasThumbnail']),
|
hasThumbnail: serializer.fromJson<bool>(json['hasThumbnail']),
|
||||||
sizeInBytes: serializer.fromJson<int?>(json['sizeInBytes']),
|
sizeInBytes: serializer.fromJson<int?>(json['sizeInBytes']),
|
||||||
|
locationLatitude: serializer.fromJson<double?>(json['locationLatitude']),
|
||||||
|
locationLongitude: serializer.fromJson<double?>(
|
||||||
|
json['locationLongitude'],
|
||||||
|
),
|
||||||
|
locationAccuracy: serializer.fromJson<double?>(json['locationAccuracy']),
|
||||||
|
locationStatus: serializer.fromJson<String?>(json['locationStatus']),
|
||||||
|
locationDeadlineAt: serializer.fromJson<int?>(json['locationDeadlineAt']),
|
||||||
|
galleryExportPending: serializer.fromJson<bool>(
|
||||||
|
json['galleryExportPending'],
|
||||||
|
),
|
||||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']),
|
createdAtMonth: serializer.fromJson<String?>(json['createdAtMonth']),
|
||||||
);
|
);
|
||||||
|
|
@ -4241,6 +4448,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
|
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
|
||||||
'hasThumbnail': serializer.toJson<bool>(hasThumbnail),
|
'hasThumbnail': serializer.toJson<bool>(hasThumbnail),
|
||||||
'sizeInBytes': serializer.toJson<int?>(sizeInBytes),
|
'sizeInBytes': serializer.toJson<int?>(sizeInBytes),
|
||||||
|
'locationLatitude': serializer.toJson<double?>(locationLatitude),
|
||||||
|
'locationLongitude': serializer.toJson<double?>(locationLongitude),
|
||||||
|
'locationAccuracy': serializer.toJson<double?>(locationAccuracy),
|
||||||
|
'locationStatus': serializer.toJson<String?>(locationStatus),
|
||||||
|
'locationDeadlineAt': serializer.toJson<int?>(locationDeadlineAt),
|
||||||
|
'galleryExportPending': serializer.toJson<bool>(galleryExportPending),
|
||||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
'createdAtMonth': serializer.toJson<String?>(createdAtMonth),
|
'createdAtMonth': serializer.toJson<String?>(createdAtMonth),
|
||||||
};
|
};
|
||||||
|
|
@ -4272,6 +4485,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||||
bool? hasThumbnail,
|
bool? hasThumbnail,
|
||||||
Value<int?> sizeInBytes = const Value.absent(),
|
Value<int?> sizeInBytes = const Value.absent(),
|
||||||
|
Value<double?> locationLatitude = const Value.absent(),
|
||||||
|
Value<double?> locationLongitude = const Value.absent(),
|
||||||
|
Value<double?> locationAccuracy = const Value.absent(),
|
||||||
|
Value<String?> locationStatus = const Value.absent(),
|
||||||
|
Value<int?> locationDeadlineAt = const Value.absent(),
|
||||||
|
bool? galleryExportPending,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
Value<String?> createdAtMonth = const Value.absent(),
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
}) => MediaFile(
|
}) => MediaFile(
|
||||||
|
|
@ -4319,6 +4538,22 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
: this.storedFileHash,
|
: this.storedFileHash,
|
||||||
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
||||||
sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes,
|
sizeInBytes: sizeInBytes.present ? sizeInBytes.value : this.sizeInBytes,
|
||||||
|
locationLatitude: locationLatitude.present
|
||||||
|
? locationLatitude.value
|
||||||
|
: this.locationLatitude,
|
||||||
|
locationLongitude: locationLongitude.present
|
||||||
|
? locationLongitude.value
|
||||||
|
: this.locationLongitude,
|
||||||
|
locationAccuracy: locationAccuracy.present
|
||||||
|
? locationAccuracy.value
|
||||||
|
: this.locationAccuracy,
|
||||||
|
locationStatus: locationStatus.present
|
||||||
|
? locationStatus.value
|
||||||
|
: this.locationStatus,
|
||||||
|
locationDeadlineAt: locationDeadlineAt.present
|
||||||
|
? locationDeadlineAt.value
|
||||||
|
: this.locationDeadlineAt,
|
||||||
|
galleryExportPending: galleryExportPending ?? this.galleryExportPending,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
createdAtMonth: createdAtMonth.present
|
createdAtMonth: createdAtMonth.present
|
||||||
? createdAtMonth.value
|
? createdAtMonth.value
|
||||||
|
|
@ -4391,6 +4626,24 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
sizeInBytes: data.sizeInBytes.present
|
sizeInBytes: data.sizeInBytes.present
|
||||||
? data.sizeInBytes.value
|
? data.sizeInBytes.value
|
||||||
: this.sizeInBytes,
|
: this.sizeInBytes,
|
||||||
|
locationLatitude: data.locationLatitude.present
|
||||||
|
? data.locationLatitude.value
|
||||||
|
: this.locationLatitude,
|
||||||
|
locationLongitude: data.locationLongitude.present
|
||||||
|
? data.locationLongitude.value
|
||||||
|
: this.locationLongitude,
|
||||||
|
locationAccuracy: data.locationAccuracy.present
|
||||||
|
? data.locationAccuracy.value
|
||||||
|
: this.locationAccuracy,
|
||||||
|
locationStatus: data.locationStatus.present
|
||||||
|
? data.locationStatus.value
|
||||||
|
: this.locationStatus,
|
||||||
|
locationDeadlineAt: data.locationDeadlineAt.present
|
||||||
|
? data.locationDeadlineAt.value
|
||||||
|
: this.locationDeadlineAt,
|
||||||
|
galleryExportPending: data.galleryExportPending.present
|
||||||
|
? data.galleryExportPending.value
|
||||||
|
: this.galleryExportPending,
|
||||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
createdAtMonth: data.createdAtMonth.present
|
createdAtMonth: data.createdAtMonth.present
|
||||||
? data.createdAtMonth.value
|
? data.createdAtMonth.value
|
||||||
|
|
@ -4426,6 +4679,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
..write('storedFileHash: $storedFileHash, ')
|
..write('storedFileHash: $storedFileHash, ')
|
||||||
..write('hasThumbnail: $hasThumbnail, ')
|
..write('hasThumbnail: $hasThumbnail, ')
|
||||||
..write('sizeInBytes: $sizeInBytes, ')
|
..write('sizeInBytes: $sizeInBytes, ')
|
||||||
|
..write('locationLatitude: $locationLatitude, ')
|
||||||
|
..write('locationLongitude: $locationLongitude, ')
|
||||||
|
..write('locationAccuracy: $locationAccuracy, ')
|
||||||
|
..write('locationStatus: $locationStatus, ')
|
||||||
|
..write('locationDeadlineAt: $locationDeadlineAt, ')
|
||||||
|
..write('galleryExportPending: $galleryExportPending, ')
|
||||||
..write('createdAt: $createdAt, ')
|
..write('createdAt: $createdAt, ')
|
||||||
..write('createdAtMonth: $createdAtMonth')
|
..write('createdAtMonth: $createdAtMonth')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
|
|
@ -4459,6 +4718,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
$driftBlobEquality.hash(storedFileHash),
|
$driftBlobEquality.hash(storedFileHash),
|
||||||
hasThumbnail,
|
hasThumbnail,
|
||||||
sizeInBytes,
|
sizeInBytes,
|
||||||
|
locationLatitude,
|
||||||
|
locationLongitude,
|
||||||
|
locationAccuracy,
|
||||||
|
locationStatus,
|
||||||
|
locationDeadlineAt,
|
||||||
|
galleryExportPending,
|
||||||
createdAt,
|
createdAt,
|
||||||
createdAtMonth,
|
createdAtMonth,
|
||||||
]);
|
]);
|
||||||
|
|
@ -4497,6 +4762,12 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
||||||
) &&
|
) &&
|
||||||
other.hasThumbnail == this.hasThumbnail &&
|
other.hasThumbnail == this.hasThumbnail &&
|
||||||
other.sizeInBytes == this.sizeInBytes &&
|
other.sizeInBytes == this.sizeInBytes &&
|
||||||
|
other.locationLatitude == this.locationLatitude &&
|
||||||
|
other.locationLongitude == this.locationLongitude &&
|
||||||
|
other.locationAccuracy == this.locationAccuracy &&
|
||||||
|
other.locationStatus == this.locationStatus &&
|
||||||
|
other.locationDeadlineAt == this.locationDeadlineAt &&
|
||||||
|
other.galleryExportPending == this.galleryExportPending &&
|
||||||
other.createdAt == this.createdAt &&
|
other.createdAt == this.createdAt &&
|
||||||
other.createdAtMonth == this.createdAtMonth);
|
other.createdAtMonth == this.createdAtMonth);
|
||||||
}
|
}
|
||||||
|
|
@ -4527,6 +4798,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
final Value<Uint8List?> storedFileHash;
|
final Value<Uint8List?> storedFileHash;
|
||||||
final Value<bool> hasThumbnail;
|
final Value<bool> hasThumbnail;
|
||||||
final Value<int?> sizeInBytes;
|
final Value<int?> sizeInBytes;
|
||||||
|
final Value<double?> locationLatitude;
|
||||||
|
final Value<double?> locationLongitude;
|
||||||
|
final Value<double?> locationAccuracy;
|
||||||
|
final Value<String?> locationStatus;
|
||||||
|
final Value<int?> locationDeadlineAt;
|
||||||
|
final Value<bool> galleryExportPending;
|
||||||
final Value<DateTime> createdAt;
|
final Value<DateTime> createdAt;
|
||||||
final Value<String?> createdAtMonth;
|
final Value<String?> createdAtMonth;
|
||||||
final Value<int> rowid;
|
final Value<int> rowid;
|
||||||
|
|
@ -4556,6 +4833,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.storedFileHash = const Value.absent(),
|
this.storedFileHash = const Value.absent(),
|
||||||
this.hasThumbnail = const Value.absent(),
|
this.hasThumbnail = const Value.absent(),
|
||||||
this.sizeInBytes = const Value.absent(),
|
this.sizeInBytes = const Value.absent(),
|
||||||
|
this.locationLatitude = const Value.absent(),
|
||||||
|
this.locationLongitude = const Value.absent(),
|
||||||
|
this.locationAccuracy = const Value.absent(),
|
||||||
|
this.locationStatus = const Value.absent(),
|
||||||
|
this.locationDeadlineAt = const Value.absent(),
|
||||||
|
this.galleryExportPending = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
this.createdAtMonth = const Value.absent(),
|
this.createdAtMonth = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
|
|
@ -4586,6 +4869,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
this.storedFileHash = const Value.absent(),
|
this.storedFileHash = const Value.absent(),
|
||||||
this.hasThumbnail = const Value.absent(),
|
this.hasThumbnail = const Value.absent(),
|
||||||
this.sizeInBytes = const Value.absent(),
|
this.sizeInBytes = const Value.absent(),
|
||||||
|
this.locationLatitude = const Value.absent(),
|
||||||
|
this.locationLongitude = const Value.absent(),
|
||||||
|
this.locationAccuracy = const Value.absent(),
|
||||||
|
this.locationStatus = const Value.absent(),
|
||||||
|
this.locationDeadlineAt = const Value.absent(),
|
||||||
|
this.galleryExportPending = const Value.absent(),
|
||||||
this.createdAt = const Value.absent(),
|
this.createdAt = const Value.absent(),
|
||||||
this.createdAtMonth = const Value.absent(),
|
this.createdAtMonth = const Value.absent(),
|
||||||
this.rowid = const Value.absent(),
|
this.rowid = const Value.absent(),
|
||||||
|
|
@ -4617,6 +4906,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Expression<Uint8List>? storedFileHash,
|
Expression<Uint8List>? storedFileHash,
|
||||||
Expression<bool>? hasThumbnail,
|
Expression<bool>? hasThumbnail,
|
||||||
Expression<int>? sizeInBytes,
|
Expression<int>? sizeInBytes,
|
||||||
|
Expression<double>? locationLatitude,
|
||||||
|
Expression<double>? locationLongitude,
|
||||||
|
Expression<double>? locationAccuracy,
|
||||||
|
Expression<String>? locationStatus,
|
||||||
|
Expression<int>? locationDeadlineAt,
|
||||||
|
Expression<bool>? galleryExportPending,
|
||||||
Expression<DateTime>? createdAt,
|
Expression<DateTime>? createdAt,
|
||||||
Expression<String>? createdAtMonth,
|
Expression<String>? createdAtMonth,
|
||||||
Expression<int>? rowid,
|
Expression<int>? rowid,
|
||||||
|
|
@ -4651,6 +4946,14 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
if (storedFileHash != null) 'stored_file_hash': storedFileHash,
|
if (storedFileHash != null) 'stored_file_hash': storedFileHash,
|
||||||
if (hasThumbnail != null) 'has_thumbnail': hasThumbnail,
|
if (hasThumbnail != null) 'has_thumbnail': hasThumbnail,
|
||||||
if (sizeInBytes != null) 'size_in_bytes': sizeInBytes,
|
if (sizeInBytes != null) 'size_in_bytes': sizeInBytes,
|
||||||
|
if (locationLatitude != null) 'location_latitude': locationLatitude,
|
||||||
|
if (locationLongitude != null) 'location_longitude': locationLongitude,
|
||||||
|
if (locationAccuracy != null) 'location_accuracy': locationAccuracy,
|
||||||
|
if (locationStatus != null) 'location_status': locationStatus,
|
||||||
|
if (locationDeadlineAt != null)
|
||||||
|
'location_deadline_at': locationDeadlineAt,
|
||||||
|
if (galleryExportPending != null)
|
||||||
|
'gallery_export_pending': galleryExportPending,
|
||||||
if (createdAt != null) 'created_at': createdAt,
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
if (createdAtMonth != null) 'created_at_month': createdAtMonth,
|
if (createdAtMonth != null) 'created_at_month': createdAtMonth,
|
||||||
if (rowid != null) 'rowid': rowid,
|
if (rowid != null) 'rowid': rowid,
|
||||||
|
|
@ -4683,6 +4986,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
Value<Uint8List?>? storedFileHash,
|
Value<Uint8List?>? storedFileHash,
|
||||||
Value<bool>? hasThumbnail,
|
Value<bool>? hasThumbnail,
|
||||||
Value<int?>? sizeInBytes,
|
Value<int?>? sizeInBytes,
|
||||||
|
Value<double?>? locationLatitude,
|
||||||
|
Value<double?>? locationLongitude,
|
||||||
|
Value<double?>? locationAccuracy,
|
||||||
|
Value<String?>? locationStatus,
|
||||||
|
Value<int?>? locationDeadlineAt,
|
||||||
|
Value<bool>? galleryExportPending,
|
||||||
Value<DateTime>? createdAt,
|
Value<DateTime>? createdAt,
|
||||||
Value<String?>? createdAtMonth,
|
Value<String?>? createdAtMonth,
|
||||||
Value<int>? rowid,
|
Value<int>? rowid,
|
||||||
|
|
@ -4716,6 +5025,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
storedFileHash: storedFileHash ?? this.storedFileHash,
|
storedFileHash: storedFileHash ?? this.storedFileHash,
|
||||||
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
hasThumbnail: hasThumbnail ?? this.hasThumbnail,
|
||||||
sizeInBytes: sizeInBytes ?? this.sizeInBytes,
|
sizeInBytes: sizeInBytes ?? this.sizeInBytes,
|
||||||
|
locationLatitude: locationLatitude ?? this.locationLatitude,
|
||||||
|
locationLongitude: locationLongitude ?? this.locationLongitude,
|
||||||
|
locationAccuracy: locationAccuracy ?? this.locationAccuracy,
|
||||||
|
locationStatus: locationStatus ?? this.locationStatus,
|
||||||
|
locationDeadlineAt: locationDeadlineAt ?? this.locationDeadlineAt,
|
||||||
|
galleryExportPending: galleryExportPending ?? this.galleryExportPending,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
createdAtMonth: createdAtMonth ?? this.createdAtMonth,
|
createdAtMonth: createdAtMonth ?? this.createdAtMonth,
|
||||||
rowid: rowid ?? this.rowid,
|
rowid: rowid ?? this.rowid,
|
||||||
|
|
@ -4818,6 +5133,26 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
if (sizeInBytes.present) {
|
if (sizeInBytes.present) {
|
||||||
map['size_in_bytes'] = Variable<int>(sizeInBytes.value);
|
map['size_in_bytes'] = Variable<int>(sizeInBytes.value);
|
||||||
}
|
}
|
||||||
|
if (locationLatitude.present) {
|
||||||
|
map['location_latitude'] = Variable<double>(locationLatitude.value);
|
||||||
|
}
|
||||||
|
if (locationLongitude.present) {
|
||||||
|
map['location_longitude'] = Variable<double>(locationLongitude.value);
|
||||||
|
}
|
||||||
|
if (locationAccuracy.present) {
|
||||||
|
map['location_accuracy'] = Variable<double>(locationAccuracy.value);
|
||||||
|
}
|
||||||
|
if (locationStatus.present) {
|
||||||
|
map['location_status'] = Variable<String>(locationStatus.value);
|
||||||
|
}
|
||||||
|
if (locationDeadlineAt.present) {
|
||||||
|
map['location_deadline_at'] = Variable<int>(locationDeadlineAt.value);
|
||||||
|
}
|
||||||
|
if (galleryExportPending.present) {
|
||||||
|
map['gallery_export_pending'] = Variable<bool>(
|
||||||
|
galleryExportPending.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (createdAt.present) {
|
if (createdAt.present) {
|
||||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
}
|
}
|
||||||
|
|
@ -4858,6 +5193,12 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
||||||
..write('storedFileHash: $storedFileHash, ')
|
..write('storedFileHash: $storedFileHash, ')
|
||||||
..write('hasThumbnail: $hasThumbnail, ')
|
..write('hasThumbnail: $hasThumbnail, ')
|
||||||
..write('sizeInBytes: $sizeInBytes, ')
|
..write('sizeInBytes: $sizeInBytes, ')
|
||||||
|
..write('locationLatitude: $locationLatitude, ')
|
||||||
|
..write('locationLongitude: $locationLongitude, ')
|
||||||
|
..write('locationAccuracy: $locationAccuracy, ')
|
||||||
|
..write('locationStatus: $locationStatus, ')
|
||||||
|
..write('locationDeadlineAt: $locationDeadlineAt, ')
|
||||||
|
..write('galleryExportPending: $galleryExportPending, ')
|
||||||
..write('createdAt: $createdAt, ')
|
..write('createdAt: $createdAt, ')
|
||||||
..write('createdAtMonth: $createdAtMonth, ')
|
..write('createdAtMonth: $createdAtMonth, ')
|
||||||
..write('rowid: $rowid')
|
..write('rowid: $rowid')
|
||||||
|
|
@ -17839,6 +18180,12 @@ typedef $$MediaFilesTableCreateCompanionBuilder =
|
||||||
Value<Uint8List?> storedFileHash,
|
Value<Uint8List?> storedFileHash,
|
||||||
Value<bool> hasThumbnail,
|
Value<bool> hasThumbnail,
|
||||||
Value<int?> sizeInBytes,
|
Value<int?> sizeInBytes,
|
||||||
|
Value<double?> locationLatitude,
|
||||||
|
Value<double?> locationLongitude,
|
||||||
|
Value<double?> locationAccuracy,
|
||||||
|
Value<String?> locationStatus,
|
||||||
|
Value<int?> locationDeadlineAt,
|
||||||
|
Value<bool> galleryExportPending,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
Value<String?> createdAtMonth,
|
Value<String?> createdAtMonth,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
|
|
@ -17870,6 +18217,12 @@ typedef $$MediaFilesTableUpdateCompanionBuilder =
|
||||||
Value<Uint8List?> storedFileHash,
|
Value<Uint8List?> storedFileHash,
|
||||||
Value<bool> hasThumbnail,
|
Value<bool> hasThumbnail,
|
||||||
Value<int?> sizeInBytes,
|
Value<int?> sizeInBytes,
|
||||||
|
Value<double?> locationLatitude,
|
||||||
|
Value<double?> locationLongitude,
|
||||||
|
Value<double?> locationAccuracy,
|
||||||
|
Value<String?> locationStatus,
|
||||||
|
Value<int?> locationDeadlineAt,
|
||||||
|
Value<bool> galleryExportPending,
|
||||||
Value<DateTime> createdAt,
|
Value<DateTime> createdAt,
|
||||||
Value<String?> createdAtMonth,
|
Value<String?> createdAtMonth,
|
||||||
Value<int> rowid,
|
Value<int> rowid,
|
||||||
|
|
@ -18037,6 +18390,36 @@ class $$MediaFilesTableFilterComposer
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<double> get locationLatitude => $composableBuilder(
|
||||||
|
column: $table.locationLatitude,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<double> get locationLongitude => $composableBuilder(
|
||||||
|
column: $table.locationLongitude,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<double> get locationAccuracy => $composableBuilder(
|
||||||
|
column: $table.locationAccuracy,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<String> get locationStatus => $composableBuilder(
|
||||||
|
column: $table.locationStatus,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<int> get locationDeadlineAt => $composableBuilder(
|
||||||
|
column: $table.locationDeadlineAt,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<bool> get galleryExportPending => $composableBuilder(
|
||||||
|
column: $table.galleryExportPending,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
column: $table.createdAt,
|
column: $table.createdAt,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
|
|
@ -18207,6 +18590,36 @@ class $$MediaFilesTableOrderingComposer
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<double> get locationLatitude => $composableBuilder(
|
||||||
|
column: $table.locationLatitude,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<double> get locationLongitude => $composableBuilder(
|
||||||
|
column: $table.locationLongitude,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<double> get locationAccuracy => $composableBuilder(
|
||||||
|
column: $table.locationAccuracy,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<String> get locationStatus => $composableBuilder(
|
||||||
|
column: $table.locationStatus,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<int> get locationDeadlineAt => $composableBuilder(
|
||||||
|
column: $table.locationDeadlineAt,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get galleryExportPending => $composableBuilder(
|
||||||
|
column: $table.galleryExportPending,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
column: $table.createdAt,
|
column: $table.createdAt,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
|
@ -18346,6 +18759,36 @@ class $$MediaFilesTableAnnotationComposer
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<double> get locationLatitude => $composableBuilder(
|
||||||
|
column: $table.locationLatitude,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<double> get locationLongitude => $composableBuilder(
|
||||||
|
column: $table.locationLongitude,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<double> get locationAccuracy => $composableBuilder(
|
||||||
|
column: $table.locationAccuracy,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get locationStatus => $composableBuilder(
|
||||||
|
column: $table.locationStatus,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get locationDeadlineAt => $composableBuilder(
|
||||||
|
column: $table.locationDeadlineAt,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get galleryExportPending => $composableBuilder(
|
||||||
|
column: $table.galleryExportPending,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
GeneratedColumn<DateTime> get createdAt =>
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
|
@ -18433,6 +18876,12 @@ class $$MediaFilesTableTableManager
|
||||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||||
Value<bool> hasThumbnail = const Value.absent(),
|
Value<bool> hasThumbnail = const Value.absent(),
|
||||||
Value<int?> sizeInBytes = const Value.absent(),
|
Value<int?> sizeInBytes = const Value.absent(),
|
||||||
|
Value<double?> locationLatitude = const Value.absent(),
|
||||||
|
Value<double?> locationLongitude = const Value.absent(),
|
||||||
|
Value<double?> locationAccuracy = const Value.absent(),
|
||||||
|
Value<String?> locationStatus = const Value.absent(),
|
||||||
|
Value<int?> locationDeadlineAt = const Value.absent(),
|
||||||
|
Value<bool> galleryExportPending = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
Value<String?> createdAtMonth = const Value.absent(),
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
|
|
@ -18462,6 +18911,12 @@ class $$MediaFilesTableTableManager
|
||||||
storedFileHash: storedFileHash,
|
storedFileHash: storedFileHash,
|
||||||
hasThumbnail: hasThumbnail,
|
hasThumbnail: hasThumbnail,
|
||||||
sizeInBytes: sizeInBytes,
|
sizeInBytes: sizeInBytes,
|
||||||
|
locationLatitude: locationLatitude,
|
||||||
|
locationLongitude: locationLongitude,
|
||||||
|
locationAccuracy: locationAccuracy,
|
||||||
|
locationStatus: locationStatus,
|
||||||
|
locationDeadlineAt: locationDeadlineAt,
|
||||||
|
galleryExportPending: galleryExportPending,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
createdAtMonth: createdAtMonth,
|
createdAtMonth: createdAtMonth,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
|
|
@ -18493,6 +18948,12 @@ class $$MediaFilesTableTableManager
|
||||||
Value<Uint8List?> storedFileHash = const Value.absent(),
|
Value<Uint8List?> storedFileHash = const Value.absent(),
|
||||||
Value<bool> hasThumbnail = const Value.absent(),
|
Value<bool> hasThumbnail = const Value.absent(),
|
||||||
Value<int?> sizeInBytes = const Value.absent(),
|
Value<int?> sizeInBytes = const Value.absent(),
|
||||||
|
Value<double?> locationLatitude = const Value.absent(),
|
||||||
|
Value<double?> locationLongitude = const Value.absent(),
|
||||||
|
Value<double?> locationAccuracy = const Value.absent(),
|
||||||
|
Value<String?> locationStatus = const Value.absent(),
|
||||||
|
Value<int?> locationDeadlineAt = const Value.absent(),
|
||||||
|
Value<bool> galleryExportPending = const Value.absent(),
|
||||||
Value<DateTime> createdAt = const Value.absent(),
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
Value<String?> createdAtMonth = const Value.absent(),
|
Value<String?> createdAtMonth = const Value.absent(),
|
||||||
Value<int> rowid = const Value.absent(),
|
Value<int> rowid = const Value.absent(),
|
||||||
|
|
@ -18522,6 +18983,12 @@ class $$MediaFilesTableTableManager
|
||||||
storedFileHash: storedFileHash,
|
storedFileHash: storedFileHash,
|
||||||
hasThumbnail: hasThumbnail,
|
hasThumbnail: hasThumbnail,
|
||||||
sizeInBytes: sizeInBytes,
|
sizeInBytes: sizeInBytes,
|
||||||
|
locationLatitude: locationLatitude,
|
||||||
|
locationLongitude: locationLongitude,
|
||||||
|
locationAccuracy: locationAccuracy,
|
||||||
|
locationStatus: locationStatus,
|
||||||
|
locationDeadlineAt: locationDeadlineAt,
|
||||||
|
galleryExportPending: galleryExportPending,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
createdAtMonth: createdAtMonth,
|
createdAtMonth: createdAtMonth,
|
||||||
rowid: rowid,
|
rowid: rowid,
|
||||||
|
|
|
||||||
|
|
@ -521,9 +521,27 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @settingsStorageData.
|
/// No description provided for @settingsStorageData.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Data and storage'**
|
/// **'Memories & Storage'**
|
||||||
String get settingsStorageData;
|
String get settingsStorageData;
|
||||||
|
|
||||||
|
/// No description provided for @memoryLocationTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Store location in Memories'**
|
||||||
|
String get memoryLocationTitle;
|
||||||
|
|
||||||
|
/// No description provided for @memoryLocationSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Location for Memories and gallery exports. Never sent to contacts.'**
|
||||||
|
String get memoryLocationSubtitle;
|
||||||
|
|
||||||
|
/// No description provided for @memoryLocationPermissionDenied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Precise location access was not granted. The setting remains disabled.'**
|
||||||
|
String get memoryLocationPermissionDenied;
|
||||||
|
|
||||||
/// No description provided for @settingsStorageDataStoreInGTitle.
|
/// No description provided for @settingsStorageDataStoreInGTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get settingsProfile => 'Profil';
|
String get settingsProfile => 'Profil';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageData => 'Daten und Speicher';
|
String get settingsStorageData => 'Memories & Speicher';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get memoryLocationTitle => 'Standort in Memories speichern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get memoryLocationSubtitle =>
|
||||||
|
'Standort für Memories und Galerie-Exporte. Wird nicht an Kontakte gesendet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get memoryLocationPermissionDenied =>
|
||||||
|
'Der genaue Standortzugriff wurde nicht erlaubt. Die Einstellung bleibt deaktiviert.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern';
|
String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern';
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get settingsProfile => 'Profile';
|
String get settingsProfile => 'Profile';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageData => 'Data and storage';
|
String get settingsStorageData => 'Memories & Storage';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get memoryLocationTitle => 'Store location in Memories';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get memoryLocationSubtitle =>
|
||||||
|
'Location for Memories and gallery exports. Never sent to contacts.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get memoryLocationPermissionDenied =>
|
||||||
|
'Precise location access was not granted. The setting remains disabled.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataStoreInGTitle => 'Store in Gallery';
|
String get settingsStorageDataStoreInGTitle => 'Store in Gallery';
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,10 @@
|
||||||
"imageEditorDrawOk": "Zeichnung machen",
|
"imageEditorDrawOk": "Zeichnung machen",
|
||||||
"settingsTitle": "Einstellungen",
|
"settingsTitle": "Einstellungen",
|
||||||
"settingsChats": "Chats",
|
"settingsChats": "Chats",
|
||||||
"settingsStorageData": "Daten und Speicher",
|
"settingsStorageData": "Memories & Speicher",
|
||||||
|
"memoryLocationTitle": "Standort in Memories speichern",
|
||||||
|
"memoryLocationSubtitle": "Standort für Memories und Galerie-Exporte. Wird nicht an Kontakte gesendet.",
|
||||||
|
"memoryLocationPermissionDenied": "Der genaue Standortzugriff wurde nicht erlaubt. Die Einstellung bleibt deaktiviert.",
|
||||||
"settingsStorageDataStoreInGTitle": "In der Galerie speichern",
|
"settingsStorageDataStoreInGTitle": "In der Galerie speichern",
|
||||||
"settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload",
|
"settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload",
|
||||||
"settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten",
|
"settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten",
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,10 @@
|
||||||
"settingsAutomaticallyMarkEqualMediaFilesAsOpenedSubtitle": "If you receive the same media in multiple chats, opening one marks all others as opened.",
|
"settingsAutomaticallyMarkEqualMediaFilesAsOpenedSubtitle": "If you receive the same media in multiple chats, opening one marks all others as opened.",
|
||||||
"settingsPreSelectedReactionsError": "A maximum of 12 reactions can be selected.",
|
"settingsPreSelectedReactionsError": "A maximum of 12 reactions can be selected.",
|
||||||
"settingsProfile": "Profile",
|
"settingsProfile": "Profile",
|
||||||
"settingsStorageData": "Data and storage",
|
"settingsStorageData": "Memories & Storage",
|
||||||
|
"memoryLocationTitle": "Store location in Memories",
|
||||||
|
"memoryLocationSubtitle": "Location for Memories and gallery exports. Never sent to contacts.",
|
||||||
|
"memoryLocationPermissionDenied": "Precise location access was not granted. The setting remains disabled.",
|
||||||
"settingsStorageDataStoreInGTitle": "Store in Gallery",
|
"settingsStorageDataStoreInGTitle": "Store in Gallery",
|
||||||
"settingsStorageDataMediaAutoDownload": "Media auto-download",
|
"settingsStorageDataMediaAutoDownload": "Media auto-download",
|
||||||
"settingsStorageDataAutoDownMobile": "When using mobile data",
|
"settingsStorageDataAutoDownMobile": "When using mobile data",
|
||||||
|
|
|
||||||
|
|
@ -108,11 +108,14 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
isPayingUser(
|
isPayingUser(
|
||||||
planFromString(userService.currentUser.subscriptionPlan),
|
planFromString(userService.currentUser.subscriptionPlan),
|
||||||
)) {
|
)) {
|
||||||
globalForceIpaCheck = Timer(const Duration(seconds: 5), () async {
|
globalForceIpaCheck = Timer(const Duration(seconds: 5), () {
|
||||||
Log.info(
|
Log.info(
|
||||||
'Force Ipa check was not stopped. Requesting forced check...',
|
'Force Ipa check was not stopped. Requesting forced check...',
|
||||||
);
|
);
|
||||||
await RustApi.forceIpaCheck();
|
unawaitedRustCall(
|
||||||
|
RustApi.forceIpaCheck(),
|
||||||
|
'Forced IAP check',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
13
lib/src/services/location_metadata.service.dart
Normal file
13
lib/src/services/location_metadata.service.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class LocationMetadataService {
|
||||||
|
static const _channel = MethodChannel('eu.twonly/location_metadata');
|
||||||
|
|
||||||
|
/// Opens the native permission sheet only after the user enables the switch.
|
||||||
|
/// Approximate/reduced access is rejected because this feature promises a
|
||||||
|
/// precise capture location.
|
||||||
|
static Future<bool> requestPrecisePermission() async {
|
||||||
|
return await _channel.invokeMethod<bool>('requestPrecisePermission') ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -384,6 +384,7 @@ class MemoriesService {
|
||||||
final mediaFile = item.mediaService.mediaFile;
|
final mediaFile = item.mediaService.mediaFile;
|
||||||
if (!mediaFile.hasThumbnail &&
|
if (!mediaFile.hasThumbnail &&
|
||||||
mediaFile.type != MediaType.audio &&
|
mediaFile.type != MediaType.audio &&
|
||||||
|
item.mediaService.storedPath.existsSync() &&
|
||||||
_thumbnailRequests.add(mediaFile.mediaId)) {
|
_thumbnailRequests.add(mediaFile.mediaId)) {
|
||||||
unawaited(item.mediaService.createThumbnail());
|
unawaited(item.mediaService.createThumbnail());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
initVolumeControl();
|
initVolumeControl();
|
||||||
initAsync();
|
initAsync();
|
||||||
_checkAndInitCamera();
|
_checkAndInitCamera();
|
||||||
|
_prewarmMemoryLocation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -178,12 +179,24 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
if (widget.isVisible) {
|
if (widget.isVisible) {
|
||||||
initVolumeControl();
|
initVolumeControl();
|
||||||
_checkAndInitCamera();
|
_checkAndInitCamera();
|
||||||
|
_prewarmMemoryLocation();
|
||||||
} else {
|
} else {
|
||||||
_deInitVolumeControl();
|
_deInitVolumeControl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _prewarmMemoryLocation() {
|
||||||
|
// QR scanning deliberately does not touch location. A normal camera view
|
||||||
|
// has controllers and can create a Memory, so start looking as soon as it
|
||||||
|
// becomes visible instead of waiting for the shutter.
|
||||||
|
if (!widget.isVisible || widget.hideControllers) return;
|
||||||
|
unawaitedRustCall(
|
||||||
|
RustApi.prewarmMemoryLocation(),
|
||||||
|
'prewarmMemoryLocation',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _checkAndInitCamera() {
|
void _checkAndInitCamera() {
|
||||||
if (widget.isVisible &&
|
if (widget.isVisible &&
|
||||||
mc.cameraController == null &&
|
mc.cameraController == null &&
|
||||||
|
|
|
||||||
|
|
@ -52,13 +52,19 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
MediaFilesCompanion(
|
MediaFilesCompanion(
|
||||||
type: Value(widget.mediaService.mediaFile.type),
|
type: Value(widget.mediaService.mediaFile.type),
|
||||||
createdAt: Value(clock.now()),
|
createdAt: Value(clock.now()),
|
||||||
stored: const Value(true),
|
// Rust publishes the Memory as stored only after the
|
||||||
|
// plaintext and its thumbnail have been written.
|
||||||
|
stored: const Value(false),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newMediaFile != null) {
|
if (newMediaFile != null) {
|
||||||
final newService = MediaFileService(newMediaFile);
|
final newService = MediaFileService(newMediaFile);
|
||||||
|
|
||||||
|
await RustApi.initializeMemoryLocation(
|
||||||
|
mediaId: newMediaFile.mediaId,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.mediaService.tempPath.existsSync()) {
|
if (widget.mediaService.tempPath.existsSync()) {
|
||||||
widget.mediaService.tempPath.copySync(
|
widget.mediaService.tempPath.copySync(
|
||||||
newService.tempPath.path,
|
newService.tempPath.path,
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,13 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant MemoriesThumbnailComp oldWidget) {
|
void didUpdateWidget(covariant MemoriesThumbnailComp oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.galleryItem.mediaService.mediaFile.mediaId !=
|
final oldMedia = oldWidget.galleryItem.mediaService.mediaFile;
|
||||||
widget.galleryItem.mediaService.mediaFile.mediaId) {
|
final media = widget.galleryItem.mediaService.mediaFile;
|
||||||
|
final changedMedia = oldMedia.mediaId != media.mediaId;
|
||||||
|
final sourceBecameAvailable =
|
||||||
|
!oldMedia.stored && media.stored ||
|
||||||
|
!oldMedia.hasThumbnail && media.hasThumbnail;
|
||||||
|
if (changedMedia || sourceBecameAvailable) {
|
||||||
_imageStream?.removeListener(_listener);
|
_imageStream?.removeListener(_listener);
|
||||||
_imageStream = null;
|
_imageStream = null;
|
||||||
_imageProvider = null;
|
_imageProvider = null;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/services/location_metadata.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/media_download_policy.dart';
|
import 'package:twonly/src/services/mediafiles/media_download_policy.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
class DataAndStorageView extends StatefulWidget {
|
class DataAndStorageView extends StatefulWidget {
|
||||||
|
|
@ -18,6 +20,7 @@ class DataAndStorageView extends StatefulWidget {
|
||||||
|
|
||||||
class _DataAndStorageViewState extends State<DataAndStorageView> {
|
class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
late Future<Map<MediaType, int>> _storageStatsFuture;
|
late Future<Map<MediaType, int>> _storageStatsFuture;
|
||||||
|
bool _isUpdatingLocationSetting = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -86,6 +89,46 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> toggleLocationInMemories() async {
|
||||||
|
if (_isUpdatingLocationSetting) return;
|
||||||
|
setState(() => _isUpdatingLocationSetting = true);
|
||||||
|
final currentlyEnabled = userService.currentUser.storeLocationInMemories;
|
||||||
|
try {
|
||||||
|
if (!currentlyEnabled) {
|
||||||
|
final granted =
|
||||||
|
await LocationMetadataService.requestPrecisePermission();
|
||||||
|
if (!granted) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.lang.memoryLocationPermissionDenied),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await UserService.update((user) {
|
||||||
|
user.storeLocationInMemories = !currentlyEnabled;
|
||||||
|
});
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
Log.error(
|
||||||
|
'Could not update the Memory location setting',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(context.lang.memoryLocationPermissionDenied),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isUpdatingLocationSetting = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -124,6 +167,22 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
||||||
onChanged: (a) => toggleStoreInGallery(),
|
onChanged: (a) => toggleStoreInGallery(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.lang.memoryLocationTitle),
|
||||||
|
subtitle: Text(
|
||||||
|
context.lang.memoryLocationSubtitle,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
onTap: _isUpdatingLocationSetting
|
||||||
|
? null
|
||||||
|
: toggleLocationInMemories,
|
||||||
|
trailing: Switch.adaptive(
|
||||||
|
value: userService.currentUser.storeLocationInMemories,
|
||||||
|
onChanged: _isUpdatingLocationSetting
|
||||||
|
? null
|
||||||
|
: (_) => toggleLocationInMemories(),
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.autoStoreAllSendUnlimitedMediaFiles),
|
title: Text(context.lang.autoStoreAllSendUnlimitedMediaFiles),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,7 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
hasCropAnalyzed: false,
|
hasCropAnalyzed: false,
|
||||||
hasThumbnail: false,
|
hasThumbnail: false,
|
||||||
|
galleryExportPending: false,
|
||||||
cloudState: CloudState.none,
|
cloudState: CloudState.none,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
420
pubspec.lock
420
pubspec.lock
File diff suppressed because it is too large
Load diff
|
|
@ -71,7 +71,9 @@ dependencies:
|
||||||
permission_handler: ^12.0.0+1 # 2 mio
|
permission_handler: ^12.0.0+1 # 2 mio
|
||||||
|
|
||||||
# Not yet checked
|
# Not yet checked
|
||||||
cryptography_flutter_plus: ^3.0.0
|
# 3.1.0 has a broken AGP 9 build script: it skips the Kotlin plugin but still
|
||||||
|
# configures the missing `kotlin` extension.
|
||||||
|
cryptography_flutter_plus: 3.0.0
|
||||||
cryptography_plus: ^3.0.0
|
cryptography_plus: ^3.0.0
|
||||||
flutter_android_volume_keydown: ^1.0.1
|
flutter_android_volume_keydown: ^1.0.1
|
||||||
flutter_volume_controller: ^1.3.4
|
flutter_volume_controller: ^1.3.4
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,20 @@ impl RustApi {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts the optional location window for a Memory row created by the
|
||||||
|
/// local save button rather than the outgoing-media initializer.
|
||||||
|
pub async fn initialize_memory_location(media_id: String) -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
crate::services::location_metadata::initialize_for_media(ctx, &media_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begins the optional precise-location lookup when the capture camera is
|
||||||
|
/// opened, so a later photo usually has a fix immediately available.
|
||||||
|
pub async fn prewarm_memory_location() -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
crate::services::location_metadata::prewarm(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates one outgoing message per selected group and starts the upload.
|
/// Creates one outgoing message per selected group and starts the upload.
|
||||||
pub async fn send_media_to_groups(
|
pub async fn send_media_to_groups(
|
||||||
media_id: String,
|
media_id: String,
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,17 @@ impl UserConfigApi {
|
||||||
|
|
||||||
pub async fn update(base: UserConfig, config: UserConfig) -> Result<UserConfig> {
|
pub async fn update(base: UserConfig, config: UserConfig) -> Result<UserConfig> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
|
let location_was_disabled =
|
||||||
|
base.store_location_in_memories && !config.store_location_in_memories;
|
||||||
let normalized = UserConfig::update_json(
|
let normalized = UserConfig::update_json(
|
||||||
ctx,
|
ctx,
|
||||||
&serde_json::to_string(&base)?,
|
&serde_json::to_string(&base)?,
|
||||||
&serde_json::to_string(&config)?,
|
&serde_json::to_string(&config)?,
|
||||||
)?;
|
)?;
|
||||||
let config: UserConfig = serde_json::from_str(&normalized)?;
|
let config: UserConfig = serde_json::from_str(&normalized)?;
|
||||||
|
if location_was_disabled {
|
||||||
|
crate::services::location_metadata::disable_pending(ctx).await?;
|
||||||
|
}
|
||||||
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
|
||||||
(callbacks.api.user_config_changed)(config.clone()).await;
|
(callbacks.api.user_config_changed)(config.clone()).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
rust/src/database/app/migrations/0023_memory_locations.sql
Normal file
16
rust/src/database/app/migrations/0023_memory_locations.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- Optional capture location for a locally stored Memory. Coordinates never
|
||||||
|
-- leave the device through the messaging path; they are only written to a
|
||||||
|
-- gallery export after the user explicitly enables the feature.
|
||||||
|
ALTER TABLE media_files ADD COLUMN location_latitude REAL;
|
||||||
|
ALTER TABLE media_files ADD COLUMN location_longitude REAL;
|
||||||
|
ALTER TABLE media_files ADD COLUMN location_accuracy REAL;
|
||||||
|
ALTER TABLE media_files ADD COLUMN location_status TEXT
|
||||||
|
CHECK (location_status IN ('pending', 'resolved', 'timedOut'));
|
||||||
|
ALTER TABLE media_files ADD COLUMN location_deadline_at INTEGER;
|
||||||
|
ALTER TABLE media_files ADD COLUMN gallery_export_pending INTEGER NOT NULL DEFAULT 0
|
||||||
|
CHECK (gallery_export_pending IN (0, 1));
|
||||||
|
|
||||||
|
CREATE INDEX idx_media_files_pending_location
|
||||||
|
ON media_files(location_status, location_deadline_at)
|
||||||
|
WHERE location_status = 'pending';
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ mod legacy_import;
|
||||||
pub mod tables;
|
pub mod tables;
|
||||||
|
|
||||||
pub const APP_DATABASE_FILE: &str = "app_db.sqlite";
|
pub const APP_DATABASE_FILE: &str = "app_db.sqlite";
|
||||||
pub const APP_SCHEMA_VERSION: i64 = 8;
|
pub const APP_SCHEMA_VERSION: i64 = 9;
|
||||||
|
|
||||||
/// User-owned application tables in the current Rust schema. Rust-only outbox
|
/// User-owned application tables in the current Rust schema. Rust-only outbox
|
||||||
/// tables are deliberately absent because they are reconstructed locally.
|
/// tables are deliberately absent because they are reconstructed locally.
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1591152700;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 101877534;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
|
|
@ -2568,6 +2568,44 @@ fn wire__crate__bridge__api__rust_api_initialize_media_upload_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__api__rust_api_initialize_memory_location_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_api_initialize_memory_location",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_media_id = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok =
|
||||||
|
crate::bridge::api::RustApi::initialize_memory_location(api_media_id)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(
|
fn wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -3050,6 +3088,42 @@ fn wire__crate__bridge__api__rust_api_prerender_media_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_api_prewarm_memory_location",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok =
|
||||||
|
crate::bridge::api::RustApi::prewarm_memory_location().await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(
|
fn wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -7320,6 +7394,7 @@ impl SseDecode for crate::user_config::UserConfig {
|
||||||
let mut var_autoDownloadOptions =
|
let mut var_autoDownloadOptions =
|
||||||
<Option<std::collections::HashMap<String, Vec<String>>>>::sse_decode(deserializer);
|
<Option<std::collections::HashMap<String, Vec<String>>>>::sse_decode(deserializer);
|
||||||
let mut var_storeMediaFilesInGallery = <bool>::sse_decode(deserializer);
|
let mut var_storeMediaFilesInGallery = <bool>::sse_decode(deserializer);
|
||||||
|
let mut var_storeLocationInMemories = <bool>::sse_decode(deserializer);
|
||||||
let mut var_autoStoreAllSendUnlimitedMediaFiles = <bool>::sse_decode(deserializer);
|
let mut var_autoStoreAllSendUnlimitedMediaFiles = <bool>::sse_decode(deserializer);
|
||||||
let mut var_typingIndicators = <bool>::sse_decode(deserializer);
|
let mut var_typingIndicators = <bool>::sse_decode(deserializer);
|
||||||
let mut var_showRestoreFlame = <bool>::sse_decode(deserializer);
|
let mut var_showRestoreFlame = <bool>::sse_decode(deserializer);
|
||||||
|
|
@ -7387,6 +7462,7 @@ impl SseDecode for crate::user_config::UserConfig {
|
||||||
pre_selected_emojies: var_preSelectedEmojies,
|
pre_selected_emojies: var_preSelectedEmojies,
|
||||||
auto_download_options: var_autoDownloadOptions,
|
auto_download_options: var_autoDownloadOptions,
|
||||||
store_media_files_in_gallery: var_storeMediaFilesInGallery,
|
store_media_files_in_gallery: var_storeMediaFilesInGallery,
|
||||||
|
store_location_in_memories: var_storeLocationInMemories,
|
||||||
auto_store_all_send_unlimited_media_files: var_autoStoreAllSendUnlimitedMediaFiles,
|
auto_store_all_send_unlimited_media_files: var_autoStoreAllSendUnlimitedMediaFiles,
|
||||||
typing_indicators: var_typingIndicators,
|
typing_indicators: var_typingIndicators,
|
||||||
show_restore_flame: var_showRestoreFlame,
|
show_restore_flame: var_showRestoreFlame,
|
||||||
|
|
@ -7617,103 +7693,105 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||||
67 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
|
67 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
|
||||||
68 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len),
|
68 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len),
|
||||||
69 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len),
|
69 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len),
|
||||||
70 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
|
70 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len),
|
||||||
71 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
|
71 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
|
||||||
72 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
|
72 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
|
||||||
73 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
|
73 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
|
||||||
74 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
|
74 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
|
||||||
75 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
|
75 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
|
||||||
76 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len),
|
76 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
|
||||||
77 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len),
|
77 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len),
|
||||||
78 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len),
|
78 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len),
|
||||||
79 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
|
79 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len),
|
||||||
80 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
|
80 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
|
||||||
81 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len),
|
81 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
|
||||||
82 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
|
82 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
83 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
|
83 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len),
|
||||||
84 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
|
84 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
|
||||||
85 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
|
85 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
86 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
86 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
|
||||||
87 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
|
87 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
|
||||||
88 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
|
88 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
||||||
89 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
|
89 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
|
||||||
90 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
|
90 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
|
||||||
91 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
|
91 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
|
||||||
92 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
|
92 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
|
||||||
93 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
|
93 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
|
||||||
94 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
|
94 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
|
||||||
95 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
|
95 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
|
||||||
96 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
|
96 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
|
||||||
97 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
|
97 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
|
||||||
98 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
|
98 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
|
||||||
99 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
|
99 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
|
||||||
100 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
|
100 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
|
||||||
101 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
|
101 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
102 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
|
102 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
|
||||||
103 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
|
103 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
|
||||||
104 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
|
104 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
|
||||||
105 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
|
105 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
|
||||||
106 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
|
106 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
|
||||||
107 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
|
107 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
|
||||||
108 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
|
108 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
|
||||||
109 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
|
109 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
|
||||||
110 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
|
110 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
|
||||||
111 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
|
111 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
|
||||||
112 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
|
112 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
|
||||||
113 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
|
113 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
|
||||||
114 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
|
114 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
|
||||||
115 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
|
115 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
|
||||||
116 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
|
116 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
|
||||||
117 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
|
117 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
|
||||||
118 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
|
118 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
119 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
|
119 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
|
||||||
120 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
|
120 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
|
||||||
121 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
121 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
|
||||||
122 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
|
122 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
123 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
|
123 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
||||||
124 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
|
124 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
|
||||||
125 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
|
125 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
126 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
|
126 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
127 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
|
127 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
|
||||||
128 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
|
128 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
|
||||||
129 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
|
129 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
|
||||||
130 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
130 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
|
||||||
131 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
|
131 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
|
||||||
132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
132 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
||||||
133 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
|
133 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
|
||||||
134 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
134 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
||||||
135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
|
135 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
136 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
|
137 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
|
||||||
138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
138 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
139 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
139 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
|
||||||
140 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
140 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
141 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
142 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
|
143 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||||
144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
144 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
|
145 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
|
||||||
146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
146 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||||
147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
147 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
|
||||||
148 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
|
148 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
||||||
149 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
|
149 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
150 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
150 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||||
151 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
151 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||||
152 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
152 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||||
153 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
153 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
154 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
154 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
155 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
155 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
156 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
156 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||||
157 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
157 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||||
158 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
158 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||||
159 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
159 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||||
160 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
160 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||||
161 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
161 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
||||||
163 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
162 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||||
164 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
163 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
||||||
165 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
165 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
||||||
166 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
166 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
||||||
167 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
167 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
168 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
169 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7731,12 +7809,12 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||||
47 => {
|
47 => {
|
||||||
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
||||||
}
|
}
|
||||||
162 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
164 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
||||||
ptr,
|
ptr,
|
||||||
rust_vec_len,
|
rust_vec_len,
|
||||||
data_len,
|
data_len,
|
||||||
),
|
),
|
||||||
168 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
170 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8571,6 +8649,7 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig {
|
||||||
self.store_media_files_in_gallery
|
self.store_media_files_in_gallery
|
||||||
.into_into_dart()
|
.into_into_dart()
|
||||||
.into_dart(),
|
.into_dart(),
|
||||||
|
self.store_location_in_memories.into_into_dart().into_dart(),
|
||||||
self.auto_store_all_send_unlimited_media_files
|
self.auto_store_all_send_unlimited_media_files
|
||||||
.into_into_dart()
|
.into_into_dart()
|
||||||
.into_dart(),
|
.into_dart(),
|
||||||
|
|
@ -9688,6 +9767,7 @@ impl SseEncode for crate::user_config::UserConfig {
|
||||||
serializer,
|
serializer,
|
||||||
);
|
);
|
||||||
<bool>::sse_encode(self.store_media_files_in_gallery, serializer);
|
<bool>::sse_encode(self.store_media_files_in_gallery, serializer);
|
||||||
|
<bool>::sse_encode(self.store_location_in_memories, serializer);
|
||||||
<bool>::sse_encode(self.auto_store_all_send_unlimited_media_files, serializer);
|
<bool>::sse_encode(self.auto_store_all_send_unlimited_media_files, serializer);
|
||||||
<bool>::sse_encode(self.typing_indicators, serializer);
|
<bool>::sse_encode(self.typing_indicators, serializer);
|
||||||
<bool>::sse_encode(self.show_restore_flame, serializer);
|
<bool>::sse_encode(self.show_restore_flame, serializer);
|
||||||
|
|
|
||||||
96
rust/src/native/location.rs
Normal file
96
rust/src/native/location.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! One-shot, precise foreground location access implemented by the host app.
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
|
use crate::error::TwonlyError;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct LocationFix {
|
||||||
|
pub latitude: f64,
|
||||||
|
pub longitude: f64,
|
||||||
|
pub accuracy: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "ios")]
|
||||||
|
pub(crate) fn current(timeout: Duration) -> Result<Option<LocationFix>> {
|
||||||
|
type CurrentLocation = unsafe extern "C" fn(i64, *mut f64, *mut f64, *mut f64) -> bool;
|
||||||
|
|
||||||
|
let callback = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"twonly_current_location".as_ptr()) };
|
||||||
|
if callback.is_null() {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"iOS location service is unavailable".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// SAFETY: the Swift @_cdecl declaration has this exact stable C ABI signature.
|
||||||
|
let callback: CurrentLocation = unsafe { std::mem::transmute(callback) };
|
||||||
|
let mut latitude = 0.0;
|
||||||
|
let mut longitude = 0.0;
|
||||||
|
let mut accuracy = 0.0;
|
||||||
|
// SAFETY: all output pointers remain valid until the synchronous call returns.
|
||||||
|
let found = unsafe {
|
||||||
|
callback(
|
||||||
|
timeout.as_millis().min(i64::MAX as u128) as i64,
|
||||||
|
&mut latitude,
|
||||||
|
&mut longitude,
|
||||||
|
&mut accuracy,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Ok(found.then_some(LocationFix {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
accuracy,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub(crate) fn current(timeout: Duration) -> Result<Option<LocationFix>> {
|
||||||
|
use crate::native::transfer::android::{jni_env, location_class};
|
||||||
|
use jni::objects::JString;
|
||||||
|
|
||||||
|
let class = location_class()?;
|
||||||
|
let mut env = jni_env()?;
|
||||||
|
let value = env
|
||||||
|
.call_static_method(
|
||||||
|
class,
|
||||||
|
"current",
|
||||||
|
"(J)Ljava/lang/String;",
|
||||||
|
&[jni::objects::JValue::Long(
|
||||||
|
timeout.as_millis().min(i64::MAX as u128) as i64,
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.and_then(|value| value.l());
|
||||||
|
if env.exception_check().unwrap_or(false) {
|
||||||
|
let _ = env.exception_describe();
|
||||||
|
let _ = env.exception_clear();
|
||||||
|
}
|
||||||
|
let value = value.map_err(|error| TwonlyError::Generic(error.to_string()))?;
|
||||||
|
if value.is_null() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let json: String = env
|
||||||
|
.get_string(&JString::from(value))
|
||||||
|
.map_err(|error| TwonlyError::Generic(error.to_string()))?
|
||||||
|
.into();
|
||||||
|
let values: Vec<f64> = serde_json::from_str(&json)?;
|
||||||
|
if values.len() != 3 {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"Android returned an invalid location".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Some(LocationFix {
|
||||||
|
latitude: values[0],
|
||||||
|
longitude: values[1],
|
||||||
|
accuracy: values[2],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub(crate) fn current(_timeout: Duration) -> Result<Option<LocationFix>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
pub(crate) mod background;
|
pub(crate) mod background;
|
||||||
pub(crate) mod gallery;
|
pub(crate) mod gallery;
|
||||||
pub(crate) mod image;
|
pub(crate) mod image;
|
||||||
|
pub(crate) mod location;
|
||||||
pub(crate) mod notifications;
|
pub(crate) mod notifications;
|
||||||
pub(crate) mod prepare;
|
pub(crate) mod prepare;
|
||||||
pub(crate) mod transfer;
|
pub(crate) mod transfer;
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ pub(crate) mod android {
|
||||||
const MEDIA_CODEC_CLASS: &str = "eu/twonly/media/NativeImageCodec";
|
const MEDIA_CODEC_CLASS: &str = "eu/twonly/media/NativeImageCodec";
|
||||||
const VIDEO_CODEC_CLASS: &str = "eu/twonly/media/NativeVideoCodec";
|
const VIDEO_CODEC_CLASS: &str = "eu/twonly/media/NativeVideoCodec";
|
||||||
const GALLERY_CLASS: &str = "eu/twonly/media/NativeGallery";
|
const GALLERY_CLASS: &str = "eu/twonly/media/NativeGallery";
|
||||||
|
const LOCATION_CLASS: &str = "eu/twonly/location/NativeLocation";
|
||||||
|
|
||||||
static JAVA_VM: OnceLock<JavaVM> = OnceLock::new();
|
static JAVA_VM: OnceLock<JavaVM> = OnceLock::new();
|
||||||
static TRANSFER: OnceLock<GlobalRef> = OnceLock::new();
|
static TRANSFER: OnceLock<GlobalRef> = OnceLock::new();
|
||||||
|
|
@ -51,6 +52,7 @@ pub(crate) mod android {
|
||||||
static MEDIA_CODEC: OnceLock<GlobalRef> = OnceLock::new();
|
static MEDIA_CODEC: OnceLock<GlobalRef> = OnceLock::new();
|
||||||
static VIDEO_CODEC: OnceLock<GlobalRef> = OnceLock::new();
|
static VIDEO_CODEC: OnceLock<GlobalRef> = OnceLock::new();
|
||||||
static GALLERY: OnceLock<GlobalRef> = OnceLock::new();
|
static GALLERY: OnceLock<GlobalRef> = OnceLock::new();
|
||||||
|
static LOCATION: OnceLock<GlobalRef> = OnceLock::new();
|
||||||
/// `JNI_OnLoad` runs before Rust logging exists, so failures there are
|
/// `JNI_OnLoad` runs before Rust logging exists, so failures there are
|
||||||
/// recorded rather than logged, and reported by the first call that needs
|
/// recorded rather than logged, and reported by the first call that needs
|
||||||
/// the class.
|
/// the class.
|
||||||
|
|
@ -71,6 +73,7 @@ pub(crate) mod android {
|
||||||
(MEDIA_CODEC_CLASS, &MEDIA_CODEC),
|
(MEDIA_CODEC_CLASS, &MEDIA_CODEC),
|
||||||
(VIDEO_CODEC_CLASS, &VIDEO_CODEC),
|
(VIDEO_CODEC_CLASS, &VIDEO_CODEC),
|
||||||
(GALLERY_CLASS, &GALLERY),
|
(GALLERY_CLASS, &GALLERY),
|
||||||
|
(LOCATION_CLASS, &LOCATION),
|
||||||
] {
|
] {
|
||||||
match env
|
match env
|
||||||
.find_class(name)
|
.find_class(name)
|
||||||
|
|
@ -123,6 +126,10 @@ pub(crate) mod android {
|
||||||
cached_class(&GALLERY, GALLERY_CLASS)
|
cached_class(&GALLERY, GALLERY_CLASS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn location_class() -> Result<&'static GlobalRef> {
|
||||||
|
cached_class(&LOCATION, LOCATION_CLASS)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn jni_env() -> Result<AttachGuard<'static>> {
|
pub(crate) fn jni_env() -> Result<AttachGuard<'static>> {
|
||||||
JAVA_VM
|
JAVA_VM
|
||||||
.get()
|
.get()
|
||||||
|
|
|
||||||
233
rust/src/services/location_metadata.rs
Normal file
233
rust/src/services/location_metadata.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! Optional, device-local location metadata for Memories.
|
||||||
|
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use crate::native::location::{self, LocationFix};
|
||||||
|
use crate::services::media_upload::MediaUploadService;
|
||||||
|
use crate::user_config::UserConfig;
|
||||||
|
use sqlx::FromRow;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Mutex as SyncMutex;
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
pub(crate) const LOCATION_WINDOW_SECONDS: i64 = 3 * 60;
|
||||||
|
|
||||||
|
static LOCATION_REQUEST: LazyLock<Mutex<Option<(SystemTime, LocationFix)>>> =
|
||||||
|
LazyLock::new(|| Mutex::new(None));
|
||||||
|
static LOCATION_IN_FLIGHT: LazyLock<SyncMutex<HashSet<String>>> = LazyLock::new(Default::default);
|
||||||
|
static CAMERA_WARMUP_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
struct CameraWarmup;
|
||||||
|
|
||||||
|
impl Drop for CameraWarmup {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
CAMERA_WARMUP_IN_FLIGHT.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InFlight(String);
|
||||||
|
|
||||||
|
impl Drop for InFlight {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Ok(mut active) = LOCATION_IN_FLIGHT.lock() {
|
||||||
|
active.remove(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow)]
|
||||||
|
struct PendingLocation {
|
||||||
|
media_id: String,
|
||||||
|
location_deadline_at: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts acquiring a fix as soon as the user opens a real capture camera.
|
||||||
|
/// Nothing is persisted until a media row exists; the result only warms the
|
||||||
|
/// in-process cache consumed by `initialize_for_media`.
|
||||||
|
pub(crate) fn prewarm(ctx: &Arc<Context>) -> Result<()> {
|
||||||
|
if !UserConfig::load_required_from(ctx)?.store_location_in_memories
|
||||||
|
|| CAMERA_WARMUP_IN_FLIGHT.swap(true, Ordering::AcqRel)
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _warmup = CameraWarmup;
|
||||||
|
let mut request = LOCATION_REQUEST.lock().await;
|
||||||
|
if request.as_ref().is_some_and(|(captured_at, _)| {
|
||||||
|
captured_at.elapsed().unwrap_or(Duration::MAX)
|
||||||
|
< Duration::from_secs(LOCATION_WINDOW_SECONDS as u64)
|
||||||
|
}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let timeout = Duration::from_secs(LOCATION_WINDOW_SECONDS as u64);
|
||||||
|
match tokio::task::spawn_blocking(move || location::current(timeout)).await {
|
||||||
|
Ok(Ok(Some(location))) => {
|
||||||
|
*request = Some((SystemTime::now(), location));
|
||||||
|
}
|
||||||
|
Ok(Ok(None)) => {}
|
||||||
|
Ok(Err(error)) => tracing::warn!(%error, "camera location warm-up failed"),
|
||||||
|
Err(error) => tracing::warn!(%error, "camera location worker failed"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn initialize_for_media(ctx: &Arc<Context>, media_id: &str) -> Result<()> {
|
||||||
|
let config = UserConfig::load_required_from(ctx)?;
|
||||||
|
if !config.store_location_in_memories {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let deadline = chrono::Utc::now().timestamp() + LOCATION_WINDOW_SECONDS;
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
sqlx::query(
|
||||||
|
r#"UPDATE media_files
|
||||||
|
SET location_status = 'pending', location_deadline_at = ?,
|
||||||
|
location_latitude = NULL, location_longitude = NULL,
|
||||||
|
location_accuracy = NULL
|
||||||
|
WHERE media_id = ?"#,
|
||||||
|
)
|
||||||
|
.bind(deadline)
|
||||||
|
.bind(media_id)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
schedule(ctx, media_id.to_owned(), deadline);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn schedule(ctx: &Arc<Context>, media_id: String, deadline: i64) {
|
||||||
|
let Ok(mut active) = LOCATION_IN_FLIGHT.lock() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !active.insert(media_id.clone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(active);
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _in_flight = InFlight(media_id.clone());
|
||||||
|
if let Err(error) = capture(&ctx, &media_id, deadline).await {
|
||||||
|
tracing::warn!(media_id, %error, "could not capture Memory location");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn capture(ctx: &Arc<Context>, media_id: &str, deadline: i64) -> Result<()> {
|
||||||
|
// One native request serves captures made close together. Besides saving
|
||||||
|
// battery, this avoids multiple CLLocationManager/LocationManager sessions
|
||||||
|
// racing for identical coordinates.
|
||||||
|
let mut request = LOCATION_REQUEST.lock().await;
|
||||||
|
if let Some((captured_at, location)) = *request {
|
||||||
|
let captured_before_deadline = captured_at
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|value| value.as_secs() <= deadline.max(0) as u64)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if captured_before_deadline
|
||||||
|
&& captured_at.elapsed().unwrap_or(Duration::MAX)
|
||||||
|
< Duration::from_secs(LOCATION_WINDOW_SECONDS as u64)
|
||||||
|
{
|
||||||
|
drop(request);
|
||||||
|
finish(ctx, media_id, Some(location)).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recalculate only after waiting for another Memory's request. Otherwise
|
||||||
|
// that wait would accidentally extend this Memory's three-minute window.
|
||||||
|
let remaining = deadline.saturating_sub(chrono::Utc::now().timestamp());
|
||||||
|
if remaining <= 0 {
|
||||||
|
drop(request);
|
||||||
|
finish(ctx, media_id, None).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let timeout = Duration::from_secs(remaining as u64);
|
||||||
|
let location = tokio::task::spawn_blocking(move || location::current(timeout))
|
||||||
|
.await
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("location worker failed: {error}")))??;
|
||||||
|
if let Some(location) = location {
|
||||||
|
*request = Some((SystemTime::now(), location));
|
||||||
|
}
|
||||||
|
drop(request);
|
||||||
|
finish(ctx, media_id, location).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finish(ctx: &Arc<Context>, media_id: &str, location: Option<LocationFix>) -> Result<()> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
if let Some(location) = location {
|
||||||
|
sqlx::query(
|
||||||
|
r#"UPDATE media_files
|
||||||
|
SET location_status = 'resolved', location_latitude = ?,
|
||||||
|
location_longitude = ?, location_accuracy = ?
|
||||||
|
WHERE media_id = ? AND location_status = 'pending'"#,
|
||||||
|
)
|
||||||
|
.bind(location.latitude)
|
||||||
|
.bind(location.longitude)
|
||||||
|
.bind(location.accuracy)
|
||||||
|
.bind(media_id)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE media_files SET location_status = 'timedOut' WHERE media_id = ? AND location_status = 'pending'",
|
||||||
|
)
|
||||||
|
.bind(media_id)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
drop(database);
|
||||||
|
MediaUploadService::new(ctx)
|
||||||
|
.finish_pending_gallery_export(media_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resumes pending foreground requests and releases exports whose three-minute
|
||||||
|
/// deadline elapsed while the app was not running.
|
||||||
|
pub(crate) async fn resume(ctx: &Arc<Context>) -> Result<()> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let pending = sqlx::query_as::<_, PendingLocation>(
|
||||||
|
r#"SELECT media_id, location_deadline_at
|
||||||
|
FROM media_files WHERE location_status = 'pending'"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
for item in pending {
|
||||||
|
if item.location_deadline_at <= now {
|
||||||
|
finish(ctx, &item.media_id, None).await?;
|
||||||
|
} else if ctx.is_flutter_runtime() {
|
||||||
|
schedule(ctx, item.media_id, item.location_deadline_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turning the option off must not leave already requested gallery exports in
|
||||||
|
/// limbo. Existing coordinates remain attached to their Memories.
|
||||||
|
pub(crate) async fn disable_pending(ctx: &Arc<Context>) -> Result<()> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let media_ids = sqlx::query_scalar::<_, String>(
|
||||||
|
"SELECT media_id FROM media_files WHERE location_status = 'pending'",
|
||||||
|
)
|
||||||
|
.fetch_all(&database.pool)
|
||||||
|
.await?;
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE media_files SET location_status = 'timedOut' WHERE location_status = 'pending'",
|
||||||
|
)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
for media_id in media_ids {
|
||||||
|
MediaUploadService::new(ctx)
|
||||||
|
.finish_pending_gallery_export(&media_id)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -21,10 +21,19 @@ const DATE_FORMAT: &str = "%Y:%m:%d %H:%M:%S";
|
||||||
|
|
||||||
const TAG_DATE_TIME: u16 = 0x0132;
|
const TAG_DATE_TIME: u16 = 0x0132;
|
||||||
const TAG_EXIF_IFD: u16 = 0x8769;
|
const TAG_EXIF_IFD: u16 = 0x8769;
|
||||||
|
const TAG_GPS_IFD: u16 = 0x8825;
|
||||||
const TAG_DATE_TIME_ORIGINAL: u16 = 0x9003;
|
const TAG_DATE_TIME_ORIGINAL: u16 = 0x9003;
|
||||||
const TAG_DATE_TIME_DIGITIZED: u16 = 0x9004;
|
const TAG_DATE_TIME_DIGITIZED: u16 = 0x9004;
|
||||||
|
const TAG_GPS_VERSION: u16 = 0x0000;
|
||||||
|
const TAG_GPS_LATITUDE_REF: u16 = 0x0001;
|
||||||
|
const TAG_GPS_LATITUDE: u16 = 0x0002;
|
||||||
|
const TAG_GPS_LONGITUDE_REF: u16 = 0x0003;
|
||||||
|
const TAG_GPS_LONGITUDE: u16 = 0x0004;
|
||||||
|
const TAG_GPS_HORIZONTAL_ERROR: u16 = 0x001f;
|
||||||
|
const TYPE_BYTE: u16 = 1;
|
||||||
const TYPE_ASCII: u16 = 2;
|
const TYPE_ASCII: u16 = 2;
|
||||||
const TYPE_LONG: u16 = 4;
|
const TYPE_LONG: u16 = 4;
|
||||||
|
const TYPE_RATIONAL: u16 = 5;
|
||||||
|
|
||||||
/// The `VP8X` flag bit that says an `EXIF` chunk is present.
|
/// The `VP8X` flag bit that says an `EXIF` chunk is present.
|
||||||
const FLAG_EXIF: u8 = 0b0000_1000;
|
const FLAG_EXIF: u8 = 0b0000_1000;
|
||||||
|
|
@ -34,14 +43,56 @@ const FLAG_ALPHA: u8 = 0b0001_0000;
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct ExifMetadata {
|
pub(crate) struct ExifMetadata {
|
||||||
pub created_at: Option<DateTime<Utc>>,
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
|
pub latitude: Option<f64>,
|
||||||
|
pub longitude: Option<f64>,
|
||||||
|
pub accuracy: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExifMetadata {
|
impl ExifMetadata {
|
||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.created_at.is_none()
|
self.created_at.is_none() && (self.latitude.is_none() || self.longitude.is_none())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the complete EXIF block from the WebP upload copy without decoding
|
||||||
|
/// or recompressing its pixels.
|
||||||
|
pub(crate) fn remove_exif_from_file(path: &std::path::Path) -> Result<()> {
|
||||||
|
let bytes = std::fs::read(path)?;
|
||||||
|
let stripped = without_exif(&bytes)?;
|
||||||
|
if stripped != bytes {
|
||||||
|
std::fs::write(path, stripped)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn without_exif(webp: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let chunks = parse_chunks(webp)?;
|
||||||
|
if !chunks.iter().any(|chunk| chunk.id == *b"EXIF") {
|
||||||
|
return Ok(webp.to_vec());
|
||||||
|
}
|
||||||
|
let mut body = Vec::new();
|
||||||
|
for chunk in chunks {
|
||||||
|
if chunk.id == *b"EXIF" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if chunk.id == *b"VP8X" {
|
||||||
|
let mut header = chunk.payload.to_vec();
|
||||||
|
if let Some(flags) = header.first_mut() {
|
||||||
|
*flags &= !FLAG_EXIF;
|
||||||
|
}
|
||||||
|
write_chunk(&mut body, &chunk.id, &header);
|
||||||
|
} else {
|
||||||
|
write_chunk(&mut body, &chunk.id, chunk.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut output = Vec::with_capacity(body.len() + 12);
|
||||||
|
output.extend_from_slice(b"RIFF");
|
||||||
|
output.extend_from_slice(&((body.len() + 4) as u32).to_le_bytes());
|
||||||
|
output.extend_from_slice(b"WEBP");
|
||||||
|
output.extend_from_slice(&body);
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the WebP bytes with an `EXIF` chunk attached, converting a simple
|
/// Returns the WebP bytes with an `EXIF` chunk attached, converting a simple
|
||||||
/// file to the extended layout when needed.
|
/// file to the extended layout when needed.
|
||||||
pub(crate) fn with_exif(webp: &[u8], metadata: &ExifMetadata) -> Result<Vec<u8>> {
|
pub(crate) fn with_exif(webp: &[u8], metadata: &ExifMetadata) -> Result<Vec<u8>> {
|
||||||
|
|
@ -169,69 +220,120 @@ fn write_chunk(output: &mut Vec<u8>, id: &[u8; 4], payload: &[u8]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a little-endian TIFF header with one IFD0 and one Exif sub-IFD.
|
/// Builds a little-endian TIFF header with Exif and GPS sub-IFDs.
|
||||||
fn tiff_block(metadata: &ExifMetadata) -> Vec<u8> {
|
fn tiff_block(metadata: &ExifMetadata) -> Vec<u8> {
|
||||||
let mut ifd0: Vec<(u16, u16, Vec<u8>)> = Vec::new();
|
let mut ifd0: Vec<(u16, u16, Vec<u8>)> = Vec::new();
|
||||||
let mut exif: Vec<(u16, u16, Vec<u8>)> = Vec::new();
|
let mut exif: Vec<(u16, u16, Vec<u8>)> = Vec::new();
|
||||||
|
let mut gps: Vec<(u16, u16, Vec<u8>)> = Vec::new();
|
||||||
if let Some(created_at) = metadata.created_at {
|
if let Some(created_at) = metadata.created_at {
|
||||||
let text = format!("{}\0", created_at.format(DATE_FORMAT)).into_bytes();
|
let text = format!("{}\0", created_at.format(DATE_FORMAT)).into_bytes();
|
||||||
ifd0.push((TAG_DATE_TIME, TYPE_ASCII, text.clone()));
|
ifd0.push((TAG_DATE_TIME, TYPE_ASCII, text.clone()));
|
||||||
exif.push((TAG_DATE_TIME_ORIGINAL, TYPE_ASCII, text.clone()));
|
exif.push((TAG_DATE_TIME_ORIGINAL, TYPE_ASCII, text.clone()));
|
||||||
exif.push((TAG_DATE_TIME_DIGITIZED, TYPE_ASCII, text));
|
exif.push((TAG_DATE_TIME_DIGITIZED, TYPE_ASCII, text));
|
||||||
}
|
}
|
||||||
|
if let (Some(latitude), Some(longitude)) = (metadata.latitude, metadata.longitude) {
|
||||||
|
gps.push((TAG_GPS_VERSION, TYPE_BYTE, vec![2, 3, 0, 0]));
|
||||||
|
gps.push((
|
||||||
|
TAG_GPS_LATITUDE_REF,
|
||||||
|
TYPE_ASCII,
|
||||||
|
vec![if latitude < 0.0 { b'S' } else { b'N' }, 0],
|
||||||
|
));
|
||||||
|
gps.push((TAG_GPS_LATITUDE, TYPE_RATIONAL, coordinate(latitude)));
|
||||||
|
gps.push((
|
||||||
|
TAG_GPS_LONGITUDE_REF,
|
||||||
|
TYPE_ASCII,
|
||||||
|
vec![if longitude < 0.0 { b'W' } else { b'E' }, 0],
|
||||||
|
));
|
||||||
|
gps.push((TAG_GPS_LONGITUDE, TYPE_RATIONAL, coordinate(longitude)));
|
||||||
|
if let Some(accuracy) = metadata
|
||||||
|
.accuracy
|
||||||
|
.filter(|value| value.is_finite() && *value >= 0.0)
|
||||||
|
{
|
||||||
|
gps.push((TAG_GPS_HORIZONTAL_ERROR, TYPE_RATIONAL, rational(accuracy)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
output.extend_from_slice(b"II\x2a\x00");
|
output.extend_from_slice(b"II\x2a\x00");
|
||||||
output.extend_from_slice(&8_u32.to_le_bytes());
|
output.extend_from_slice(&8_u32.to_le_bytes());
|
||||||
|
|
||||||
// IFD0 gains a pointer entry to the Exif sub-IFD, so reserve room for it.
|
let pointer_count = usize::from(!exif.is_empty()) + usize::from(!gps.is_empty());
|
||||||
let ifd0_entries = ifd0.len() + usize::from(!exif.is_empty());
|
let ifd0_size = ifd_size(&ifd0, pointer_count);
|
||||||
let ifd0_start = 8;
|
let exif_ifd_offset = 8 + ifd0_size;
|
||||||
let ifd0_end = ifd0_start + 2 + ifd0_entries * 12 + 4;
|
let gps_ifd_offset = exif_ifd_offset + ifd_size(&exif, 0);
|
||||||
let mut values = Vec::new();
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
let mut value_offset = ifd0_end;
|
|
||||||
|
|
||||||
for (tag, kind, data) in &ifd0 {
|
|
||||||
entries.push(entry(*tag, *kind, data, &mut values, &mut value_offset));
|
|
||||||
}
|
|
||||||
// The Exif sub-IFD starts after IFD0 and its value block.
|
|
||||||
let exif_ifd_offset = ifd0_end + values.len();
|
|
||||||
if !exif.is_empty() {
|
if !exif.is_empty() {
|
||||||
entries.push(entry_inline(
|
ifd0.push((
|
||||||
TAG_EXIF_IFD,
|
TAG_EXIF_IFD,
|
||||||
TYPE_LONG,
|
TYPE_LONG,
|
||||||
1,
|
(exif_ifd_offset as u32).to_le_bytes().to_vec(),
|
||||||
(exif_ifd_offset as u32).to_le_bytes(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if !gps.is_empty() {
|
||||||
output.extend_from_slice(&(ifd0_entries as u16).to_le_bytes());
|
ifd0.push((
|
||||||
for bytes in &entries {
|
TAG_GPS_IFD,
|
||||||
output.extend_from_slice(bytes);
|
TYPE_LONG,
|
||||||
}
|
(gps_ifd_offset as u32).to_le_bytes().to_vec(),
|
||||||
output.extend_from_slice(&0_u32.to_le_bytes());
|
));
|
||||||
output.extend_from_slice(&values);
|
|
||||||
|
|
||||||
if !exif.is_empty() {
|
|
||||||
let sub_start = exif_ifd_offset;
|
|
||||||
let sub_end = sub_start + 2 + exif.len() * 12 + 4;
|
|
||||||
let mut sub_values = Vec::new();
|
|
||||||
let mut sub_entries = Vec::new();
|
|
||||||
let mut sub_offset = sub_end;
|
|
||||||
for (tag, kind, data) in &exif {
|
|
||||||
sub_entries.push(entry(*tag, *kind, data, &mut sub_values, &mut sub_offset));
|
|
||||||
}
|
|
||||||
output.extend_from_slice(&(exif.len() as u16).to_le_bytes());
|
|
||||||
for bytes in &sub_entries {
|
|
||||||
output.extend_from_slice(bytes);
|
|
||||||
}
|
|
||||||
output.extend_from_slice(&0_u32.to_le_bytes());
|
|
||||||
output.extend_from_slice(&sub_values);
|
|
||||||
}
|
}
|
||||||
|
output.extend_from_slice(&serialize_ifd(&ifd0, 8));
|
||||||
|
output.extend_from_slice(&serialize_ifd(&exif, exif_ifd_offset));
|
||||||
|
output.extend_from_slice(&serialize_ifd(&gps, gps_ifd_offset));
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ifd_size(entries: &[(u16, u16, Vec<u8>)], extra_entries: usize) -> usize {
|
||||||
|
if entries.is_empty() && extra_entries == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
2 + (entries.len() + extra_entries) * 12
|
||||||
|
+ 4
|
||||||
|
+ entries
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, data)| data.len() > 4)
|
||||||
|
.map(|(_, _, data)| data.len() + (data.len() & 1))
|
||||||
|
.sum::<usize>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_ifd(entries: &[(u16, u16, Vec<u8>)], start: usize) -> Vec<u8> {
|
||||||
|
if entries.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut values = Vec::new();
|
||||||
|
let mut encoded = Vec::new();
|
||||||
|
let mut value_offset = start + 2 + entries.len() * 12 + 4;
|
||||||
|
encoded.extend_from_slice(&(entries.len() as u16).to_le_bytes());
|
||||||
|
for (tag, kind, data) in entries {
|
||||||
|
encoded.extend_from_slice(&entry(*tag, *kind, data, &mut values, &mut value_offset));
|
||||||
|
}
|
||||||
|
encoded.extend_from_slice(&0_u32.to_le_bytes());
|
||||||
|
encoded.extend_from_slice(&values);
|
||||||
|
encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coordinate(value: f64) -> Vec<u8> {
|
||||||
|
let value = value.abs();
|
||||||
|
let degrees = value.floor();
|
||||||
|
let minutes_full = (value - degrees) * 60.0;
|
||||||
|
let minutes = minutes_full.floor();
|
||||||
|
let seconds = (minutes_full - minutes) * 60.0;
|
||||||
|
let mut bytes = Vec::with_capacity(24);
|
||||||
|
bytes.extend_from_slice(&rational(degrees));
|
||||||
|
bytes.extend_from_slice(&rational(minutes));
|
||||||
|
bytes.extend_from_slice(&rational(seconds));
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rational(value: f64) -> Vec<u8> {
|
||||||
|
const DENOMINATOR: u32 = 1_000_000;
|
||||||
|
let numerator = (value * f64::from(DENOMINATOR))
|
||||||
|
.round()
|
||||||
|
.clamp(0.0, f64::from(u32::MAX)) as u32;
|
||||||
|
let mut bytes = Vec::with_capacity(8);
|
||||||
|
bytes.extend_from_slice(&numerator.to_le_bytes());
|
||||||
|
bytes.extend_from_slice(&DENOMINATOR.to_le_bytes());
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
/// One 12-byte IFD entry. Values longer than four bytes live in the value block
|
/// One 12-byte IFD entry. Values longer than four bytes live in the value block
|
||||||
/// and the entry stores their offset instead.
|
/// and the entry stores their offset instead.
|
||||||
fn entry(
|
fn entry(
|
||||||
|
|
@ -244,7 +346,7 @@ fn entry(
|
||||||
if data.len() <= 4 {
|
if data.len() <= 4 {
|
||||||
let mut inline = [0_u8; 4];
|
let mut inline = [0_u8; 4];
|
||||||
inline[..data.len()].copy_from_slice(data);
|
inline[..data.len()].copy_from_slice(data);
|
||||||
return entry_inline(tag, kind, data.len() as u32, inline);
|
return entry_inline(tag, kind, component_count(kind, data), inline);
|
||||||
}
|
}
|
||||||
let offset = *value_offset;
|
let offset = *value_offset;
|
||||||
values.extend_from_slice(data);
|
values.extend_from_slice(data);
|
||||||
|
|
@ -253,7 +355,20 @@ fn entry(
|
||||||
*value_offset += 1;
|
*value_offset += 1;
|
||||||
}
|
}
|
||||||
*value_offset += data.len();
|
*value_offset += data.len();
|
||||||
entry_inline(tag, kind, data.len() as u32, (offset as u32).to_le_bytes())
|
entry_inline(
|
||||||
|
tag,
|
||||||
|
kind,
|
||||||
|
component_count(kind, data),
|
||||||
|
(offset as u32).to_le_bytes(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn component_count(kind: u16, data: &[u8]) -> u32 {
|
||||||
|
match kind {
|
||||||
|
TYPE_LONG => (data.len() / 4) as u32,
|
||||||
|
TYPE_RATIONAL => (data.len() / 8) as u32,
|
||||||
|
_ => data.len() as u32,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entry_inline(tag: u16, kind: u16, count: u32, value: [u8; 4]) -> [u8; 12] {
|
fn entry_inline(tag: u16, kind: u16, count: u32, value: [u8; 4]) -> [u8; 12] {
|
||||||
|
|
@ -295,6 +410,7 @@ mod tests {
|
||||||
let webp = sample_webp();
|
let webp = sample_webp();
|
||||||
let metadata = ExifMetadata {
|
let metadata = ExifMetadata {
|
||||||
created_at: Some(Utc.with_ymd_and_hms(2026, 8, 30, 12, 34, 56).unwrap()),
|
created_at: Some(Utc.with_ymd_and_hms(2026, 8, 30, 12, 34, 56).unwrap()),
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
let written = with_exif(&webp, &metadata).unwrap();
|
let written = with_exif(&webp, &metadata).unwrap();
|
||||||
|
|
||||||
|
|
@ -325,6 +441,7 @@ mod tests {
|
||||||
fn writing_twice_replaces_rather_than_appends() {
|
fn writing_twice_replaces_rather_than_appends() {
|
||||||
let metadata = ExifMetadata {
|
let metadata = ExifMetadata {
|
||||||
created_at: Some(Utc.with_ymd_and_hms(2026, 1, 2, 3, 4, 5).unwrap()),
|
created_at: Some(Utc.with_ymd_and_hms(2026, 1, 2, 3, 4, 5).unwrap()),
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
let once = with_exif(&sample_webp(), &metadata).unwrap();
|
let once = with_exif(&sample_webp(), &metadata).unwrap();
|
||||||
let twice = with_exif(&once, &metadata).unwrap();
|
let twice = with_exif(&once, &metadata).unwrap();
|
||||||
|
|
@ -333,4 +450,34 @@ mod tests {
|
||||||
assert_eq!(chunks.iter().filter(|c| c.id == *b"EXIF").count(), 1);
|
assert_eq!(chunks.iter().filter(|c| c.id == *b"EXIF").count(), 1);
|
||||||
assert_eq!(chunks.iter().filter(|c| c.id == *b"VP8X").count(), 1);
|
assert_eq!(chunks.iter().filter(|c| c.id == *b"VP8X").count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writes_gps_metadata_and_removes_it_from_the_send_copy() {
|
||||||
|
let metadata = ExifMetadata {
|
||||||
|
created_at: Some(Utc.with_ymd_and_hms(2026, 9, 10, 12, 0, 0).unwrap()),
|
||||||
|
latitude: Some(52.520_008),
|
||||||
|
longitude: Some(13.404_954),
|
||||||
|
accuracy: Some(8.5),
|
||||||
|
};
|
||||||
|
let exported = with_exif(&sample_webp(), &metadata).unwrap();
|
||||||
|
let chunks = parse_chunks(&exported).unwrap();
|
||||||
|
let exif = chunks
|
||||||
|
.iter()
|
||||||
|
.find(|chunk| chunk.id == *b"EXIF")
|
||||||
|
.expect("EXIF chunk");
|
||||||
|
assert!(
|
||||||
|
exif.payload.windows(2).any(|value| value == [0x25, 0x88]),
|
||||||
|
"IFD0 must point at a GPS IFD"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
exif.payload.windows(2).any(|value| value == [0x02, 0x00]),
|
||||||
|
"GPS IFD must contain latitude"
|
||||||
|
);
|
||||||
|
|
||||||
|
let upload = without_exif(&exported).unwrap();
|
||||||
|
let upload_chunks = parse_chunks(&upload).unwrap();
|
||||||
|
assert!(!upload_chunks.iter().any(|chunk| chunk.id == *b"EXIF"));
|
||||||
|
assert_eq!(upload_chunks[0].payload[0] & FLAG_EXIF, 0);
|
||||||
|
image::load_from_memory(&upload).expect("stripped image remains decodable");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,16 @@ struct MediaRow {
|
||||||
created_at: i64,
|
created_at: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow)]
|
||||||
|
struct GalleryLocationRow {
|
||||||
|
location_status: Option<String>,
|
||||||
|
location_deadline_at: Option<i64>,
|
||||||
|
location_latitude: Option<f64>,
|
||||||
|
location_longitude: Option<f64>,
|
||||||
|
location_accuracy: Option<f64>,
|
||||||
|
gallery_export_pending: i64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Why a media send stopped at `fileLimitReached`, for the chat entry to show.
|
/// Why a media send stopped at `fileLimitReached`, for the chat entry to show.
|
||||||
pub struct MediaSizeReport {
|
pub struct MediaSizeReport {
|
||||||
/// The encoded media as it sits on disk, while it is still there.
|
/// The encoded media as it sits on disk, while it is still there.
|
||||||
|
|
@ -147,6 +157,7 @@ impl MediaUploadService {
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
crate::services::location_metadata::initialize_for_media(&self.ctx, &media_id).await?;
|
||||||
Ok(media_id)
|
Ok(media_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,6 +411,14 @@ impl MediaUploadService {
|
||||||
self.store(media_id).await?;
|
self.store(media_id).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The stored copy is device-local and may later gain EXIF metadata.
|
||||||
|
// The upload copy is a separate file and is always scrubbed before
|
||||||
|
// encryption, including when it originated from an imported Memory.
|
||||||
|
if media.media_type == "image" {
|
||||||
|
let path = temp_path.clone();
|
||||||
|
blocking(move || media_exif::remove_exif_from_file(&path)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
match DirectMediaUploadService::new(&self.ctx)
|
match DirectMediaUploadService::new(&self.ctx)
|
||||||
.prepare_and_schedule(media_id)
|
.prepare_and_schedule(media_id)
|
||||||
.await
|
.await
|
||||||
|
|
@ -439,6 +458,7 @@ impl MediaUploadService {
|
||||||
/// settles the transfers the server has meanwhile accepted or rejected.
|
/// settles the transfers the server has meanwhile accepted or rejected.
|
||||||
pub async fn finish_started_uploads(&self) -> Result<()> {
|
pub async fn finish_started_uploads(&self) -> Result<()> {
|
||||||
let _guard = self.ctx.media_preprocessing.lock().await;
|
let _guard = self.ctx.media_preprocessing.lock().await;
|
||||||
|
crate::services::location_metadata::resume(&self.ctx).await?;
|
||||||
let direct = DirectMediaUploadService::new(&self.ctx);
|
let direct = DirectMediaUploadService::new(&self.ctx);
|
||||||
if let Err(error) = direct.reconcile().await {
|
if let Err(error) = direct.reconcile().await {
|
||||||
tracing::warn!(%error, "direct media reconciliation failed");
|
tracing::warn!(%error, "direct media reconciliation failed");
|
||||||
|
|
@ -647,13 +667,6 @@ impl MediaUploadService {
|
||||||
let Some(media) = self.load(media_id).await? else {
|
let Some(media) = self.load(media_id).await? else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let database = self.ctx.app_db.read().await.clone();
|
|
||||||
sqlx::query("UPDATE media_files SET stored = 1 WHERE media_id = ?")
|
|
||||||
.bind(media_id)
|
|
||||||
.execute(&database.pool)
|
|
||||||
.await?;
|
|
||||||
drop(database);
|
|
||||||
|
|
||||||
let files = MediaFileService::new(&self.ctx);
|
let files = MediaFileService::new(&self.ctx);
|
||||||
let temp_path = files.temp_path(media_id, &media.media_type);
|
let temp_path = files.temp_path(media_id, &media.media_type);
|
||||||
let stored_path = files.stored_path(media_id, &media.media_type);
|
let stored_path = files.stored_path(media_id, &media.media_type);
|
||||||
|
|
@ -676,14 +689,13 @@ impl MediaUploadService {
|
||||||
self.create_thumbnail(&media).await?;
|
self.create_thumbnail(&media).await?;
|
||||||
// Trust the filesystem rather than what the encoder reported: a
|
// Trust the filesystem rather than what the encoder reported: a
|
||||||
// thumbnail that was not written must not be advertised to the UI.
|
// thumbnail that was not written must not be advertised to the UI.
|
||||||
if files.thumbnail_path(media_id).exists() {
|
let has_thumbnail = i64::from(files.thumbnail_path(media_id).exists());
|
||||||
self.update_media(
|
self.update_media(
|
||||||
"UPDATE media_files SET has_thumbnail = ? WHERE media_id = ?",
|
"UPDATE media_files SET stored = 1, has_thumbnail = ? WHERE media_id = ?",
|
||||||
1_i64,
|
has_thumbnail,
|
||||||
media_id,
|
media_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
|
||||||
self.refresh_stored_metadata(media_id, &stored_path).await
|
self.refresh_stored_metadata(media_id, &stored_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1265,7 +1277,7 @@ impl MediaUploadService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the preview shown in chat lists and the gallery. /// Renders the preview shown in chat lists and the gallery.
|
/// Renders the preview shown in chat lists and the gallery.
|
||||||
async fn create_thumbnail(&self, media: &MediaRow) -> Result<()> {
|
async fn create_thumbnail(&self, media: &MediaRow) -> Result<()> {
|
||||||
let files = MediaFileService::new(&self.ctx);
|
let files = MediaFileService::new(&self.ctx);
|
||||||
let stored = files.stored_path(&media.media_id, &media.media_type);
|
let stored = files.stored_path(&media.media_id, &media.media_type);
|
||||||
|
|
@ -1295,9 +1307,83 @@ impl MediaUploadService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Exports a stored media file to the user's photo library, stamping the
|
/// Exports a stored media file, or durably queues it while its optional
|
||||||
/// capture time into the file so it survives being copied elsewhere.
|
/// location is still being resolved.
|
||||||
pub async fn save_to_gallery(&self, media_id: &str) -> Result<()> {
|
pub async fn save_to_gallery(&self, media_id: &str) -> Result<()> {
|
||||||
|
let database = self.ctx.app_db.read().await.clone();
|
||||||
|
let location = sqlx::query_as::<_, GalleryLocationRow>(
|
||||||
|
r#"SELECT location_status, location_deadline_at, location_latitude,
|
||||||
|
location_longitude, location_accuracy, gallery_export_pending
|
||||||
|
FROM media_files WHERE media_id = ?"#,
|
||||||
|
)
|
||||||
|
.bind(media_id)
|
||||||
|
.fetch_optional(&database.pool)
|
||||||
|
.await?;
|
||||||
|
let Some(location) = location else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if location.location_status.as_deref() == Some("pending")
|
||||||
|
&& location.location_deadline_at.unwrap_or_default() > chrono::Utc::now().timestamp()
|
||||||
|
{
|
||||||
|
sqlx::query("UPDATE media_files SET gallery_export_pending = 1 WHERE media_id = ?")
|
||||||
|
.bind(media_id)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
if let Some(deadline) = location.location_deadline_at {
|
||||||
|
crate::services::location_metadata::schedule(
|
||||||
|
&self.ctx,
|
||||||
|
media_id.to_owned(),
|
||||||
|
deadline,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if location.location_status.as_deref() == Some("pending") {
|
||||||
|
sqlx::query("UPDATE media_files SET location_status = 'timedOut' WHERE media_id = ?")
|
||||||
|
.bind(media_id)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
drop(database);
|
||||||
|
self.export_to_gallery_now(media_id, location).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn finish_pending_gallery_export(&self, media_id: &str) -> Result<()> {
|
||||||
|
let database = self.ctx.app_db.read().await.clone();
|
||||||
|
let location = sqlx::query_as::<_, GalleryLocationRow>(
|
||||||
|
r#"SELECT location_status, location_deadline_at, location_latitude,
|
||||||
|
location_longitude, location_accuracy, gallery_export_pending
|
||||||
|
FROM media_files WHERE media_id = ?"#,
|
||||||
|
)
|
||||||
|
.bind(media_id)
|
||||||
|
.fetch_optional(&database.pool)
|
||||||
|
.await?;
|
||||||
|
let Some(location) = location else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if location.gallery_export_pending == 0
|
||||||
|
|| location.location_status.as_deref() == Some("pending")
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// This flag represents waiting for location, not a general gallery
|
||||||
|
// retry queue. Clear it before handing the export to the platform.
|
||||||
|
sqlx::query("UPDATE media_files SET gallery_export_pending = 0 WHERE media_id = ?")
|
||||||
|
.bind(media_id)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
self.export_to_gallery_now(media_id, location).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stamps capture time and the locally stored location into a temporary
|
||||||
|
/// export. The Memory itself remains untouched.
|
||||||
|
async fn export_to_gallery_now(
|
||||||
|
&self,
|
||||||
|
media_id: &str,
|
||||||
|
location: GalleryLocationRow,
|
||||||
|
) -> Result<()> {
|
||||||
let Some(media) = self.load(media_id).await? else {
|
let Some(media) = self.load(media_id).await? else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
@ -1319,6 +1405,9 @@ impl MediaUploadService {
|
||||||
} else {
|
} else {
|
||||||
let metadata = media_exif::ExifMetadata {
|
let metadata = media_exif::ExifMetadata {
|
||||||
created_at: chrono::DateTime::from_timestamp_millis(created_at_millis),
|
created_at: chrono::DateTime::from_timestamp_millis(created_at_millis),
|
||||||
|
latitude: location.location_latitude,
|
||||||
|
longitude: location.location_longitude,
|
||||||
|
accuracy: location.location_accuracy,
|
||||||
};
|
};
|
||||||
let bytes = std::fs::read(&stored)?;
|
let bytes = std::fs::read(&stored)?;
|
||||||
match media_exif::with_exif(&bytes, &metadata) {
|
match media_exif::with_exif(&bytes, &metadata) {
|
||||||
|
|
@ -1491,6 +1580,46 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A freshly saved Memory used to become visible to Flutter before its
|
||||||
|
/// source file existed. The one-shot repair added for the Memories viewer
|
||||||
|
/// then consumed its attempt too early and left the row without a preview.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn storing_a_memory_publishes_it_with_its_thumbnail() -> anyhow::Result<()> {
|
||||||
|
let temp = tempfile::tempdir()?;
|
||||||
|
let data_dir = temp.path().join("data");
|
||||||
|
std::fs::create_dir_all(data_dir.join("keyvalue"))?;
|
||||||
|
std::fs::write(
|
||||||
|
data_dir.join("keyvalue/user.json"),
|
||||||
|
serde_json::to_vec(&UserConfig::default())?,
|
||||||
|
)?;
|
||||||
|
let ctx = Context::init_for_testing(temp.path().join("database"), data_dir).await?;
|
||||||
|
|
||||||
|
let media_id = "new-memory";
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
sqlx::query("INSERT INTO media_files(media_id, type, stored) VALUES (?, 'image', 0)")
|
||||||
|
.bind(media_id)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
drop(database);
|
||||||
|
|
||||||
|
let files = MediaFileService::new(&ctx);
|
||||||
|
let source = files.temp_path(media_id, "image");
|
||||||
|
MediaFileService::ensure_parent(&source)?;
|
||||||
|
image::DynamicImage::ImageRgb8(image::RgbImage::new(900, 1600)).save(&source)?;
|
||||||
|
|
||||||
|
MediaUploadService::new(&ctx).store(media_id).await?;
|
||||||
|
|
||||||
|
assert!(files.thumbnail_path(media_id).exists());
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let state: (i64, i64) =
|
||||||
|
sqlx::query_as("SELECT stored, has_thumbnail FROM media_files WHERE media_id = ?")
|
||||||
|
.bind(media_id)
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(state, (1, 1));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn media_lock_is_shared_per_media_id() {
|
fn media_lock_is_shared_per_media_id() {
|
||||||
let first = media_lock("media-a");
|
let first = media_lock("media-a");
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ pub mod contacts;
|
||||||
pub mod direct_media_upload;
|
pub mod direct_media_upload;
|
||||||
pub mod groups;
|
pub mod groups;
|
||||||
pub mod home_widget;
|
pub mod home_widget;
|
||||||
|
pub mod location_metadata;
|
||||||
pub mod media_codec;
|
pub mod media_codec;
|
||||||
pub mod media_exif;
|
pub mod media_exif;
|
||||||
pub mod media_upload;
|
pub mod media_upload;
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,11 @@ pub struct UserConfig {
|
||||||
#[serde(default = "defaults::true_value")]
|
#[serde(default = "defaults::true_value")]
|
||||||
#[frb(non_final)]
|
#[frb(non_final)]
|
||||||
pub store_media_files_in_gallery: bool,
|
pub store_media_files_in_gallery: bool,
|
||||||
|
/// Adds the capture location to locally stored Memories and gallery
|
||||||
|
/// exports. Disabled by default and never added to outgoing media.
|
||||||
|
#[serde(default)]
|
||||||
|
#[frb(non_final)]
|
||||||
|
pub store_location_in_memories: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[frb(non_final)]
|
#[frb(non_final)]
|
||||||
pub auto_store_all_send_unlimited_media_files: bool,
|
pub auto_store_all_send_unlimited_media_files: bool,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ UserConfig testUserConfig({
|
||||||
showShowImagePreviewWhenSending: false,
|
showShowImagePreviewWhenSending: false,
|
||||||
startWithCameraOpen: true,
|
startWithCameraOpen: true,
|
||||||
storeMediaFilesInGallery: true,
|
storeMediaFilesInGallery: true,
|
||||||
|
storeLocationInMemories: false,
|
||||||
autoStoreAllSendUnlimitedMediaFiles: false,
|
autoStoreAllSendUnlimitedMediaFiles: false,
|
||||||
typingIndicators: true,
|
typingIndicators: true,
|
||||||
showRestoreFlame: true,
|
showRestoreFlame: true,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue