mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
fix multiple smaller issues
This commit is contained in:
parent
078676c8b9
commit
6d5a9c6a1f
35 changed files with 2066 additions and 356 deletions
10
README.md
10
README.md
|
|
@ -57,6 +57,16 @@ guarantee a bounty currently :/
|
|||
|
||||
Currently there are still some core features and rewrites open I want to do involving a huge rewrite in Rust. Because of this, contributions are currently not wanted. You can still view the code (if you find any security issues please contact me!). Also, issues are currently closed. If you find a bug, please use the in-app option as there you can upload your debug log which helps a lot.
|
||||
|
||||
## Custom dependencies
|
||||
|
||||
Vendored packages are tracked in `dependencies.yaml`. Set `custom_changes: true`
|
||||
on a dependency or an individual subpackage when it contains local modifications.
|
||||
`dependencies.py` preserves these folders during normal and `--cache-only`
|
||||
updates, and keeps their local versions and paths in the managed `pubspec.yaml`
|
||||
sections. Update these packages manually and merge upstream changes with the
|
||||
local modifications. Missing custom packages must be restored from the
|
||||
`dependencies` repository.
|
||||
|
||||
## Signing Keys
|
||||
|
||||
When you download the app **via GitHub or F-Droid** you can verify the signing keys using for example the [AppVerifyer](https://github.com/soupslurpr/AppVerifier) and the following SHA-256 fingerprint of the signing certificate.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ android {
|
|||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
manifestPlaceholders = [appName: "twonly"]
|
||||
}
|
||||
signingConfigs {
|
||||
|
|
@ -75,6 +76,8 @@ flutter {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'junit:junit:4.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||
implementation platform('com.google.firebase:firebase-bom:34.9.0')
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
package eu.twonly.location
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.location.Criteria
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import androidx.test.filters.SdkSuppress
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.json.JSONArray
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
/** Run on an emulator: the mock providers are removed after every test. */
|
||||
@SdkSuppress(minSdkVersion = 31)
|
||||
class NativeLocationTest {
|
||||
companion object {
|
||||
private var activityStarted = false
|
||||
}
|
||||
private val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
private val context = instrumentation.targetContext
|
||||
private val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
private val providers = listOf("fused", LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
instrumentation.uiAutomation.grantRuntimePermission(context.packageName, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
instrumentation.uiAutomation.grantRuntimePermission(context.packageName, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
if (!activityStarted) {
|
||||
instrumentation.startActivitySync(
|
||||
Intent(context, LocationTestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
activityStarted = true
|
||||
}
|
||||
instrumentation.uiAutomation.executeShellCommand(
|
||||
"appops set ${context.packageName} android:mock_location allow",
|
||||
).use { android.os.ParcelFileDescriptor.AutoCloseInputStream(it).readBytes() }
|
||||
for (provider in providers) {
|
||||
@Suppress("DEPRECATION")
|
||||
manager.addTestProvider(provider, false, false, false, false, true, true, true,
|
||||
Criteria.POWER_LOW, Criteria.ACCURACY_FINE)
|
||||
manager.setTestProviderEnabled(provider, true)
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
for (provider in providers) {
|
||||
runCatching { manager.removeTestProvider(provider) }
|
||||
}
|
||||
instrumentation.uiAutomation.executeShellCommand(
|
||||
"appops set ${context.packageName} android:mock_location default",
|
||||
).use { android.os.ParcelFileDescriptor.AutoCloseInputStream(it).readBytes() }
|
||||
}
|
||||
|
||||
private fun fix(provider: String, ageMillis: Long = 0, accuracyMeters: Float = 10f): Location =
|
||||
Location(provider).apply {
|
||||
latitude = 52.52
|
||||
longitude = 13.405
|
||||
accuracy = accuracyMeters
|
||||
time = System.currentTimeMillis() - ageMillis
|
||||
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() - ageMillis * 1_000_000
|
||||
}
|
||||
|
||||
private fun assertFix(value: String?) {
|
||||
assertNotNull("A recent precise fix should be returned", value)
|
||||
val result = JSONArray(value!!)
|
||||
assertEquals(52.52, result.getDouble(0), 0.000001)
|
||||
assertEquals(13.405, result.getDouble(1), 0.000001)
|
||||
assertEquals(10.0, result.getDouble(2), 0.000001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recentGpsFixIsReturnedWithoutWaitingForAnUpdate() {
|
||||
manager.setTestProviderLocation(LocationManager.GPS_PROVIDER, fix(LocationManager.GPS_PROVIDER))
|
||||
assertFix(NativeLocation.current(500))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recentFusedFixIsReturnedWithoutWaitingForGps() {
|
||||
manager.setTestProviderLocation("fused", fix("fused"))
|
||||
assertFix(NativeLocation.current(500))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun staleCachedFixIsNotStoredAsTheCaptureLocation() {
|
||||
manager.setTestProviderLocation(LocationManager.GPS_PROVIDER, fix(LocationManager.GPS_PROVIDER, 240_000))
|
||||
assertNull(NativeLocation.current(200))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun liveFusedUpdateIsUsedWhenThereIsNoCachedFix() {
|
||||
manager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, false)
|
||||
manager.setTestProviderEnabled(LocationManager.NETWORK_PROVIDER, false)
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
manager.setTestProviderLocation("fused", fix("fused"))
|
||||
}, 100)
|
||||
assertFix(NativeLocation.current(2_000))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bestAvailableFixIsReturnedAtTheDeadline() {
|
||||
manager.setTestProviderLocation(LocationManager.NETWORK_PROVIDER,
|
||||
fix(LocationManager.NETWORK_PROVIDER, accuracyMeters = 100f))
|
||||
val value = NativeLocation.current(200)
|
||||
assertNotNull(value)
|
||||
assertEquals(100.0, JSONArray(value!!).getDouble(2), 0.000001)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disabledProvidersReturnWithoutARequest() {
|
||||
for (provider in providers) manager.setTestProviderEnabled(provider, false)
|
||||
assertNull(NativeLocation.current(200))
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application android:usesCleartextTraffic="true" >
|
||||
<activity android:name="eu.twonly.location.LocationTestActivity" android:exported="false" />
|
||||
<!-- // https://github.com/juliansteenbakker/mobile_scanner/issues/553 -->
|
||||
<meta-data android:name="firebase_performance_collection_deactivated" android:value="true" />
|
||||
<!-- <service
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package eu.twonly.location
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
/** Keeps native location instrumentation in the foreground without starting Flutter. */
|
||||
class LocationTestActivity : Activity()
|
||||
|
|
@ -7,53 +7,89 @@ import android.content.pm.PackageManager
|
|||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import eu.twonly.MyApplication
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/** One-shot precise location lookup called synchronously by Rust/JNI. */
|
||||
object NativeLocation {
|
||||
private const val TAG = "TwonlyLocation"
|
||||
private const val TARGET_ACCURACY_METERS = 50f
|
||||
private const val MAX_FIX_AGE_NANOS = 180_000_000_000L
|
||||
|
||||
@JvmStatic
|
||||
@SuppressLint("MissingPermission")
|
||||
fun current(timeoutMillis: Long): String? {
|
||||
val context = MyApplication.instance
|
||||
if (context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.w(TAG, "Memory location skipped: precise location permission is missing")
|
||||
return null
|
||||
}
|
||||
val manager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return null
|
||||
val finished = CountDownLatch(1)
|
||||
val best = AtomicReference<Location?>(null)
|
||||
val stopped = AtomicBoolean(false)
|
||||
fun accept(location: Location) {
|
||||
val age = SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos
|
||||
if (stopped.get() || !location.hasAccuracy() || !location.accuracy.isFinite() ||
|
||||
location.accuracy < 0 || age !in 0..MAX_FIX_AGE_NANOS) return
|
||||
val previous = best.get()
|
||||
if (previous == null || location.accuracy < previous.accuracy) best.set(Location(location))
|
||||
if (location.accuracy <= TARGET_ACCURACY_METERS) finished.countDown()
|
||||
}
|
||||
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()
|
||||
accept(location)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Android")
|
||||
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
|
||||
}
|
||||
val main = Handler(Looper.getMainLooper())
|
||||
main.post {
|
||||
val start = Runnable {
|
||||
if (stopped.get()) return@Runnable
|
||||
val providers = buildList {
|
||||
// The fused provider may have a fix even when GPS cannot get
|
||||
// a signal and the separate network provider is unavailable.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) add(LocationManager.FUSED_PROVIDER)
|
||||
add(LocationManager.GPS_PROVIDER)
|
||||
add(LocationManager.NETWORK_PROVIDER)
|
||||
}.filter { runCatching { manager.isProviderEnabled(it) }.getOrDefault(false) }
|
||||
for (provider in providers) {
|
||||
runCatching { manager.getLastKnownLocation(provider) }
|
||||
.onSuccess { location -> location?.let(::accept) }
|
||||
.onFailure { Log.w(TAG, "Could not read cached $provider location", it) }
|
||||
}
|
||||
if (finished.count == 0L) return@Runnable
|
||||
var requested = false
|
||||
for (provider in listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)) {
|
||||
if (!runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)) continue
|
||||
for (provider in providers) {
|
||||
runCatching {
|
||||
manager.requestLocationUpdates(provider, 0L, 0f, listener, Looper.getMainLooper())
|
||||
requested = true
|
||||
}
|
||||
Log.d(TAG, "Requested Memory location from $provider")
|
||||
}.onFailure { Log.w(TAG, "Could not request $provider location", it) }
|
||||
}
|
||||
if (!requested) {
|
||||
Log.w(TAG, "Memory location unavailable: no provider accepted the request")
|
||||
finished.countDown()
|
||||
}
|
||||
if (!requested) finished.countDown()
|
||||
}
|
||||
finished.await(timeoutMillis.coerceAtLeast(0), TimeUnit.MILLISECONDS)
|
||||
main.post { runCatching { manager.removeUpdates(listener) } }
|
||||
main.post(start)
|
||||
try {
|
||||
finished.await(timeoutMillis.coerceAtLeast(0), TimeUnit.MILLISECONDS)
|
||||
} finally {
|
||||
stopped.set(true)
|
||||
main.removeCallbacks(start)
|
||||
main.post { runCatching { manager.removeUpdates(listener) } }
|
||||
}
|
||||
val location = best.get() ?: return null
|
||||
return "[${location.latitude},${location.longitude},${location.accuracy}]"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c3d085882368d7269956048bbb69fbd8a69a711c
|
||||
Subproject commit 95853567e9ade7ebe27a2af17bb82c90f4cf8416
|
||||
|
|
@ -41,7 +41,39 @@ def get_default_branch(repo_path='.'):
|
|||
return result.stdout.strip().split('/')[-1]
|
||||
return 'main'
|
||||
|
||||
def get_package_version(package_path):
|
||||
pubspec_path = os.path.join(package_path, "pubspec.yaml")
|
||||
if os.path.exists(pubspec_path):
|
||||
with open(pubspec_path, "r") as f:
|
||||
ps = yaml.safe_load(f)
|
||||
if ps and isinstance(ps, dict):
|
||||
return ps.get("version", "any")
|
||||
return "any"
|
||||
|
||||
def preserve_custom_package(pkg_name, out_dir):
|
||||
out_path = os.path.join(out_dir, pkg_name)
|
||||
if not os.path.isfile(os.path.join(out_path, "pubspec.yaml")):
|
||||
raise FileNotFoundError(
|
||||
f"Custom package not found: {out_path}. "
|
||||
"Restore it from the dependencies repository; automatic updates are disabled."
|
||||
)
|
||||
print_yellow(f"Preserving {pkg_name}: custom_changes is enabled.")
|
||||
return (pkg_name, get_package_version(out_path))
|
||||
|
||||
def integrate_package(folder_name, data, cache_dir, out_dir, cache_only=False):
|
||||
packages_to_extract = data.get(
|
||||
"subpackages", [{"name": folder_name, "path": data.get("path", "")}]
|
||||
)
|
||||
# Keep custom packages intact, including when copying from the cache.
|
||||
# Read their local versions so managed pubspec entries are retained.
|
||||
if data.get("custom_changes") or all(
|
||||
pkg.get("custom_changes") for pkg in packages_to_extract
|
||||
):
|
||||
return [
|
||||
preserve_custom_package(pkg["name"], out_dir)
|
||||
for pkg in packages_to_extract
|
||||
]
|
||||
|
||||
keep_list = ["lib", "LICENSE", "pubspec.yaml", "android", "ios", "darwin"]
|
||||
if "keep" in data:
|
||||
keep_list += [item.rstrip('/') for item in data['keep']]
|
||||
|
|
@ -82,13 +114,11 @@ def integrate_package(folder_name, data, cache_dir, out_dir, cache_only=False):
|
|||
|
||||
results = [] # List of (pkg_name, version)
|
||||
|
||||
if "subpackages" in data:
|
||||
packages_to_extract = data["subpackages"]
|
||||
else:
|
||||
packages_to_extract = [{"name": folder_name, "path": data.get("path", "")}]
|
||||
|
||||
for pkg in packages_to_extract:
|
||||
pkg_name = pkg["name"]
|
||||
if pkg.get("custom_changes"):
|
||||
results.append(preserve_custom_package(pkg_name, out_dir))
|
||||
continue
|
||||
subpath = pkg.get("path", "")
|
||||
|
||||
package_src_path = os.path.join(cache_path, subpath) if subpath else cache_path
|
||||
|
|
@ -115,12 +145,7 @@ def integrate_package(folder_name, data, cache_dir, out_dir, cache_only=False):
|
|||
|
||||
version = "any"
|
||||
try:
|
||||
pubspec_path = os.path.join(package_src_path, "pubspec.yaml")
|
||||
if os.path.exists(pubspec_path):
|
||||
with open(pubspec_path, "r") as f:
|
||||
ps = yaml.safe_load(f)
|
||||
if ps and isinstance(ps, dict):
|
||||
version = ps.get("version", "any")
|
||||
version = get_package_version(package_src_path)
|
||||
except Exception as e:
|
||||
print_yellow(f"Warning: Could not read version from {pkg_name}/pubspec.yaml")
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ dependencies:
|
|||
blurhash_dart:
|
||||
git: https://github.com/justacid/blurhash-dart.git
|
||||
tag: v1.2.1
|
||||
camera_avfoundation:
|
||||
git: https://github.com/flutter/packages.git
|
||||
tag: camera_avfoundation-v0.10.3
|
||||
path: packages/camera/camera_avfoundation
|
||||
custom_changes: true
|
||||
custom_changes_description: Downscale and throttle rear-camera QR analysis frames.
|
||||
exif:
|
||||
git: https://github.com/bigflood/dartexif.git
|
||||
dependencies:
|
||||
|
|
@ -36,6 +42,8 @@ dependencies:
|
|||
path: packages/video_player/video_player_avfoundation
|
||||
- name: camera_android_camerax
|
||||
path: packages/camera/camera_android_camerax
|
||||
custom_changes: true
|
||||
custom_changes_description: Use lower-resolution rear-camera QR analysis frames.
|
||||
commit: b4f7d807a822a8ce327aae21353ac2217e928f6d
|
||||
flutter_sharing_intent:
|
||||
git: https://github.com/bhagat-techind/flutter_sharing_intent.git
|
||||
|
|
|
|||
92
lib/core/backup/backup_archive.dart
Normal file
92
lib/core/backup/backup_archive.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||
|
||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||
|
||||
import '../frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
|
||||
class BackupFileSize {
|
||||
final String name;
|
||||
|
||||
/// None means the source file is absent and will be skipped by the backup.
|
||||
final PlatformInt64? sizeBytes;
|
||||
|
||||
const BackupFileSize({
|
||||
required this.name,
|
||||
this.sizeBytes,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => name.hashCode ^ sizeBytes.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BackupFileSize &&
|
||||
runtimeType == other.runtimeType &&
|
||||
name == other.name &&
|
||||
sizeBytes == other.sizeBytes;
|
||||
}
|
||||
|
||||
class BackupStorageInfo {
|
||||
final PlatformInt64 databaseSizeBytes;
|
||||
final PlatformInt64 freeSizeBytes;
|
||||
final List<BackupTableSize> tables;
|
||||
final List<BackupFileSize> files;
|
||||
|
||||
const BackupStorageInfo({
|
||||
required this.databaseSizeBytes,
|
||||
required this.freeSizeBytes,
|
||||
required this.tables,
|
||||
required this.files,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
databaseSizeBytes.hashCode ^
|
||||
freeSizeBytes.hashCode ^
|
||||
tables.hashCode ^
|
||||
files.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BackupStorageInfo &&
|
||||
runtimeType == other.runtimeType &&
|
||||
databaseSizeBytes == other.databaseSizeBytes &&
|
||||
freeSizeBytes == other.freeSizeBytes &&
|
||||
tables == other.tables &&
|
||||
files == other.files;
|
||||
}
|
||||
|
||||
class BackupTableSize {
|
||||
final String name;
|
||||
final PlatformInt64 rows;
|
||||
final PlatformInt64 tableSizeBytes;
|
||||
final PlatformInt64 indexSizeBytes;
|
||||
|
||||
const BackupTableSize({
|
||||
required this.name,
|
||||
required this.rows,
|
||||
required this.tableSizeBytes,
|
||||
required this.indexSizeBytes,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
name.hashCode ^
|
||||
rows.hashCode ^
|
||||
tableSizeBytes.hashCode ^
|
||||
indexSizeBytes.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BackupTableSize &&
|
||||
runtimeType == other.runtimeType &&
|
||||
name == other.name &&
|
||||
rows == other.rows &&
|
||||
tableSizeBytes == other.tableSizeBytes &&
|
||||
indexSizeBytes == other.indexSizeBytes;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||
|
||||
import '../../backup/backup_archive.dart';
|
||||
import '../../frb_generated.dart';
|
||||
import '../../keys/backup_password_keys.dart';
|
||||
import '../../lib.dart';
|
||||
|
|
@ -23,6 +24,9 @@ class RustBackupArchive {
|
|||
filePath: filePath,
|
||||
);
|
||||
|
||||
static Future<BackupStorageInfo> storageInfo() => RustLib.instance.api
|
||||
.crateBridgeWrapperBackupRustBackupArchiveStorageInfo();
|
||||
|
||||
@override
|
||||
int get hashCode => 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||
|
||||
import 'api/server/prekeys.dart';
|
||||
import 'backup/backup_archive.dart';
|
||||
import 'bridge.dart';
|
||||
import 'bridge/api.dart';
|
||||
import 'bridge/callbacks.dart';
|
||||
|
|
@ -90,7 +91,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => 926647182;
|
||||
int get rustContentHash => -1385877172;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
|
|
@ -645,6 +646,9 @@ abstract class RustLibApi extends BaseApi {
|
|||
required String filePath,
|
||||
});
|
||||
|
||||
Future<BackupStorageInfo>
|
||||
crateBridgeWrapperBackupRustBackupArchiveStorageInfo();
|
||||
|
||||
Future<String?> crateBridgeWrapperBackupRustBackupIdentityGetBackupId();
|
||||
|
||||
Future<BackupPasswordKeys>
|
||||
|
|
@ -5833,7 +5837,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
);
|
||||
|
||||
@override
|
||||
Future<String?> crateBridgeWrapperBackupRustBackupIdentityGetBackupId() {
|
||||
Future<BackupStorageInfo>
|
||||
crateBridgeWrapperBackupRustBackupArchiveStorageInfo() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
|
|
@ -5845,6 +5850,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_backup_storage_info,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperBackupRustBackupArchiveStorageInfoConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta
|
||||
get kCrateBridgeWrapperBackupRustBackupArchiveStorageInfoConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "rust_backup_archive_storage_info",
|
||||
argNames: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<String?> crateBridgeWrapperBackupRustBackupIdentityGetBackupId() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 149,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_opt_String,
|
||||
decodeErrorData: null,
|
||||
|
|
@ -5879,7 +5916,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 149,
|
||||
funcId: 150,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5912,7 +5949,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 150,
|
||||
funcId: 151,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5950,7 +5987,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 151,
|
||||
funcId: 152,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -5987,7 +6024,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 152,
|
||||
funcId: 153,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6024,7 +6061,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 153,
|
||||
funcId: 154,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6062,7 +6099,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 154,
|
||||
funcId: 155,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6100,7 +6137,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 155,
|
||||
funcId: 156,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6133,7 +6170,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 156,
|
||||
funcId: 157,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6165,7 +6202,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 157,
|
||||
funcId: 158,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6200,7 +6237,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 158,
|
||||
funcId: 159,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6237,7 +6274,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 159,
|
||||
funcId: 160,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6270,7 +6307,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 160,
|
||||
funcId: 161,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6302,7 +6339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 161,
|
||||
funcId: 162,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6337,7 +6374,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 162,
|
||||
funcId: 163,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6376,7 +6413,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 163,
|
||||
funcId: 164,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6413,7 +6450,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 164,
|
||||
funcId: 165,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6443,7 +6480,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 165,
|
||||
funcId: 166,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6475,7 +6512,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 166,
|
||||
funcId: 167,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6510,7 +6547,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 167,
|
||||
funcId: 168,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6545,7 +6582,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 168,
|
||||
funcId: 169,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6576,7 +6613,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 169,
|
||||
funcId: 170,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6614,7 +6651,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 170,
|
||||
funcId: 171,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6649,7 +6686,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 171,
|
||||
funcId: 172,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6688,7 +6725,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 172,
|
||||
funcId: 173,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6723,7 +6760,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 173,
|
||||
funcId: 174,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6768,7 +6805,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 174,
|
||||
funcId: 175,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6811,7 +6848,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 175,
|
||||
funcId: 176,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6843,7 +6880,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 176,
|
||||
funcId: 177,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6878,7 +6915,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 177,
|
||||
funcId: 178,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6913,7 +6950,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 178,
|
||||
funcId: 179,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6948,7 +6985,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 179,
|
||||
funcId: 180,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -6981,7 +7018,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 180,
|
||||
funcId: 181,
|
||||
)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
|
|
@ -7021,7 +7058,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 181,
|
||||
funcId: 182,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -7066,7 +7103,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 182,
|
||||
funcId: 183,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -7096,7 +7133,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 183,
|
||||
funcId: 184,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -7129,7 +7166,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 184,
|
||||
funcId: 185,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -7164,7 +7201,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 185,
|
||||
funcId: 186,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -7203,7 +7240,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 186,
|
||||
funcId: 187,
|
||||
)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
|
|
@ -7403,6 +7440,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return ApiEventKind.values[raw as int];
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupFileSize dco_decode_backup_file_size(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 2)
|
||||
throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
|
||||
return BackupFileSize(
|
||||
name: dco_decode_String(arr[0]),
|
||||
sizeBytes: dco_decode_opt_box_autoadd_i_64(arr[1]),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
|
|
@ -7415,6 +7464,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupStorageInfo dco_decode_backup_storage_info(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
return BackupStorageInfo(
|
||||
databaseSizeBytes: dco_decode_i_64(arr[0]),
|
||||
freeSizeBytes: dco_decode_i_64(arr[1]),
|
||||
tables: dco_decode_list_backup_table_size(arr[2]),
|
||||
files: dco_decode_list_backup_file_size(arr[3]),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupTableSize dco_decode_backup_table_size(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
return BackupTableSize(
|
||||
name: dco_decode_String(arr[0]),
|
||||
rows: dco_decode_i_64(arr[1]),
|
||||
tableSizeBytes: dco_decode_i_64(arr[2]),
|
||||
indexSizeBytes: dco_decode_i_64(arr[3]),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
bool dco_decode_bool(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
|
|
@ -7766,6 +7843,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return (raw as List<dynamic>).map(dco_decode_String).toList();
|
||||
}
|
||||
|
||||
@protected
|
||||
List<BackupFileSize> dco_decode_list_backup_file_size(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return (raw as List<dynamic>).map(dco_decode_backup_file_size).toList();
|
||||
}
|
||||
|
||||
@protected
|
||||
List<BackupTableSize> dco_decode_list_backup_table_size(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return (raw as List<dynamic>).map(dco_decode_backup_table_size).toList();
|
||||
}
|
||||
|
||||
@protected
|
||||
List<FrbAdditionalAccount> dco_decode_list_frb_additional_account(
|
||||
dynamic raw,
|
||||
|
|
@ -8591,6 +8680,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return ApiEventKind.values[inner];
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupFileSize sse_decode_backup_file_size(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var var_name = sse_decode_String(deserializer);
|
||||
var var_sizeBytes = sse_decode_opt_box_autoadd_i_64(deserializer);
|
||||
return BackupFileSize(name: var_name, sizeBytes: var_sizeBytes);
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupPasswordKeys sse_decode_backup_password_keys(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -8604,6 +8701,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupStorageInfo sse_decode_backup_storage_info(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var var_databaseSizeBytes = sse_decode_i_64(deserializer);
|
||||
var var_freeSizeBytes = sse_decode_i_64(deserializer);
|
||||
var var_tables = sse_decode_list_backup_table_size(deserializer);
|
||||
var var_files = sse_decode_list_backup_file_size(deserializer);
|
||||
return BackupStorageInfo(
|
||||
databaseSizeBytes: var_databaseSizeBytes,
|
||||
freeSizeBytes: var_freeSizeBytes,
|
||||
tables: var_tables,
|
||||
files: var_files,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
BackupTableSize sse_decode_backup_table_size(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var var_name = sse_decode_String(deserializer);
|
||||
var var_rows = sse_decode_i_64(deserializer);
|
||||
var var_tableSizeBytes = sse_decode_i_64(deserializer);
|
||||
var var_indexSizeBytes = sse_decode_i_64(deserializer);
|
||||
return BackupTableSize(
|
||||
name: var_name,
|
||||
rows: var_rows,
|
||||
tableSizeBytes: var_tableSizeBytes,
|
||||
indexSizeBytes: var_indexSizeBytes,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
|
@ -8999,6 +9128,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
return ans_;
|
||||
}
|
||||
|
||||
@protected
|
||||
List<BackupFileSize> sse_decode_list_backup_file_size(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
var len_ = sse_decode_i_32(deserializer);
|
||||
var ans_ = <BackupFileSize>[];
|
||||
for (var idx_ = 0; idx_ < len_; ++idx_) {
|
||||
ans_.add(sse_decode_backup_file_size(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
|
||||
@protected
|
||||
List<BackupTableSize> sse_decode_list_backup_table_size(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
var len_ = sse_decode_i_32(deserializer);
|
||||
var ans_ = <BackupTableSize>[];
|
||||
for (var idx_ = 0; idx_ < len_; ++idx_) {
|
||||
ans_.add(sse_decode_backup_table_size(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
|
||||
@protected
|
||||
List<FrbAdditionalAccount> sse_decode_list_frb_additional_account(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -10142,6 +10299,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
sse_encode_i_32(self.index, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_file_size(
|
||||
BackupFileSize self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_String(self.name, serializer);
|
||||
sse_encode_opt_box_autoadd_i_64(self.sizeBytes, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_password_keys(
|
||||
BackupPasswordKeys self,
|
||||
|
|
@ -10152,6 +10319,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
sse_encode_u_8_array_32(self.encryptionKey, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_storage_info(
|
||||
BackupStorageInfo self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_64(self.databaseSizeBytes, serializer);
|
||||
sse_encode_i_64(self.freeSizeBytes, serializer);
|
||||
sse_encode_list_backup_table_size(self.tables, serializer);
|
||||
sse_encode_list_backup_file_size(self.files, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_table_size(
|
||||
BackupTableSize self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_String(self.name, serializer);
|
||||
sse_encode_i_64(self.rows, serializer);
|
||||
sse_encode_i_64(self.tableSizeBytes, serializer);
|
||||
sse_encode_i_64(self.indexSizeBytes, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
|
@ -10504,6 +10695,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_backup_file_size(
|
||||
List<BackupFileSize> self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_32(self.length, serializer);
|
||||
for (final item in self) {
|
||||
sse_encode_backup_file_size(item, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_backup_table_size(
|
||||
List<BackupTableSize> self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_32(self.length, serializer);
|
||||
for (final item in self) {
|
||||
sse_encode_backup_table_size(item, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_frb_additional_account(
|
||||
List<FrbAdditionalAccount> self,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||
|
||||
import 'api/server/prekeys.dart';
|
||||
import 'backup/backup_archive.dart';
|
||||
import 'bridge.dart';
|
||||
import 'bridge/api.dart';
|
||||
import 'bridge/callbacks.dart';
|
||||
|
|
@ -86,9 +87,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
ApiEventKind dco_decode_api_event_kind(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupFileSize dco_decode_backup_file_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupStorageInfo dco_decode_backup_storage_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupTableSize dco_decode_backup_table_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
|
|
@ -207,6 +217,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
List<String> dco_decode_list_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<BackupFileSize> dco_decode_list_backup_file_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<BackupTableSize> dco_decode_list_backup_table_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<FrbAdditionalAccount> dco_decode_list_frb_additional_account(
|
||||
dynamic raw,
|
||||
|
|
@ -493,11 +509,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
ApiEventKind sse_decode_api_event_kind(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BackupFileSize sse_decode_backup_file_size(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BackupPasswordKeys sse_decode_backup_password_keys(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
BackupStorageInfo sse_decode_backup_storage_info(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
BackupTableSize sse_decode_backup_table_size(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -644,6 +671,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<BackupFileSize> sse_decode_list_backup_file_size(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<BackupTableSize> sse_decode_list_backup_table_size(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<FrbAdditionalAccount> sse_decode_list_frb_additional_account(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -995,12 +1032,30 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_api_event_kind(ApiEventKind self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_file_size(
|
||||
BackupFileSize self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_password_keys(
|
||||
BackupPasswordKeys self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_storage_info(
|
||||
BackupStorageInfo self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_table_size(
|
||||
BackupTableSize self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -1190,6 +1245,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_backup_file_size(
|
||||
List<BackupFileSize> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_backup_table_size(
|
||||
List<BackupTableSize> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_frb_additional_account(
|
||||
List<FrbAdditionalAccount> self,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
// ignore_for_file: argument_type_not_assignable
|
||||
|
||||
import 'api/server/prekeys.dart';
|
||||
import 'backup/backup_archive.dart';
|
||||
import 'bridge.dart';
|
||||
import 'bridge/api.dart';
|
||||
import 'bridge/callbacks.dart';
|
||||
|
|
@ -88,9 +89,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
ApiEventKind dco_decode_api_event_kind(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupFileSize dco_decode_backup_file_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupStorageInfo dco_decode_backup_storage_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
BackupTableSize dco_decode_backup_table_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
bool dco_decode_bool(dynamic raw);
|
||||
|
||||
|
|
@ -209,6 +219,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
List<String> dco_decode_list_String(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<BackupFileSize> dco_decode_list_backup_file_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<BackupTableSize> dco_decode_list_backup_table_size(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<FrbAdditionalAccount> dco_decode_list_frb_additional_account(
|
||||
dynamic raw,
|
||||
|
|
@ -495,11 +511,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
ApiEventKind sse_decode_api_event_kind(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BackupFileSize sse_decode_backup_file_size(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BackupPasswordKeys sse_decode_backup_password_keys(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
BackupStorageInfo sse_decode_backup_storage_info(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
BackupTableSize sse_decode_backup_table_size(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -646,6 +673,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
List<String> sse_decode_list_String(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<BackupFileSize> sse_decode_list_backup_file_size(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<BackupTableSize> sse_decode_list_backup_table_size(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
List<FrbAdditionalAccount> sse_decode_list_frb_additional_account(
|
||||
SseDeserializer deserializer,
|
||||
|
|
@ -997,12 +1034,30 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_api_event_kind(ApiEventKind self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_file_size(
|
||||
BackupFileSize self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_password_keys(
|
||||
BackupPasswordKeys self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_storage_info(
|
||||
BackupStorageInfo self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_backup_table_size(
|
||||
BackupTableSize self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
|
||||
|
|
@ -1192,6 +1247,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
void sse_encode_list_String(List<String> self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_backup_file_size(
|
||||
List<BackupFileSize> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_backup_table_size(
|
||||
List<BackupTableSize> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_frb_additional_account(
|
||||
List<FrbAdditionalAccount> self,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
// of this object.
|
||||
// ignore: matching_super_parameters
|
||||
GroupsDao(super.db);
|
||||
|
||||
// Former members stay stored so their messages and group history remain
|
||||
// readable. A null state is a legacy member who is still in the group.
|
||||
Expression<bool> _isActiveMember(GroupMembers member) =>
|
||||
member.memberState.equals(MemberState.leftGroup.name).not() |
|
||||
member.memberState.isNull();
|
||||
|
||||
Future<void> deleteGroup(String groupId) async {
|
||||
await (delete(groups)..where((t) => t.groupId.equals(groupId))).go();
|
||||
}
|
||||
|
|
@ -37,10 +44,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
|
||||
Future<List<GroupMember>> getGroupNonLeftMembers(String groupId) async {
|
||||
return (select(groupMembers)..where(
|
||||
(t) =>
|
||||
t.groupId.equals(groupId) &
|
||||
(t.memberState.equals(MemberState.leftGroup.name).not() |
|
||||
t.memberState.isNull()),
|
||||
(t) => t.groupId.equals(groupId) & _isActiveMember(t),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
|
@ -131,7 +135,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
),
|
||||
])
|
||||
..orderBy([OrderingTerm.desc(groupMembers.lastMessage)])
|
||||
..where(groupMembers.groupId.equals(groupId)));
|
||||
..where(
|
||||
groupMembers.groupId.equals(groupId) &
|
||||
_isActiveMember(groupMembers),
|
||||
));
|
||||
return query.map((row) => row.readTable(contacts)).get();
|
||||
}
|
||||
|
||||
|
|
@ -145,25 +152,31 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
),
|
||||
])
|
||||
..orderBy([OrderingTerm.desc(groupMembers.lastMessage)])
|
||||
..where(groupMembers.groupId.equals(groupId)));
|
||||
..where(
|
||||
groupMembers.groupId.equals(groupId) &
|
||||
_isActiveMember(groupMembers),
|
||||
));
|
||||
return query.map((row) => row.readTable(contacts)).watch();
|
||||
}
|
||||
|
||||
Stream<List<(Contact, GroupMember)>> watchGroupMembers(String groupId) {
|
||||
final query =
|
||||
(select(groupMembers)..where((t) => t.groupId.equals(groupId))).join([
|
||||
leftOuterJoin(
|
||||
contacts,
|
||||
contacts.userId.equalsExp(groupMembers.contactId),
|
||||
),
|
||||
]);
|
||||
(select(groupMembers)..where(
|
||||
(t) => t.groupId.equals(groupId) & _isActiveMember(t),
|
||||
))
|
||||
.join([
|
||||
leftOuterJoin(
|
||||
contacts,
|
||||
contacts.userId.equalsExp(groupMembers.contactId),
|
||||
),
|
||||
]);
|
||||
return query
|
||||
.map((row) => (row.readTable(contacts), row.readTable(groupMembers)))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<(Contact, GroupMember)>> watchAllGroupMembers() {
|
||||
final query = select(groupMembers).join([
|
||||
final query = (select(groupMembers)..where(_isActiveMember)).join([
|
||||
innerJoin(contacts, contacts.userId.equalsExp(groupMembers.contactId)),
|
||||
]);
|
||||
return query
|
||||
|
|
@ -207,8 +220,12 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
|
||||
Stream<List<GroupMember>> watchTypingGroupMembers() {
|
||||
return (select(
|
||||
groupMembers,
|
||||
)..where((member) => member.lastTypeIndicator.isNotNull())).watch();
|
||||
groupMembers,
|
||||
)..where(
|
||||
(member) =>
|
||||
member.lastTypeIndicator.isNotNull() & _isActiveMember(member),
|
||||
))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<Group?> watchGroup(String groupId) {
|
||||
|
|
@ -333,7 +350,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
),
|
||||
])..where(
|
||||
groups.isDirectChat.equals(false) &
|
||||
groupMembers.contactId.equals(contactId),
|
||||
groupMembers.contactId.equals(contactId) &
|
||||
_isActiveMember(groupMembers),
|
||||
);
|
||||
|
||||
return query.map((row) => row.readTable(groups)).watch();
|
||||
|
|
|
|||
|
|
@ -78,6 +78,24 @@ String _avatarPngPathFor(Contact contact) {
|
|||
}
|
||||
|
||||
class _AvatarIconState extends State<AvatarIcon> {
|
||||
double get _avatarSize => 2 * (widget.fontSize ?? 20);
|
||||
|
||||
bool _isPhotoAvatar(String? path) => path?.endsWith('.webp') ?? false;
|
||||
|
||||
Widget _avatarImage(String path) {
|
||||
final image = Image.file(
|
||||
File(path),
|
||||
width: _avatarSize,
|
||||
height: _avatarSize,
|
||||
errorBuilder: errorBuilder,
|
||||
);
|
||||
if (!_isPhotoAvatar(path)) return image;
|
||||
return Transform.scale(
|
||||
scale: (_avatarSize + 10) / _avatarSize,
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
|
||||
List<Contact> _avatarContacts = [];
|
||||
Set<int> _contactsWithPngAvatar = {};
|
||||
final Map<int, String> _resolvedAvatarPaths = {};
|
||||
|
|
@ -175,11 +193,8 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
|
||||
Widget getAvatarForContact(Contact contact) {
|
||||
if (_contactsWithPngAvatar.contains(contact.userId)) {
|
||||
return Image.file(
|
||||
File(
|
||||
_resolvedAvatarPaths[contact.userId] ?? _avatarPngPathFor(contact),
|
||||
),
|
||||
errorBuilder: errorBuilder,
|
||||
return _avatarImage(
|
||||
_resolvedAvatarPaths[contact.userId] ?? _avatarPngPathFor(contact),
|
||||
);
|
||||
}
|
||||
// Deliberately no SVG fallback: the render kicked off by
|
||||
|
|
@ -227,17 +242,8 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
}
|
||||
|
||||
Future<void> _updateMyAvatar() async {
|
||||
final avatarSvg = userService.currentUser.avatarSvg;
|
||||
if (avatarSvg == null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_myAvatarPath = null;
|
||||
_avatarContacts = [];
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Rust resolves photo avatars as well as public SVG avatars, including
|
||||
// profiles that have only a photo avatar.
|
||||
final path = await RustApi.currentUserAvatarPath();
|
||||
|
||||
if (mounted) {
|
||||
|
|
@ -249,16 +255,13 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final proSize = (widget.fontSize == null) ? 40 : (widget.fontSize! * 2);
|
||||
final proSize = _avatarSize;
|
||||
|
||||
Widget avatars = Container();
|
||||
|
||||
if (widget.myAvatar) {
|
||||
if (_myAvatarPath != null) {
|
||||
avatars = Image.file(
|
||||
File(_myAvatarPath!),
|
||||
errorBuilder: errorBuilder,
|
||||
);
|
||||
avatars = _avatarImage(_myAvatarPath!);
|
||||
} else {
|
||||
avatars = const SvgPicture(
|
||||
AssetBytesLoader('assets/images/default_avatar.svg.vec'),
|
||||
|
|
@ -320,16 +323,16 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 2 * (widget.fontSize ?? 20),
|
||||
minWidth: 2 * (widget.fontSize ?? 20),
|
||||
maxWidth: 2 * (widget.fontSize ?? 20),
|
||||
maxHeight: 2 * (widget.fontSize ?? 20),
|
||||
minHeight: proSize,
|
||||
minWidth: proSize,
|
||||
maxWidth: proSize,
|
||||
maxHeight: proSize,
|
||||
),
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: proSize as double,
|
||||
height: proSize,
|
||||
width: proSize,
|
||||
color: widget.color,
|
||||
child: Center(child: avatars),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/loader/ripple.loader.dart';
|
||||
|
|
@ -13,6 +14,8 @@ class ConnectionStatusComp extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!userService.currentUser.isDeveloper) return child;
|
||||
|
||||
final isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||
return Stack(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -145,7 +145,8 @@ class CameraPreviewView extends StatefulWidget {
|
|||
State<CameraPreviewView> createState() => _CameraPreviewViewState();
|
||||
}
|
||||
|
||||
class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||
class _CameraPreviewViewState extends State<CameraPreviewView>
|
||||
with WidgetsBindingObserver {
|
||||
bool _galleryLoadedImageIsShown = false;
|
||||
bool _showSelfieFlash = false;
|
||||
double _basePanY = 0;
|
||||
|
|
@ -164,10 +165,22 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
MainCameraController get mc => widget.mainCameraController;
|
||||
|
||||
StreamSubscription<HardwareButton>? androidVolumeDownSub;
|
||||
StreamSubscription<void>? _userUpdates;
|
||||
late bool _storeLocationInMemories;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_storeLocationInMemories = userService.currentUser.storeLocationInMemories;
|
||||
_userUpdates = userService.onUserUpdated.listen((_) {
|
||||
final enabled = userService.currentUser.storeLocationInMemories;
|
||||
final wasEnabled = _storeLocationInMemories;
|
||||
_storeLocationInMemories = enabled;
|
||||
// The camera can remain mounted while settings are open, so enabling
|
||||
// location does not necessarily cause a visibility change.
|
||||
if (enabled && !wasEnabled) _prewarmMemoryLocation();
|
||||
});
|
||||
unawaited(VideoRecordingBudget.ensureLoaded());
|
||||
initVolumeControl();
|
||||
initAsync();
|
||||
|
|
@ -200,6 +213,11 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) _prewarmMemoryLocation();
|
||||
}
|
||||
|
||||
void _checkAndInitCamera() {
|
||||
if (widget.isVisible &&
|
||||
mc.cameraController == null &&
|
||||
|
|
@ -215,6 +233,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
unawaited(_userUpdates?.cancel());
|
||||
_videoRecordingTimer?.cancel();
|
||||
_deInitVolumeControl();
|
||||
super.dispose();
|
||||
|
|
|
|||
|
|
@ -32,11 +32,12 @@ import 'package:twonly/src/visual/views/camera/camera_preview_components/painter
|
|||
import 'package:twonly/src/visual/views/camera/camera_preview_components/painters/face_filters/dog_filter_painter.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart';
|
||||
|
||||
// A captured widget cannot contain more camera detail than the live preview.
|
||||
// Full HD keeps the screenshot capture fast while avoiding the visible 720p
|
||||
// upscale on most modern phone displays. Ultra-high/max would also make the
|
||||
// continuously running barcode and face-detection image stream much heavier.
|
||||
const ResolutionPreset _cameraResolutionPreset = ResolutionPreset.veryHigh;
|
||||
// Ordinary photos retain the rear camera's 1080p preview. Selfie previews and
|
||||
// face-filter streams use 720p.
|
||||
ResolutionPreset cameraPreviewResolution(CameraLensDirection direction) =>
|
||||
direction == CameraLensDirection.front
|
||||
? ResolutionPreset.high
|
||||
: ResolutionPreset.veryHigh;
|
||||
|
||||
class PreviewLink {
|
||||
const PreviewLink({
|
||||
|
|
@ -120,6 +121,7 @@ class MainCameraController {
|
|||
Future<void>? _initializeFuture;
|
||||
Future<void>? _pendingDisposal;
|
||||
int _cameraSessionId = 0;
|
||||
DateTime? _lastBarcodeFrame;
|
||||
|
||||
Future<void> closeCamera() async {
|
||||
_cameraSessionId++;
|
||||
|
|
@ -200,13 +202,24 @@ class MainCameraController {
|
|||
}
|
||||
|
||||
selectedCameraDetails.isZoomAble = false;
|
||||
|
||||
_lastBarcodeFrame = null;
|
||||
final currentController = cameraController;
|
||||
if (currentController == null || !currentController.value.isInitialized) {
|
||||
// Preserve an active recording's preset when switching lenses. Recreating
|
||||
// its controller just to lower the selfie preview would end the video.
|
||||
final resolution = isVideoRecording && currentController != null
|
||||
? currentController.resolutionPreset
|
||||
: cameraPreviewResolution(
|
||||
AppEnvironment.cameras[cameraId].lensDirection,
|
||||
);
|
||||
if (currentController == null ||
|
||||
!currentController.value.isInitialized ||
|
||||
currentController.resolutionPreset != resolution) {
|
||||
final controllerToDispose = cameraController;
|
||||
cameraController = null;
|
||||
setState?.call();
|
||||
if (controllerToDispose != null) {
|
||||
unawaited(controllerToDispose.dispose());
|
||||
await controllerToDispose.dispose();
|
||||
if (sessionId != _cameraSessionId) return;
|
||||
}
|
||||
|
||||
final hasMic = await micPermissionFuture;
|
||||
|
|
@ -214,7 +227,7 @@ class MainCameraController {
|
|||
|
||||
var controller = CameraController(
|
||||
AppEnvironment.cameras[cameraId],
|
||||
_cameraResolutionPreset,
|
||||
resolution,
|
||||
enableAudio: hasMic,
|
||||
imageFormatGroup: Platform.isAndroid
|
||||
? ImageFormatGroup.nv21
|
||||
|
|
@ -234,7 +247,7 @@ class MainCameraController {
|
|||
await controller.dispose();
|
||||
controller = CameraController(
|
||||
AppEnvironment.cameras[cameraId],
|
||||
_cameraResolutionPreset,
|
||||
resolution,
|
||||
enableAudio: hasMic,
|
||||
);
|
||||
_initializeFuture = controller.initialize();
|
||||
|
|
@ -457,16 +470,23 @@ class MainCameraController {
|
|||
if (isVideoRecording || isSharePreviewIsShown) {
|
||||
return;
|
||||
}
|
||||
final inputImage = _inputImageFromCameraImage(image);
|
||||
if (inputImage == null) return;
|
||||
// check if front camera is selected
|
||||
if (cameraController?.description.lensDirection ==
|
||||
CameraLensDirection.front) {
|
||||
if (_currentFilterType != FaceFilterType.none) {
|
||||
_processFaces(inputImage);
|
||||
}
|
||||
if (_currentFilterType == FaceFilterType.none || _isBusyFaces) return;
|
||||
final inputImage = _inputImageFromCameraImage(image);
|
||||
if (inputImage != null) unawaited(_processFaces(inputImage));
|
||||
} else {
|
||||
_processBarcode(inputImage);
|
||||
if (_isBusy) return;
|
||||
final now = clock.now();
|
||||
if (_lastBarcodeFrame != null &&
|
||||
now.difference(_lastBarcodeFrame!) <
|
||||
const Duration(milliseconds: 150)) {
|
||||
return;
|
||||
}
|
||||
_lastBarcodeFrame = now;
|
||||
final inputImage = _inputImageFromCameraImage(image);
|
||||
if (inputImage != null) unawaited(_processBarcode(inputImage));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -166,6 +166,9 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
otherUsers.add(group);
|
||||
}
|
||||
}
|
||||
otherUsers.sort(
|
||||
(a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_bestFriends = bestFriends;
|
||||
|
|
@ -271,103 +274,119 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
child: CustomScrollView(
|
||||
restorationId: 'share_image_contact_selection',
|
||||
slivers: [
|
||||
if (_allGroups.isEmpty)
|
||||
const EmptyChatListComp()
|
||||
const SliverToBoxAdapter(child: EmptyChatListComp())
|
||||
else ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: MyInput(
|
||||
controller: searchUserName,
|
||||
dense: true,
|
||||
fontWeight: FontWeight.normal,
|
||||
hintText: context.lang.shareImageSearchAllContacts,
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
onChanged: _filterUsers,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (!widget.sendToWidget) ...[
|
||||
ContactGroupShortcutRow(
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
StoryTargetSelector(
|
||||
target: widget.storyTarget,
|
||||
enabled: _storyPossible,
|
||||
onChanged: () => setState(() {}),
|
||||
onAudienceChanged: (chats) {
|
||||
if (!mounted) return;
|
||||
setState(() => _storyAudienceChats = chats);
|
||||
},
|
||||
),
|
||||
// The next section's heading brings its own space.
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
BestFriendsSelector(
|
||||
groups: _pinnedContacts,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
title: context.lang.shareImagePinnedContacts,
|
||||
showSelectAll:
|
||||
!widget.sendToWidget &&
|
||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||
),
|
||||
if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10),
|
||||
BestFriendsSelector(
|
||||
groups: _bestFriends,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
title: context.lang.shareImageBestFriends,
|
||||
showSelectAll:
|
||||
!widget.sendToWidget &&
|
||||
!widget.mediaFileService.mediaFile.requiresAuthentication,
|
||||
),
|
||||
if (_bestFriends.isNotEmpty) const SizedBox(height: 10),
|
||||
if (_otherUsers.isNotEmpty)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
HeadLineComp(context.lang.shareImageAllUsers),
|
||||
if (_allGroups.any((x) => x.archived))
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: MyInput(
|
||||
controller: searchUserName,
|
||||
dense: true,
|
||||
fontWeight: FontWeight.normal,
|
||||
hintText: context.lang.shareImageSearchAllContacts,
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
onChanged: _filterUsers,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (!widget.sendToWidget) ...[
|
||||
ContactGroupShortcutRow(
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
StoryTargetSelector(
|
||||
target: widget.storyTarget,
|
||||
enabled: _storyPossible,
|
||||
onChanged: () => setState(() {}),
|
||||
onAudienceChanged: (chats) {
|
||||
if (!mounted) return;
|
||||
setState(() => _storyAudienceChats = chats);
|
||||
},
|
||||
),
|
||||
// The next section's heading brings its own space.
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
BestFriendsSelector(
|
||||
groups: _pinnedContacts,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
title: context.lang.shareImagePinnedContacts,
|
||||
showSelectAll:
|
||||
!widget.sendToWidget &&
|
||||
!widget
|
||||
.mediaFileService
|
||||
.mediaFile
|
||||
.requiresAuthentication,
|
||||
),
|
||||
if (_pinnedContacts.isNotEmpty)
|
||||
const SizedBox(height: 10),
|
||||
BestFriendsSelector(
|
||||
groups: _bestFriends,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
title: context.lang.shareImageBestFriends,
|
||||
showSelectAll:
|
||||
!widget.sendToWidget &&
|
||||
!widget
|
||||
.mediaFileService
|
||||
.mediaFile
|
||||
.requiresAuthentication,
|
||||
),
|
||||
if (_bestFriends.isNotEmpty) const SizedBox(height: 10),
|
||||
if (_otherUsers.isNotEmpty)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.shareImageShowArchived,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
Transform.scale(
|
||||
scale: 0.75,
|
||||
child: Checkbox.adaptive(
|
||||
value: !hideArchivedUsers,
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline,
|
||||
);
|
||||
},
|
||||
),
|
||||
onChanged: (a) async {
|
||||
hideArchivedUsers = !hideArchivedUsers;
|
||||
await _filterUsers(lastQuery);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
HeadLineComp(context.lang.shareImageAllUsers),
|
||||
if (_allGroups.any((x) => x.archived))
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
context.lang.shareImageShowArchived,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
Transform.scale(
|
||||
scale: 0.75,
|
||||
child: Checkbox.adaptive(
|
||||
value: !hideArchivedUsers,
|
||||
side: WidgetStateBorderSide.resolveWith((
|
||||
states,
|
||||
) {
|
||||
if (states.contains(
|
||||
WidgetState.selected,
|
||||
)) {
|
||||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline,
|
||||
);
|
||||
}),
|
||||
onChanged: (a) async {
|
||||
hideArchivedUsers = !hideArchivedUsers;
|
||||
await _filterUsers(lastQuery);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_otherUsers.isNotEmpty)
|
||||
UserList(
|
||||
List.from(_otherUsers),
|
||||
_otherUsers,
|
||||
selectedGroupIds: widget.selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
),
|
||||
|
|
@ -472,56 +491,52 @@ class UserList extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Step 1: Sort the users alphabetically
|
||||
groups.sort(
|
||||
(a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange),
|
||||
);
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
restorationId: 'new_message_users_list',
|
||||
itemCount: groups.length,
|
||||
itemBuilder: (context, i) {
|
||||
final group = groups[i];
|
||||
return ListTile(
|
||||
key: ValueKey(group.groupId),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(substringBy(group.groupName, 12)),
|
||||
FlameCounterWidget(
|
||||
groupId: group.groupId,
|
||||
prefix: true,
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: groups.length,
|
||||
(context, i) {
|
||||
final group = groups[i];
|
||||
return ListTile(
|
||||
key: ValueKey(group.groupId),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(substringBy(group.groupName, 12)),
|
||||
FlameCounterWidget(
|
||||
groupId: group.groupId,
|
||||
prefix: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: AvatarIcon(
|
||||
group: group,
|
||||
fontSize: 15,
|
||||
),
|
||||
trailing: Checkbox.adaptive(
|
||||
value: selectedGroupIds.contains(group.groupId),
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: AvatarIcon(
|
||||
group: group,
|
||||
fontSize: 15,
|
||||
),
|
||||
trailing: Checkbox.adaptive(
|
||||
value: selectedGroupIds.contains(group.groupId),
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(color: Theme.of(context).colorScheme.outline);
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
updateSelectedGroupIds(group.groupId, value);
|
||||
},
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
updateSelectedGroupIds(group.groupId, value);
|
||||
onTap: () {
|
||||
updateSelectedGroupIds(
|
||||
group.groupId,
|
||||
!selectedGroupIds.contains(group.groupId),
|
||||
);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
updateSelectedGroupIds(
|
||||
group.groupId,
|
||||
!selectedGroupIds.contains(group.groupId),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -538,6 +538,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
void updateSelectedGroupIds(String groupId, bool checked) {
|
||||
// ShareImageView rebuilds its own controls. Rebuilding this editor while it
|
||||
// is covered by that route needlessly redraws the full media composition.
|
||||
if (checked) {
|
||||
if (media.requiresAuthentication) {
|
||||
selectedGroupIds.clear();
|
||||
|
|
@ -546,7 +548,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
} else {
|
||||
selectedGroupIds.remove(groupId);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<ScreenshotImageHelper?> storeImageAsOriginal() =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/core/backup/backup_archive.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/backup.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class DatabaseSizesDeveloperView extends StatefulWidget {
|
||||
const DatabaseSizesDeveloperView({super.key});
|
||||
|
||||
@override
|
||||
State<DatabaseSizesDeveloperView> createState() =>
|
||||
_DatabaseSizesDeveloperViewState();
|
||||
}
|
||||
|
||||
class _DatabaseSizesDeveloperViewState
|
||||
extends State<DatabaseSizesDeveloperView> {
|
||||
late Future<BackupStorageInfo> _storageInfo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_storageInfo = RustBackupArchive.storageInfo();
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
final future = RustBackupArchive.storageInfo();
|
||||
setState(() => _storageInfo = future);
|
||||
// FutureBuilder displays any failure, including on pull-to-refresh.
|
||||
try {
|
||||
await future;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Database & Backup Sizes'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh sizes',
|
||||
onPressed: _reload,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<BackupStorageInfo>(
|
||||
future: _storageInfo,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Could not load storage sizes.\n${snapshot.error}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonal(
|
||||
onPressed: _reload,
|
||||
child: const Text('Try again'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final info = snapshot.requireData;
|
||||
final tableBytes = info.tables.fold<int>(
|
||||
0,
|
||||
(total, table) => total + table.tableSizeBytes,
|
||||
);
|
||||
final indexBytes = info.tables.fold<int>(
|
||||
0,
|
||||
(total, table) => total + table.indexSizeBytes,
|
||||
);
|
||||
final fileBytes = info.files.fold<int>(
|
||||
0,
|
||||
(total, file) => total + (file.sizeBytes ?? 0),
|
||||
);
|
||||
final overhead =
|
||||
info.databaseSizeBytes -
|
||||
info.freeSizeBytes -
|
||||
tableBytes -
|
||||
indexBytes;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _reload,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
children: [
|
||||
const _SectionTitle('Main database · app_db.sqlite'),
|
||||
_SizeTile(
|
||||
title: 'Allocated database size',
|
||||
bytes: info.databaseSizeBytes,
|
||||
),
|
||||
_SizeTile(title: 'Table data', bytes: tableBytes),
|
||||
_SizeTile(title: 'Indexes', bytes: indexBytes),
|
||||
_SizeTile(
|
||||
title: 'Free pages',
|
||||
subtitle: 'Allocated space available for reuse',
|
||||
bytes: info.freeSizeBytes,
|
||||
),
|
||||
_SizeTile(
|
||||
title: 'Schema and other overhead',
|
||||
bytes: overhead,
|
||||
),
|
||||
const Divider(),
|
||||
_SectionTitle('Tables · ${info.tables.length}'),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
'Largest first. Sizes include allocated table pages and '
|
||||
'all indexes belonging to each table, including automatic '
|
||||
'indexes. Empty tables can still occupy pages.',
|
||||
),
|
||||
),
|
||||
for (final table in info.tables)
|
||||
_SizeTile(
|
||||
title: table.name,
|
||||
subtitle:
|
||||
'${table.rows} rows · '
|
||||
'data ${formatBytes(table.tableSizeBytes)} · '
|
||||
'indexes ${formatBytes(table.indexSizeBytes)}',
|
||||
bytes: table.tableSizeBytes + table.indexSizeBytes,
|
||||
),
|
||||
const Divider(),
|
||||
const _SectionTitle('Contacts & messages backup contents'),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
'Current local file sizes before backup. Database '
|
||||
'snapshots, compression and encryption can change the '
|
||||
'final archive size. Photos and videos use the separate '
|
||||
'memories backup.',
|
||||
),
|
||||
),
|
||||
_SizeTile(title: 'Total source files', bytes: fileBytes),
|
||||
for (final file in info.files)
|
||||
_SizeTile(
|
||||
title: file.name,
|
||||
subtitle: _fileDescription(file.name),
|
||||
bytes: file.sizeBytes,
|
||||
),
|
||||
const ListTile(
|
||||
title: Text('backup-manifest.json'),
|
||||
subtitle: Text(
|
||||
'File sizes, checksums and schema versions. '
|
||||
'Generated when the backup is created.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _fileDescription(String name) => switch (name) {
|
||||
'app_db.sqlite' => 'Main database, including the tables above',
|
||||
'rust_db.sqlite' => 'Signal sessions, identities and encryption prekeys',
|
||||
'twonly.sqlite' => 'Legacy database retained for migration compatibility',
|
||||
'user_discovery_config.json' => 'User discovery configuration',
|
||||
'user.json' => 'Account settings and preferences',
|
||||
_ => 'Backup source file',
|
||||
};
|
||||
|
||||
class _SectionTitle extends StatelessWidget {
|
||||
const _SectionTitle(this.title);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
|
||||
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||
);
|
||||
}
|
||||
|
||||
class _SizeTile extends StatelessWidget {
|
||||
const _SizeTile({required this.title, required this.bytes, this.subtitle});
|
||||
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final int? bytes;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = bytes;
|
||||
final description = [
|
||||
?subtitle,
|
||||
if (size == null) 'Missing · skipped by backup',
|
||||
].join('\n');
|
||||
return ListTile(
|
||||
title: Text(title),
|
||||
subtitle: description.isEmpty ? null : Text(description),
|
||||
trailing: size == null
|
||||
? const Text('—')
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(formatBytes(size)),
|
||||
Text(
|
||||
'$size bytes',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import 'package:twonly/src/utils/storage.dart';
|
|||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/database_sizes_developer.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/home_widget_developer.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/developer/user_discovery_developer.view.dart';
|
||||
|
||||
|
|
@ -405,6 +406,12 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
|||
title: const Text('Informations'),
|
||||
onTap: () => context.push(Routes.settingsDeveloperInformations),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Database & Backup Sizes'),
|
||||
subtitle: const Text('Table sizes and backup contents'),
|
||||
onTap: () =>
|
||||
context.navPush(const DatabaseSizesDeveloperView()),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Show Retransmission Database'),
|
||||
onTap: () => context.push(
|
||||
|
|
|
|||
|
|
@ -190,12 +190,11 @@ packages:
|
|||
source: path
|
||||
version: "0.7.4+6"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera_avfoundation
|
||||
sha256: "843247230a583e04742ea1434ce3f91e536760922fab67c72ce46aa34fac3e9d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "dependencies/camera_avfoundation"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.10.3"
|
||||
camera_platform_interface:
|
||||
dependency: transitive
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
|||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.6.0+175
|
||||
version: 0.6.0+178
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
|
@ -97,6 +97,7 @@ dependencies:
|
|||
video_player_android: ^2.12.0
|
||||
video_player_avfoundation: ^2.11.0
|
||||
camera_android_camerax: ^0.7.4+6
|
||||
camera_avfoundation: ^0.10.3
|
||||
flutter_sharing_intent: ^2.0.4
|
||||
hand_signature: ^3.1.0+2
|
||||
hashlib: ^2.3.0
|
||||
|
|
@ -143,6 +144,8 @@ dependency_overrides:
|
|||
path: ./dependencies/video_player_avfoundation
|
||||
camera_android_camerax:
|
||||
path: ./dependencies/camera_android_camerax
|
||||
camera_avfoundation:
|
||||
path: ./dependencies/camera_avfoundation
|
||||
flutter_sharing_intent:
|
||||
path: ./dependencies/flutter_sharing_intent
|
||||
hand_signature:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id,\n COALESCE(c.display_name, c.username) AS \"sender_name!: String\",\n c.avatar_svg_compressed, c.sender_profile_counter,\n g.group_name AS conversation_name,\n COALESCE(g.is_direct_chat, 0) AS \"is_direct_chat!: i64\",\n n.message_id, n.kind, n.content, n.created_at,\n target_message.type AS target_message_type,\n target_media.type AS target_media_type\n FROM notification_outbox n\n JOIN contacts c ON c.user_id = n.sender_id\n LEFT JOIN groups g ON g.group_id = n.conversation_id\n LEFT JOIN messages target_message ON target_message.message_id = n.message_id\n LEFT JOIN media_files target_media ON target_media.media_id = target_message.media_id\n WHERE n.delivered_at IS NULL AND n.cleared_at IS NULL\n ORDER BY n.created_at, n.event_id\n LIMIT ?\n ",
|
||||
"query": "\n SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id,\n COALESCE(NULLIF(c.nick_name, ''), c.display_name, c.username) AS \"sender_name!: String\",\n g.group_name AS conversation_name,\n COALESCE(g.is_direct_chat, 0) AS \"is_direct_chat!: i64\",\n n.message_id, n.kind, n.content, n.created_at,\n target_message.type AS target_message_type,\n target_media.type AS target_media_type\n FROM notification_outbox n\n JOIN contacts c ON c.user_id = n.sender_id\n LEFT JOIN groups g ON g.group_id = n.conversation_id\n LEFT JOIN messages target_message ON target_message.message_id = n.message_id\n LEFT JOIN media_files target_media ON target_media.media_id = target_message.media_id\n WHERE n.delivered_at IS NULL AND n.cleared_at IS NULL\n ORDER BY n.created_at, n.event_id\n LIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
|
@ -50,34 +50,12 @@
|
|||
{
|
||||
"name": "sender_name!: String",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text",
|
||||
"type_info": "Null",
|
||||
"origin": "Expression"
|
||||
},
|
||||
{
|
||||
"name": "avatar_svg_compressed",
|
||||
"ordinal": 5,
|
||||
"type_info": "Blob",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "avatar_svg_compressed"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sender_profile_counter",
|
||||
"ordinal": 6,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "contacts",
|
||||
"name": "sender_profile_counter"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conversation_name",
|
||||
"ordinal": 7,
|
||||
"ordinal": 5,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
|
|
@ -88,13 +66,13 @@
|
|||
},
|
||||
{
|
||||
"name": "is_direct_chat!: i64",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer",
|
||||
"ordinal": 6,
|
||||
"type_info": "Null",
|
||||
"origin": "Expression"
|
||||
},
|
||||
{
|
||||
"name": "message_id",
|
||||
"ordinal": 9,
|
||||
"ordinal": 7,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
|
|
@ -105,7 +83,7 @@
|
|||
},
|
||||
{
|
||||
"name": "kind",
|
||||
"ordinal": 10,
|
||||
"ordinal": 8,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
|
|
@ -116,7 +94,7 @@
|
|||
},
|
||||
{
|
||||
"name": "content",
|
||||
"ordinal": 11,
|
||||
"ordinal": 9,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
|
|
@ -127,7 +105,7 @@
|
|||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 12,
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
|
|
@ -138,7 +116,7 @@
|
|||
},
|
||||
{
|
||||
"name": "target_message_type",
|
||||
"ordinal": 13,
|
||||
"ordinal": 11,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
|
|
@ -149,7 +127,7 @@
|
|||
},
|
||||
{
|
||||
"name": "target_media_type",
|
||||
"ordinal": 14,
|
||||
"ordinal": 12,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
|
|
@ -167,18 +145,16 @@
|
|||
false,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7ee254f54718b616580ea6ca979679f8ec8065490cf0ffd0a8f44818c1f57167"
|
||||
"hash": "6b0d6a1df75da706448489fd05157a51a9c093260171ec5b89934e3aee116f8e"
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ use crate::error::Result;
|
|||
use crate::keys::{DatabaseKey, KeyManager};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{AssertSqlSafe, Row};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{remove_file, File};
|
||||
use std::io::{copy, Cursor};
|
||||
|
|
@ -52,7 +53,104 @@ struct BackupManifestFile {
|
|||
sha256: String,
|
||||
}
|
||||
|
||||
pub struct BackupStorageInfo {
|
||||
pub database_size_bytes: i64,
|
||||
pub free_size_bytes: i64,
|
||||
pub tables: Vec<BackupTableSize>,
|
||||
pub files: Vec<BackupFileSize>,
|
||||
}
|
||||
|
||||
pub struct BackupTableSize {
|
||||
pub name: String,
|
||||
pub rows: i64,
|
||||
pub table_size_bytes: i64,
|
||||
pub index_size_bytes: i64,
|
||||
}
|
||||
|
||||
pub struct BackupFileSize {
|
||||
pub name: String,
|
||||
/// None means the source file is absent and will be skipped by the backup.
|
||||
pub size_bytes: Option<i64>,
|
||||
}
|
||||
|
||||
impl BackupArchive {
|
||||
/// Read current storage usage without creating or uploading a backup.
|
||||
pub(crate) async fn storage_info(ctx: &Context) -> Result<BackupStorageInfo> {
|
||||
let files = {
|
||||
let keys = ctx.key_manager.lock().await;
|
||||
Self::get_backup_files(ctx, &keys)?
|
||||
.into_iter()
|
||||
.map(|(name, directory, _, mut key)| {
|
||||
key.zeroize();
|
||||
let size_bytes = match directory.join(name).metadata() {
|
||||
Ok(metadata) => Some(metadata.len() as i64),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
Ok(BackupFileSize {
|
||||
name: name.to_owned(),
|
||||
size_bytes,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
};
|
||||
|
||||
let database = ctx.app_db.read().await.clone();
|
||||
let mut transaction = database.pool.begin().await?;
|
||||
// SQLCipher returns page_size as TEXT for encrypted databases, unlike
|
||||
// SQLite. Cast the table-valued pragma so both return an integer.
|
||||
let page_size: i64 =
|
||||
sqlx::query_scalar("SELECT CAST(page_size AS INTEGER) FROM pragma_page_size")
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
let page_count: i64 = sqlx::query_scalar("PRAGMA page_count")
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
let free_pages: i64 = sqlx::query_scalar("PRAGMA freelist_count")
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
// Attribute each index (including SQLite's automatic indexes) to its
|
||||
// owning table. dbstat includes overflow pages for large text/blobs.
|
||||
let table_sizes = sqlx::query(
|
||||
"SELECT t.name, \
|
||||
COALESCE(SUM(CASE WHEN s.type = 'table' THEN d.pgsize ELSE 0 END), 0) AS table_bytes, \
|
||||
COALESCE(SUM(CASE WHEN s.type = 'index' THEN d.pgsize ELSE 0 END), 0) AS index_bytes \
|
||||
FROM sqlite_schema t \
|
||||
LEFT JOIN sqlite_schema s ON s.tbl_name = t.name AND s.type IN ('table', 'index') \
|
||||
LEFT JOIN dbstat d ON d.name = s.name \
|
||||
WHERE t.type = 'table' AND t.name NOT LIKE 'sqlite_%' \
|
||||
GROUP BY t.name ORDER BY table_bytes + index_bytes DESC, t.name",
|
||||
)
|
||||
.fetch_all(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let mut tables = Vec::with_capacity(table_sizes.len());
|
||||
for table in table_sizes {
|
||||
let name: String = table.try_get("name")?;
|
||||
let quoted_name = name.replace('"', "\"\"");
|
||||
let rows = sqlx::query_scalar(AssertSqlSafe(format!(
|
||||
"SELECT COUNT(*) FROM \"{quoted_name}\""
|
||||
)))
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
tables.push(BackupTableSize {
|
||||
name,
|
||||
rows,
|
||||
table_size_bytes: table.try_get("table_bytes")?,
|
||||
index_size_bytes: table.try_get("index_bytes")?,
|
||||
});
|
||||
}
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(BackupStorageInfo {
|
||||
database_size_bytes: page_count * page_size,
|
||||
free_size_bytes: free_pages * page_size,
|
||||
tables,
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn get_backup_files(
|
||||
ctx: &Context,
|
||||
|
|
@ -440,6 +538,98 @@ mod tests {
|
|||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn storage_info_measures_pages_indexes_and_backup_sources() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let ctx = Context::init_for_testing(
|
||||
temp_dir.path().join("database"),
|
||||
temp_dir.path().join("data"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let data_dir = PathBuf::from(&ctx.config.data_dir);
|
||||
std::fs::create_dir_all(data_dir.join("keyvalue")).unwrap();
|
||||
std::fs::write(data_dir.join("keyvalue/user.json"), "{\"userId\":1}").unwrap();
|
||||
std::fs::write(data_dir.join("user_discovery_config.json"), "{}").unwrap();
|
||||
|
||||
let database = ctx.app_db.read().await.clone();
|
||||
// A quoted identifier exercises row counting; the large blobs require
|
||||
// overflow pages, and UNIQUE creates an automatic index.
|
||||
sqlx::query("CREATE TABLE \"size\"\"test\" (name TEXT UNIQUE, payload BLOB)")
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("CREATE INDEX size_test_payload ON \"size\"\"test\" (payload)")
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(
|
||||
"INSERT INTO \"size\"\"test\" VALUES ('first', zeroblob(100000)), ('second', zeroblob(100000))",
|
||||
)
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let info = BackupArchive::storage_info(&ctx).await.unwrap();
|
||||
let table = info.tables.iter().find(|t| t.name == "size\"test").unwrap();
|
||||
assert_eq!(table.rows, 2);
|
||||
assert!(table.table_size_bytes >= 200000);
|
||||
assert!(table.index_size_bytes >= 200000);
|
||||
let empty = info.tables.iter().find(|t| t.name == "contacts").unwrap();
|
||||
assert_eq!(empty.rows, 0);
|
||||
assert!(empty.table_size_bytes > 0);
|
||||
assert!(info.tables.windows(2).all(|pair| {
|
||||
pair[0].table_size_bytes + pair[0].index_size_bytes
|
||||
>= pair[1].table_size_bytes + pair[1].index_size_bytes
|
||||
}));
|
||||
let used_bytes: i64 = info
|
||||
.tables
|
||||
.iter()
|
||||
.map(|t| t.table_size_bytes + t.index_size_bytes)
|
||||
.sum();
|
||||
assert!(used_bytes + info.free_size_bytes <= info.database_size_bytes);
|
||||
let main_file = info
|
||||
.files
|
||||
.iter()
|
||||
.find(|f| f.name == APP_DATABASE_FILE)
|
||||
.unwrap();
|
||||
assert_eq!(main_file.size_bytes, Some(info.database_size_bytes));
|
||||
assert_eq!(info.files.len(), 5);
|
||||
for (name, size) in [
|
||||
("twonly.sqlite", None),
|
||||
("user.json", Some(12)),
|
||||
("user_discovery_config.json", Some(2)),
|
||||
] {
|
||||
assert_eq!(
|
||||
info.files
|
||||
.iter()
|
||||
.find(|f| f.name == name)
|
||||
.unwrap()
|
||||
.size_bytes,
|
||||
size
|
||||
);
|
||||
}
|
||||
assert!(!data_dir.join("temp_backup_dir").exists());
|
||||
assert!(!data_dir.join("temp_backup.zip").exists());
|
||||
|
||||
sqlx::query("DELETE FROM \"size\"\"test\" WHERE 1")
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let after_delete = BackupArchive::storage_info(&ctx).await.unwrap();
|
||||
assert_eq!(after_delete.database_size_bytes, info.database_size_bytes);
|
||||
assert!(after_delete.free_size_bytes > info.free_size_bytes);
|
||||
assert_eq!(
|
||||
after_delete
|
||||
.tables
|
||||
.iter()
|
||||
.find(|t| t.name == "size\"test")
|
||||
.unwrap()
|
||||
.rows,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_backup_and_restore() {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::backup::backup_archive::BackupArchive;
|
||||
pub use crate::backup::backup_archive::{BackupFileSize, BackupStorageInfo, BackupTableSize};
|
||||
use crate::backup::backup_identity::BackupIdentity;
|
||||
use crate::bridge::get_twonly_flutter;
|
||||
use crate::context::Context;
|
||||
|
|
@ -73,6 +74,10 @@ impl RustBackupIdentity {
|
|||
}
|
||||
}
|
||||
impl RustBackupArchive {
|
||||
pub async fn storage_info() -> Result<BackupStorageInfo> {
|
||||
BackupArchive::storage_info(Context::get_static()?).await
|
||||
}
|
||||
|
||||
pub async fn create_backup_archive() -> Result<(String, String)> {
|
||||
let ctx = Context::get_static()?;
|
||||
let path = BackupArchive::create_backup(ctx).await?;
|
||||
|
|
|
|||
|
|
@ -138,27 +138,12 @@ async fn import(
|
|||
)))
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
let target_count: i64 = sqlx::query_scalar(AssertSqlSafe(format!(
|
||||
r#"SELECT COUNT(*) FROM main."{table}""#
|
||||
)))
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
if source_count != target_count {
|
||||
return Err(TwonlyError::Generic(format!(
|
||||
"Row count mismatch for {table}: source={source_count}, target={target_count}"
|
||||
)));
|
||||
}
|
||||
let mismatch: Option<i64> = sqlx::query_scalar(AssertSqlSafe(format!(
|
||||
r#"SELECT 1 FROM (SELECT {quoted} FROM main."{table}" EXCEPT SELECT {quoted} FROM legacy."{table}") LIMIT 1"#
|
||||
)))
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
let reverse_mismatch: Option<i64> = sqlx::query_scalar(AssertSqlSafe(format!(
|
||||
r#"SELECT 1 FROM (SELECT {quoted} FROM legacy."{table}" EXCEPT SELECT {quoted} FROM main."{table}") LIMIT 1"#
|
||||
)))
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
if mismatch.is_some() || reverse_mismatch.is_some() {
|
||||
if reverse_mismatch.is_some() {
|
||||
return Err(TwonlyError::Generic(format!(
|
||||
"Data mismatch while importing {table}"
|
||||
)));
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 926647182;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1385877172;
|
||||
|
||||
// Section: executor
|
||||
|
||||
|
|
@ -5515,6 +5515,43 @@ fn wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_arch
|
|||
})().await)
|
||||
} })
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__backup__rust_backup_archive_storage_info_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_backup_archive_storage_info",
|
||||
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::wrapper::backup::RustBackupArchive::storage_info()
|
||||
.await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
|
|
@ -7039,6 +7076,18 @@ impl SseDecode for crate::bridge::api::ApiEventKind {
|
|||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::backup::backup_archive::BackupFileSize {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_name = <String>::sse_decode(deserializer);
|
||||
let mut var_sizeBytes = <Option<i64>>::sse_decode(deserializer);
|
||||
return crate::backup::backup_archive::BackupFileSize {
|
||||
name: var_name,
|
||||
size_bytes: var_sizeBytes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::keys::backup_password_keys::BackupPasswordKeys {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
|
|
@ -7051,6 +7100,40 @@ impl SseDecode for crate::keys::backup_password_keys::BackupPasswordKeys {
|
|||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::backup::backup_archive::BackupStorageInfo {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_databaseSizeBytes = <i64>::sse_decode(deserializer);
|
||||
let mut var_freeSizeBytes = <i64>::sse_decode(deserializer);
|
||||
let mut var_tables =
|
||||
<Vec<crate::backup::backup_archive::BackupTableSize>>::sse_decode(deserializer);
|
||||
let mut var_files =
|
||||
<Vec<crate::backup::backup_archive::BackupFileSize>>::sse_decode(deserializer);
|
||||
return crate::backup::backup_archive::BackupStorageInfo {
|
||||
database_size_bytes: var_databaseSizeBytes,
|
||||
free_size_bytes: var_freeSizeBytes,
|
||||
tables: var_tables,
|
||||
files: var_files,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::backup::backup_archive::BackupTableSize {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_name = <String>::sse_decode(deserializer);
|
||||
let mut var_rows = <i64>::sse_decode(deserializer);
|
||||
let mut var_tableSizeBytes = <i64>::sse_decode(deserializer);
|
||||
let mut var_indexSizeBytes = <i64>::sse_decode(deserializer);
|
||||
return crate::backup::backup_archive::BackupTableSize {
|
||||
name: var_name,
|
||||
rows: var_rows,
|
||||
table_size_bytes: var_tableSizeBytes,
|
||||
index_size_bytes: var_indexSizeBytes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for bool {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
|
|
@ -7357,6 +7440,32 @@ impl SseDecode for Vec<String> {
|
|||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<crate::backup::backup_archive::BackupFileSize> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut len_ = <i32>::sse_decode(deserializer);
|
||||
let mut ans_ = Vec::with_capacity(len_ as usize);
|
||||
for idx_ in 0..len_ {
|
||||
ans_.push(<crate::backup::backup_archive::BackupFileSize>::sse_decode(
|
||||
deserializer,
|
||||
));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<crate::backup::backup_archive::BackupTableSize> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut len_ = <i32>::sse_decode(deserializer);
|
||||
let mut ans_ = Vec::with_capacity(len_ as usize);
|
||||
for idx_ in 0..len_ {
|
||||
ans_.push(<crate::backup::backup_archive::BackupTableSize>::sse_decode(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<crate::bridge::api::FrbAdditionalAccount> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
|
|
@ -8510,43 +8619,44 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||
145 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
||||
146 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
|
||||
147 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
||||
148 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
149 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
150 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
|
||||
151 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
152 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
|
||||
153 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||
157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
158 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
|
||||
159 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||
160 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
|
||||
161 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
||||
162 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
163 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||
164 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||
165 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||
166 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||
167 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
168 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len),
|
||||
169 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
170 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||
171 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
|
||||
172 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||
173 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||
174 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
175 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||
176 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len),
|
||||
177 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
||||
178 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||
179 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
||||
181 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
||||
182 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
||||
183 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
||||
184 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
||||
185 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
148 => wire__crate__bridge__wrapper__backup__rust_backup_archive_storage_info_impl(port, ptr, rust_vec_len, data_len),
|
||||
149 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
150 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
151 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
|
||||
152 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
153 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
|
||||
154 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||
158 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
159 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
|
||||
160 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||
161 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
|
||||
162 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
||||
163 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
164 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||
165 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||
166 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||
167 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||
168 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
169 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len),
|
||||
170 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||
171 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||
172 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
|
||||
173 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||
174 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||
175 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
176 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||
177 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len),
|
||||
178 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
||||
179 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||
180 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
||||
182 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
||||
183 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
||||
184 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
||||
185 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
||||
186 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -8565,12 +8675,12 @@ fn pde_ffi_dispatcher_sync_impl(
|
|||
56 => {
|
||||
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
||||
}
|
||||
180 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
||||
181 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
186 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
||||
187 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -8651,6 +8761,27 @@ impl flutter_rust_bridge::IntoIntoDart<crate::bridge::api::ApiEventKind>
|
|||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::backup::backup_archive::BackupFileSize {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
self.name.into_into_dart().into_dart(),
|
||||
self.size_bytes.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::backup::backup_archive::BackupFileSize
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::backup::backup_archive::BackupFileSize>
|
||||
for crate::backup::backup_archive::BackupFileSize
|
||||
{
|
||||
fn into_into_dart(self) -> crate::backup::backup_archive::BackupFileSize {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::keys::backup_password_keys::BackupPasswordKeys {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
|
|
@ -8672,6 +8803,52 @@ impl flutter_rust_bridge::IntoIntoDart<crate::keys::backup_password_keys::Backup
|
|||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::backup::backup_archive::BackupStorageInfo {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
self.database_size_bytes.into_into_dart().into_dart(),
|
||||
self.free_size_bytes.into_into_dart().into_dart(),
|
||||
self.tables.into_into_dart().into_dart(),
|
||||
self.files.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::backup::backup_archive::BackupStorageInfo
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::backup::backup_archive::BackupStorageInfo>
|
||||
for crate::backup::backup_archive::BackupStorageInfo
|
||||
{
|
||||
fn into_into_dart(self) -> crate::backup::backup_archive::BackupStorageInfo {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::backup::backup_archive::BackupTableSize {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
self.name.into_into_dart().into_dart(),
|
||||
self.rows.into_into_dart().into_dart(),
|
||||
self.table_size_bytes.into_into_dart().into_dart(),
|
||||
self.index_size_bytes.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::backup::backup_archive::BackupTableSize
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::backup::backup_archive::BackupTableSize>
|
||||
for crate::backup::backup_archive::BackupTableSize
|
||||
{
|
||||
fn into_into_dart(self) -> crate::backup::backup_archive::BackupTableSize {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::services::avatars::CustomAvatarInfo {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
|
|
@ -9861,6 +10038,14 @@ impl SseEncode for crate::bridge::api::ApiEventKind {
|
|||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::backup::backup_archive::BackupFileSize {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<String>::sse_encode(self.name, serializer);
|
||||
<Option<i64>>::sse_encode(self.size_bytes, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::keys::backup_password_keys::BackupPasswordKeys {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
|
|
@ -9869,6 +10054,26 @@ impl SseEncode for crate::keys::backup_password_keys::BackupPasswordKeys {
|
|||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::backup::backup_archive::BackupStorageInfo {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i64>::sse_encode(self.database_size_bytes, serializer);
|
||||
<i64>::sse_encode(self.free_size_bytes, serializer);
|
||||
<Vec<crate::backup::backup_archive::BackupTableSize>>::sse_encode(self.tables, serializer);
|
||||
<Vec<crate::backup::backup_archive::BackupFileSize>>::sse_encode(self.files, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::backup::backup_archive::BackupTableSize {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<String>::sse_encode(self.name, serializer);
|
||||
<i64>::sse_encode(self.rows, serializer);
|
||||
<i64>::sse_encode(self.table_size_bytes, serializer);
|
||||
<i64>::sse_encode(self.index_size_bytes, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for bool {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
|
|
@ -10095,6 +10300,26 @@ impl SseEncode for Vec<String> {
|
|||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<crate::backup::backup_archive::BackupFileSize> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i32>::sse_encode(self.len() as _, serializer);
|
||||
for item in self {
|
||||
<crate::backup::backup_archive::BackupFileSize>::sse_encode(item, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<crate::backup::backup_archive::BackupTableSize> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i32>::sse_encode(self.len() as _, serializer);
|
||||
for item in self {
|
||||
<crate::backup::backup_archive::BackupTableSize>::sse_encode(item, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<crate::bridge::api::FrbAdditionalAccount> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
|
|
|
|||
|
|
@ -570,7 +570,7 @@ pub async fn pending_batch(ctx: &Arc<Context>, locale: &str) -> Result<Notificat
|
|||
PendingRow,
|
||||
r#"
|
||||
SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id,
|
||||
COALESCE(c.display_name, c.username) AS "sender_name!: String",
|
||||
COALESCE(NULLIF(c.nick_name, ''), c.display_name, c.username) AS "sender_name!: String",
|
||||
g.group_name AS conversation_name,
|
||||
COALESCE(g.is_direct_chat, 0) AS "is_direct_chat!: i64",
|
||||
n.message_id, n.kind, n.content, n.created_at,
|
||||
|
|
|
|||
144
test/drift/groups_dao_test.dart
Normal file
144
test/drift/groups_dao_test.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:twonly/src/database/tables/groups.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
||||
void main() {
|
||||
late TwonlyDB database;
|
||||
|
||||
setUp(() async {
|
||||
database = TwonlyDB(NativeDatabase.memory());
|
||||
for (final statement in [
|
||||
"INSERT INTO contacts(user_id, username) VALUES (7, 'alice'), (8, 'bob'), (9, 'carol'), (10, 'former')",
|
||||
"INSERT INTO groups(group_id, group_name) VALUES ('group', 'Friends'), ('other', 'Other')",
|
||||
"INSERT INTO groups(group_id, group_name, is_direct_chat) VALUES ('direct', 'Alice', 1)",
|
||||
// Legacy (null), normal and admin members must all remain visible.
|
||||
'INSERT INTO group_members(group_id, contact_id, member_state, last_type_indicator) '
|
||||
"VALUES ('group', 7, NULL, 1), ('group', 8, 'normal', 1), ('group', 9, 'admin', 1), ('group', 10, 'leftGroup', 1)",
|
||||
"INSERT INTO group_members(group_id, contact_id) VALUES ('other', 7), ('direct', 7)",
|
||||
'INSERT INTO messages(group_id, message_id, sender_id, type, content) '
|
||||
"VALUES ('group', 'message', 7, 'text', 'Hello')",
|
||||
'INSERT INTO group_histories(group_history_id, group_id, affected_contact_id, type) '
|
||||
"VALUES ('removal', 'group', 7, 'removedMember')",
|
||||
]) {
|
||||
await database.customStatement(statement);
|
||||
}
|
||||
});
|
||||
|
||||
tearDown(() => database.close());
|
||||
|
||||
Future<void> setMemberState(MemberState state) =>
|
||||
database.groupsDao.insertOrUpdateGroupMember(
|
||||
GroupMembersCompanion(
|
||||
groupId: const Value('group'),
|
||||
contactId: const Value(7),
|
||||
memberState: Value(state),
|
||||
),
|
||||
);
|
||||
|
||||
test(
|
||||
'removed members leave active queries while history is retained',
|
||||
() async {
|
||||
expect(
|
||||
(await database.groupsDao.getGroupContact(
|
||||
'group',
|
||||
)).map((c) => c.userId),
|
||||
unorderedEquals([7, 8, 9]),
|
||||
);
|
||||
|
||||
await setMemberState(MemberState.leftGroup);
|
||||
|
||||
expect(
|
||||
(await database.groupsDao.getGroupContact(
|
||||
'group',
|
||||
)).map((c) => c.userId),
|
||||
unorderedEquals([8, 9]),
|
||||
);
|
||||
expect(
|
||||
(await database.groupsDao.getGroupNonLeftMembers(
|
||||
'group',
|
||||
)).map((m) => m.contactId),
|
||||
unorderedEquals([8, 9]),
|
||||
);
|
||||
expect(
|
||||
await database.groupsDao.getAllGroupMembers('group'),
|
||||
hasLength(4),
|
||||
);
|
||||
expect(
|
||||
(await database.select(database.messages).get()).single.content,
|
||||
'Hello',
|
||||
);
|
||||
expect(
|
||||
(await database.groupsDao.watchGroupActions('group').first)
|
||||
.single
|
||||
.affectedContactId,
|
||||
7,
|
||||
);
|
||||
expect(
|
||||
(await database.groupsDao.getGroupContact('other')).single.userId,
|
||||
7,
|
||||
);
|
||||
expect(
|
||||
(await database.groupsDao.getGroupContact('direct')).single.userId,
|
||||
7,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final memberStreams = <String, Stream<List<int>> Function()>{
|
||||
'member list': () => database.groupsDao
|
||||
.watchGroupMembers('group')
|
||||
.map((rows) => rows.map((row) => row.$1.userId).toList()),
|
||||
'group contacts': () => database.groupsDao
|
||||
.watchGroupContact('group')
|
||||
.map((rows) => rows.map((row) => row.userId).toList()),
|
||||
'chat list members': () => database.groupsDao.watchAllGroupMembers().map(
|
||||
(rows) => rows
|
||||
.where((row) => row.$2.groupId == 'group')
|
||||
.map((row) => row.$1.userId)
|
||||
.toList(),
|
||||
),
|
||||
'typing members': () => database.groupsDao.watchTypingGroupMembers().map(
|
||||
(rows) => rows.map((row) => row.contactId).toList(),
|
||||
),
|
||||
};
|
||||
|
||||
for (final entry in memberStreams.entries) {
|
||||
test('${entry.key} updates on removal and rejoining', () async {
|
||||
final stream = StreamIterator(entry.value());
|
||||
addTearDown(stream.cancel);
|
||||
expect(await stream.moveNext(), isTrue);
|
||||
expect(stream.current, unorderedEquals([7, 8, 9]));
|
||||
|
||||
final removal = stream.moveNext();
|
||||
await setMemberState(MemberState.leftGroup);
|
||||
expect(await removal, isTrue);
|
||||
expect(stream.current, unorderedEquals([8, 9]));
|
||||
|
||||
final rejoining = stream.moveNext();
|
||||
await setMemberState(MemberState.normal);
|
||||
expect(await rejoining, isTrue);
|
||||
expect(stream.current, unorderedEquals([7, 8, 9]));
|
||||
});
|
||||
}
|
||||
|
||||
test('mutual groups update when a member is removed', () async {
|
||||
final stream = StreamIterator(
|
||||
database.groupsDao.watchNonDirectGroupsForMember(7),
|
||||
);
|
||||
addTearDown(stream.cancel);
|
||||
expect(await stream.moveNext(), isTrue);
|
||||
expect(
|
||||
stream.current.map((g) => g.groupId),
|
||||
unorderedEquals(['group', 'other']),
|
||||
);
|
||||
|
||||
final removal = stream.moveNext();
|
||||
await setMemberState(MemberState.leftGroup);
|
||||
expect(await removal, isTrue);
|
||||
expect(stream.current.map((g) => g.groupId), ['other']);
|
||||
});
|
||||
}
|
||||
117
test/scripts/test_dependencies.py
Normal file
117
test/scripts/test_dependencies.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().parents[2] / "dependencies.py"
|
||||
spec = importlib.util.spec_from_file_location("dependency_updater", SCRIPT_PATH)
|
||||
updater = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(updater)
|
||||
|
||||
|
||||
class CustomDependencyTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp_dir.cleanup)
|
||||
self.root = Path(self.temp_dir.name)
|
||||
self.cache = self.root / "cache"
|
||||
self.output = self.root / "dependencies"
|
||||
|
||||
def create_package(self, root, name, version, contents):
|
||||
package = root / name
|
||||
(package / "lib").mkdir(parents=True)
|
||||
(package / "pubspec.yaml").write_text(f"name: {name}\nversion: {version}\n")
|
||||
(package / "lib" / "camera.dart").write_text(contents)
|
||||
return package
|
||||
|
||||
def test_custom_package_is_preserved_without_cache_or_git(self):
|
||||
package = self.create_package(self.output, "camera", "0.10.3", "custom code")
|
||||
for cache_only in (False, True):
|
||||
with self.subTest(cache_only=cache_only), patch.object(
|
||||
updater.subprocess, "run"
|
||||
) as git:
|
||||
result = updater.integrate_package(
|
||||
"camera", {"custom_changes": True},
|
||||
self.cache, self.output, cache_only=cache_only,
|
||||
)
|
||||
self.assertEqual(result, [("camera", "0.10.3")])
|
||||
self.assertEqual((package / "lib" / "camera.dart").read_text(), "custom code")
|
||||
git.assert_not_called()
|
||||
|
||||
def test_custom_subpackage_is_preserved_while_other_package_updates(self):
|
||||
custom = self.create_package(self.output, "camera", "0.7.4+6", "custom code")
|
||||
self.create_package(self.output, "video", "1.0.0", "old code")
|
||||
self.create_package(self.cache / "flutter_packages", "video", "2.0.0", "new code")
|
||||
# The protected camera source is deliberately absent from the cache.
|
||||
result = updater.integrate_package(
|
||||
"flutter_packages",
|
||||
{"subpackages": [
|
||||
{"name": "camera", "path": "camera", "custom_changes": True},
|
||||
{"name": "video", "path": "video"},
|
||||
]},
|
||||
self.cache, self.output, cache_only=True,
|
||||
)
|
||||
self.assertEqual(result, [("camera", "0.7.4+6"), ("video", "2.0.0")])
|
||||
self.assertEqual((custom / "lib" / "camera.dart").read_text(), "custom code")
|
||||
self.assertEqual((self.output / "video" / "lib" / "camera.dart").read_text(), "new code")
|
||||
|
||||
def test_all_custom_subpackages_need_no_cache_or_git(self):
|
||||
self.create_package(self.output, "camera", "0.7.4+6", "custom code")
|
||||
with patch.object(updater.subprocess, "run") as git:
|
||||
result = updater.integrate_package(
|
||||
"flutter_packages",
|
||||
{"subpackages": [{"name": "camera", "custom_changes": True}]},
|
||||
self.cache, self.output,
|
||||
)
|
||||
self.assertEqual(result, [("camera", "0.7.4+6")])
|
||||
git.assert_not_called()
|
||||
|
||||
def test_missing_custom_package_fails_without_fetching_or_recreating_it(self):
|
||||
with patch.object(updater.subprocess, "run") as git:
|
||||
with self.assertRaisesRegex(FileNotFoundError, "Custom package not found"):
|
||||
updater.integrate_package(
|
||||
"camera", {"custom_changes": True}, self.cache, self.output,
|
||||
)
|
||||
git.assert_not_called()
|
||||
self.assertFalse((self.output / "camera").exists())
|
||||
|
||||
def test_full_update_retains_custom_flags_and_managed_pubspec_entries(self):
|
||||
self.create_package(self.output, "camera", "0.10.3", "custom code")
|
||||
config = {
|
||||
"cache": str(self.cache), "outdir": str(self.output),
|
||||
"dependencies": {"camera": {"custom_changes": True}},
|
||||
}
|
||||
(self.root / "dependencies.yaml").write_text(yaml.safe_dump(config))
|
||||
(self.root / "pubspec.yaml").write_text(
|
||||
"dependencies:\n"
|
||||
"## --- Start Managed Dependencies ---\n"
|
||||
"## --- End Managed Dependencies ---\n"
|
||||
"dependency_overrides:\n"
|
||||
"## --- Start Managed Dependency Overrides ---\n"
|
||||
"## --- End Managed Dependency Overrides ---\n"
|
||||
)
|
||||
previous_cwd = Path.cwd()
|
||||
try:
|
||||
os.chdir(self.root)
|
||||
with patch("sys.argv", ["dependencies.py"]), patch.object(
|
||||
updater.subprocess, "run"
|
||||
) as git:
|
||||
updater.main()
|
||||
git.assert_not_called()
|
||||
finally:
|
||||
os.chdir(previous_cwd)
|
||||
pubspec = yaml.safe_load((self.root / "pubspec.yaml").read_text())
|
||||
self.assertEqual(pubspec["dependencies"]["camera"], "^0.10.3")
|
||||
self.assertEqual(pubspec["dependency_overrides"]["camera"]["path"], str(self.output / "camera"))
|
||||
saved_config = yaml.safe_load((self.root / "dependencies.yaml").read_text())
|
||||
self.assertTrue(saved_config["dependencies"]["camera"]["custom_changes"])
|
||||
self.assertEqual((self.output / "camera" / "lib" / "camera.dart").read_text(), "custom code")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
16
test/visual/camera_preview_resolution_test.dart
Normal file
16
test/visual/camera_preview_resolution_test.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart';
|
||||
|
||||
void main() {
|
||||
test('selfie preview uses 720p while rear screenshot detail stays 1080p', () {
|
||||
expect(
|
||||
cameraPreviewResolution(CameraLensDirection.front),
|
||||
ResolutionPreset.high,
|
||||
);
|
||||
expect(
|
||||
cameraPreviewResolution(CameraLensDirection.back),
|
||||
ResolutionPreset.veryHigh,
|
||||
);
|
||||
});
|
||||
}
|
||||
88
test/visual/components/avatar_icon_test.dart
Normal file
88
test/visual/components/avatar_icon_test.dart
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:twonly/core/frb_generated.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||
|
||||
import '../../mocks/user_config.dart';
|
||||
|
||||
class _AvatarApi implements RustLibApi {
|
||||
String? path;
|
||||
|
||||
@override
|
||||
Future<String?> crateBridgeApiRustApiCurrentUserAvatarPath() async => path;
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
final api = _AvatarApi();
|
||||
final photoPath = File('assets/images/default_avatar.png').absolute.path;
|
||||
|
||||
setUpAll(() => RustLib.initMock(api: api));
|
||||
tearDownAll(RustLib.dispose);
|
||||
|
||||
setUp(() async {
|
||||
await locator.reset();
|
||||
locator.registerSingleton<UserService>(UserService());
|
||||
userService.currentUser = testUserConfig(
|
||||
userId: 1,
|
||||
username: 'me',
|
||||
displayName: 'Me',
|
||||
subscriptionPlan: 'Free',
|
||||
currentSetupPage: null,
|
||||
appVersion: 100,
|
||||
);
|
||||
api.path = null;
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await locator.reset();
|
||||
});
|
||||
|
||||
Future<void> pumpAvatar(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: AvatarIcon(myAvatar: true)),
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
void expectPhoto(WidgetTester tester) {
|
||||
final image = tester.widget<Image>(find.byType(Image));
|
||||
expect((image.image as FileImage).file.path, photoPath);
|
||||
expect(find.byType(SvgPicture), findsNothing);
|
||||
}
|
||||
|
||||
testWidgets('shows a photo avatar without a public SVG avatar', (
|
||||
tester,
|
||||
) async {
|
||||
api.path = photoPath;
|
||||
await pumpAvatar(tester);
|
||||
expectPhoto(tester);
|
||||
});
|
||||
|
||||
testWidgets('refreshes when a photo-only avatar is added or removed', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpAvatar(tester);
|
||||
expect(find.byType(SvgPicture), findsOneWidget);
|
||||
|
||||
api.path = photoPath;
|
||||
userService.triggerUserUpdate();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expectPhoto(tester);
|
||||
|
||||
api.path = null;
|
||||
userService.triggerUserUpdate();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.byType(Image), findsNothing);
|
||||
expect(find.byType(SvgPicture), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue