Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d54cb957c | ||
|
|
b930f3c201 | ||
|
|
830c42da93 | ||
|
|
e28c68a0ae | ||
|
|
f59090cec8 | ||
|
|
2a092fc9f8 | ||
|
|
5fa8583cdf | ||
|
|
9ab78e0ab8 | ||
|
|
d13b5e8782 | ||
|
|
76931f7b1d | ||
|
|
115843ce51 | ||
|
|
713f13a46d | ||
|
|
560d6fc683 | ||
|
|
c0f8b166ea | ||
|
|
6b68367c0d | ||
|
|
d80ed4caa7 | ||
|
|
a627ed39f2 | ||
|
|
5d00213d95 | ||
|
|
2e3bb5dcc3 | ||
|
|
749d44650d | ||
|
|
3ff2d6ef84 | ||
|
|
18c72ebf1d |
@@ -47,3 +47,4 @@ app.*.map.json
|
||||
|
||||
# Rust/CargoKit compiled native libraries
|
||||
android/app/src/main/jniLibs/
|
||||
rust/target/
|
||||
|
||||
@@ -21,7 +21,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "me.elcaju"
|
||||
minSdk 21 // Required for FlutterSecureStorage (EncryptedSharedPreferences)
|
||||
minSdkVersion flutter.minSdkVersion // Required for FlutterSecureStorage (EncryptedSharedPreferences)
|
||||
targetSdk flutter.targetSdkVersion
|
||||
versionCode flutter.versionCode
|
||||
versionName flutter.versionName
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<!-- Cámara opcional: la app funciona sin ella (pegando desde portapapeles) -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
|
||||
<!-- NFC para compartir tokens Cashu -->
|
||||
<uses-permission android:name="android.permission.NFC"/>
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
|
||||
|
||||
<application
|
||||
android:label="ElCaju"
|
||||
@@ -34,6 +37,19 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- HCE service: phone emulates NFC tag for phone-to-phone transfers -->
|
||||
<service
|
||||
android:name=".NfcHceService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_NFC_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.nfc.cardemulation.host_apdu_service"
|
||||
android:resource="@xml/hce_nfc" />
|
||||
</service>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
package me.elcaju
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.cardemulation.CardEmulation
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val CHANNEL = "me.elcaju/nfc_hce"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"setPayload" -> {
|
||||
val payload = call.argument<ByteArray>("payload")
|
||||
NfcHceService.ndefPayload = payload
|
||||
|
||||
// Force Android to route the NDEF AID to our service
|
||||
// instead of manufacturer services (Xiaomi Mi Share, etc.)
|
||||
try {
|
||||
val adapter = NfcAdapter.getDefaultAdapter(this)
|
||||
if (adapter != null) {
|
||||
val cardEmulation = CardEmulation.getInstance(adapter)
|
||||
cardEmulation.setPreferredService(
|
||||
this,
|
||||
ComponentName(this, NfcHceService::class.java)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "setPreferredService failed (non-critical)", e)
|
||||
}
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
"clearPayload" -> {
|
||||
NfcHceService.ndefPayload = null
|
||||
|
||||
// Release preferred service routing
|
||||
try {
|
||||
val adapter = NfcAdapter.getDefaultAdapter(this)
|
||||
if (adapter != null) {
|
||||
val cardEmulation = CardEmulation.getInstance(adapter)
|
||||
cardEmulation.unsetPreferredService(this)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "unsetPreferredService failed (non-critical)", e)
|
||||
}
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package me.elcaju
|
||||
|
||||
import android.nfc.cardemulation.HostApduService
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Host Card Emulation service that makes the phone act as an NFC Forum Type 4 tag.
|
||||
* Another phone in reader mode can tap and read the NDEF message we serve.
|
||||
*
|
||||
* The payload is set from Flutter via MainActivity's MethodChannel.
|
||||
*/
|
||||
class NfcHceService : HostApduService() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NfcHceService"
|
||||
|
||||
// Shared payload - set by Flutter via MethodChannel
|
||||
@Volatile
|
||||
var ndefPayload: ByteArray? = null
|
||||
|
||||
private val OK = byteArrayOf(0x90.toByte(), 0x00.toByte())
|
||||
private val NOT_FOUND = byteArrayOf(0x6A.toByte(), 0x82.toByte())
|
||||
|
||||
// Capability Container (CC) file - fixed structure
|
||||
// MLe=0xFF (255), MLc=0xFF (255), max NDEF=0x70FF (28671)
|
||||
// Compatible with Numo
|
||||
private val CC_FILE = byteArrayOf(
|
||||
0x00, 0x0F, // CC length (15)
|
||||
0x20, // Mapping version 2.0
|
||||
0x00, 0xFF.toByte(), // MLe: max read 255 bytes
|
||||
0x00, 0xFF.toByte(), // MLc: max write 255 bytes
|
||||
0x04, 0x06, // NDEF File Control TLV
|
||||
0xE1.toByte(), 0x04, // NDEF file ID
|
||||
0x70, 0xFF.toByte(), // Max NDEF size: 28671 bytes
|
||||
0x00, // Read access: open
|
||||
0xFF.toByte() // Write access: denied
|
||||
)
|
||||
}
|
||||
|
||||
private var selectedFile: String = "none"
|
||||
private var ndefFileCache: ByteArray? = null
|
||||
|
||||
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
|
||||
if (commandApdu.size < 4) return NOT_FOUND
|
||||
|
||||
val ins = commandApdu[1]
|
||||
|
||||
// SELECT command
|
||||
if (ins == 0xA4.toByte()) {
|
||||
return handleSelect(commandApdu)
|
||||
}
|
||||
|
||||
// READ BINARY command
|
||||
if (ins == 0xB0.toByte()) {
|
||||
return handleRead(commandApdu)
|
||||
}
|
||||
|
||||
return NOT_FOUND
|
||||
}
|
||||
|
||||
private fun handleSelect(apdu: ByteArray): ByteArray {
|
||||
// Select ElCaju proprietary AID (F04543414A5500)
|
||||
// Avoids conflicts with Xiaomi Mi Share / Samsung Beam
|
||||
if (apdu.size >= 12 && apdu[5] == 0xF0.toByte() && apdu[6] == 0x45.toByte()) {
|
||||
selectedFile = "app"
|
||||
Log.d(TAG, "Selected ElCaju Application (proprietary AID)")
|
||||
return OK
|
||||
}
|
||||
|
||||
// Select NDEF Application (AID: D2760000850101)
|
||||
if (apdu.size >= 12 && apdu[5] == 0xD2.toByte() && apdu[6] == 0x76.toByte()) {
|
||||
selectedFile = "app"
|
||||
Log.d(TAG, "Selected NDEF Application")
|
||||
return OK
|
||||
}
|
||||
|
||||
// Select by file ID
|
||||
if (apdu.size >= 7) {
|
||||
val fileId = ((apdu[5].toInt() and 0xFF) shl 8) or (apdu[6].toInt() and 0xFF)
|
||||
when (fileId) {
|
||||
0xE103 -> {
|
||||
selectedFile = "cc"
|
||||
Log.d(TAG, "Selected CC file")
|
||||
return OK
|
||||
}
|
||||
0xE104 -> {
|
||||
selectedFile = "ndef"
|
||||
// Cache the NDEF file when selected
|
||||
val payload = ndefPayload
|
||||
if (payload != null) {
|
||||
val file = ByteArray(2 + payload.size)
|
||||
file[0] = (payload.size shr 8).toByte()
|
||||
file[1] = (payload.size and 0xFF).toByte()
|
||||
System.arraycopy(payload, 0, file, 2, payload.size)
|
||||
ndefFileCache = file
|
||||
Log.d(TAG, "Selected NDEF file (${payload.size} bytes payload)")
|
||||
} else {
|
||||
ndefFileCache = null
|
||||
Log.w(TAG, "Selected NDEF file but no payload set")
|
||||
}
|
||||
return OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NOT_FOUND
|
||||
}
|
||||
|
||||
private fun handleRead(apdu: ByteArray): ByteArray {
|
||||
if (apdu.size < 5) return NOT_FOUND
|
||||
|
||||
val offset = ((apdu[2].toInt() and 0xFF) shl 8) or (apdu[3].toInt() and 0xFF)
|
||||
// Le=0x00 means 256 bytes in short APDU encoding
|
||||
var length = apdu[4].toInt() and 0xFF
|
||||
if (length == 0) length = 256
|
||||
|
||||
val data = when (selectedFile) {
|
||||
"cc" -> CC_FILE
|
||||
"ndef" -> ndefFileCache ?: return NOT_FOUND
|
||||
else -> return NOT_FOUND
|
||||
}
|
||||
|
||||
if (offset >= data.size) {
|
||||
Log.w(TAG, "Read offset $offset beyond data size ${data.size}")
|
||||
return NOT_FOUND
|
||||
}
|
||||
|
||||
val end = minOf(offset + length, data.size)
|
||||
val response = data.copyOfRange(offset, end)
|
||||
Log.d(TAG, "Read $selectedFile: offset=$offset len=$length returned=${response.size} bytes")
|
||||
return response + OK
|
||||
}
|
||||
|
||||
override fun onDeactivated(reason: Int) {
|
||||
Log.d(TAG, "Deactivated (reason=$reason)")
|
||||
selectedFile = "none"
|
||||
ndefFileCache = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="hce_description">ElCaju NFC</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,12 @@
|
||||
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/hce_description"
|
||||
android:requireDeviceUnlock="false">
|
||||
<aid-group
|
||||
android:category="other"
|
||||
android:description="@string/hce_description">
|
||||
<!-- ElCaju proprietary AID (avoids Xiaomi/Samsung NFC service conflicts) -->
|
||||
<aid-filter android:name="F04543414A5500" />
|
||||
<!-- NDEF Tag Application AID (NFC Forum Type 4) -->
|
||||
<aid-filter android:name="D2760000850101" />
|
||||
</aid-group>
|
||||
</host-apdu-service>
|
||||
@@ -0,0 +1,3 @@
|
||||
rust_input: crate::api
|
||||
rust_root: rust/
|
||||
dart_output: lib/src/rust
|
||||
@@ -2,4 +2,3 @@ arb-dir: lib/l10n
|
||||
template-arb-file: app_es.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: L10n
|
||||
synthetic-package: false
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:nfc_manager/nfc_manager.dart';
|
||||
import 'package:nfc_manager/nfc_manager_android.dart';
|
||||
import 'package:ndef_record/ndef_record.dart';
|
||||
|
||||
/// NFC availability state for UI rendering.
|
||||
enum NfcState {
|
||||
/// Device does not have NFC hardware.
|
||||
unsupported,
|
||||
|
||||
/// Device has NFC but it is disabled in settings.
|
||||
disabled,
|
||||
|
||||
/// NFC is supported and enabled.
|
||||
enabled,
|
||||
}
|
||||
|
||||
/// Service for reading and writing Cashu tokens via NFC.
|
||||
///
|
||||
/// Writes tokens as NDEF Text Records for maximum compatibility
|
||||
/// with cashu.me and Numo. Reads both Text and URI records.
|
||||
class NfcService {
|
||||
static const _hceChannel = MethodChannel('me.elcaju/nfc_hce');
|
||||
|
||||
// ─── HCE (phone-to-phone) ───
|
||||
|
||||
/// Start emulating an NFC tag with the given text payload.
|
||||
/// The phone will appear as an NFC tag to other devices.
|
||||
static Future<void> startEmulating(String text) async {
|
||||
final ndefMessage = _buildNdefTextMessage(text);
|
||||
await _hceChannel.invokeMethod('setPayload', {'payload': ndefMessage});
|
||||
}
|
||||
|
||||
/// Stop emulating an NFC tag.
|
||||
static Future<void> stopEmulating() async {
|
||||
await _hceChannel.invokeMethod('clearPayload');
|
||||
}
|
||||
|
||||
/// Build an NDEF message with a Text Record.
|
||||
/// Uses Short Record (SR) for payloads ≤ 255 bytes,
|
||||
/// Long Record for > 255 bytes (compatible with Numo).
|
||||
static Uint8List _buildNdefTextMessage(String text) {
|
||||
final textBytes = Uint8List.fromList(text.codeUnits);
|
||||
final languageCode = Uint8List.fromList('en'.codeUnits);
|
||||
|
||||
// NDEF Text Record payload: [status byte][lang code][text]
|
||||
final recordPayload = Uint8List(1 + languageCode.length + textBytes.length);
|
||||
recordPayload[0] = languageCode.length;
|
||||
recordPayload.setRange(1, 1 + languageCode.length, languageCode);
|
||||
recordPayload.setRange(1 + languageCode.length, recordPayload.length, textBytes);
|
||||
|
||||
final payloadLength = recordPayload.length;
|
||||
final isShortRecord = payloadLength <= 255;
|
||||
|
||||
if (isShortRecord) {
|
||||
// Short Record: flags(1) + typeLen(1) + payloadLen(1) + type(1) + payload
|
||||
final record = Uint8List(4 + payloadLength);
|
||||
record[0] = 0xD1; // MB|ME|SR|TNF=well-known
|
||||
record[1] = 1; // type length
|
||||
record[2] = payloadLength;
|
||||
record[3] = 0x54; // 'T'
|
||||
record.setRange(4, 4 + payloadLength, recordPayload);
|
||||
return record;
|
||||
} else {
|
||||
// Long Record: flags(1) + typeLen(1) + payloadLen(4) + type(1) + payload
|
||||
final record = Uint8List(7 + payloadLength);
|
||||
record[0] = 0xC1; // MB|ME|TNF=well-known (no SR flag)
|
||||
record[1] = 1; // type length
|
||||
record[2] = (payloadLength >> 24) & 0xFF;
|
||||
record[3] = (payloadLength >> 16) & 0xFF;
|
||||
record[4] = (payloadLength >> 8) & 0xFF;
|
||||
record[5] = payloadLength & 0xFF;
|
||||
record[6] = 0x54; // 'T'
|
||||
record.setRange(7, 7 + payloadLength, recordPayload);
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tag read/write ───
|
||||
|
||||
/// Check NFC state on the device.
|
||||
static Future<NfcState> checkState() async {
|
||||
if (!Platform.isAndroid) return NfcState.unsupported;
|
||||
try {
|
||||
final availability = await NfcManager.instance.checkAvailability();
|
||||
return switch (availability) {
|
||||
NfcAvailability.enabled => NfcState.enabled,
|
||||
NfcAvailability.disabled => NfcState.disabled,
|
||||
NfcAvailability.unsupported => NfcState.unsupported,
|
||||
};
|
||||
} catch (_) {
|
||||
return NfcState.unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a Cashu token to an NFC tag as NDEF Text Record.
|
||||
static Future<void> startWrite({
|
||||
required String token,
|
||||
required void Function() onSuccess,
|
||||
required void Function(String error) onError,
|
||||
}) async {
|
||||
try {
|
||||
await NfcManager.instance.startSession(
|
||||
pollingOptions: {NfcPollingOption.iso14443},
|
||||
onDiscovered: (NfcTag tag) async {
|
||||
final ndef = NdefAndroid.from(tag);
|
||||
if (ndef == null || !ndef.isWritable) {
|
||||
onError('Tag is not writable');
|
||||
await NfcManager.instance.stopSession();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build NDEF Text Record: [status byte][language code][text]
|
||||
final textBytes = Uint8List.fromList(token.codeUnits);
|
||||
final languageCode = Uint8List.fromList('en'.codeUnits);
|
||||
final payload = Uint8List(1 + languageCode.length + textBytes.length);
|
||||
payload[0] = languageCode.length; // status byte (UTF-8, no length)
|
||||
payload.setRange(1, 1 + languageCode.length, languageCode);
|
||||
payload.setRange(1 + languageCode.length, payload.length, textBytes);
|
||||
|
||||
// Check size
|
||||
if (payload.length + 7 > ndef.maxSize) {
|
||||
onError('Token too large for this NFC tag '
|
||||
'(${payload.length + 7}B > ${ndef.maxSize}B)');
|
||||
await NfcManager.instance.stopSession();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final message = NdefMessage(records: [
|
||||
NdefRecord(
|
||||
typeNameFormat: TypeNameFormat.wellKnown,
|
||||
type: Uint8List.fromList([0x54]), // 'T' = Text Record
|
||||
identifier: Uint8List(0),
|
||||
payload: payload,
|
||||
),
|
||||
]);
|
||||
await ndef.writeNdefMessage(message);
|
||||
onSuccess();
|
||||
await NfcManager.instance.stopSession();
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
await NfcManager.instance.stopSession();
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop any active NFC session.
|
||||
static Future<void> stopWrite() async {
|
||||
await NfcManager.instance.stopSession();
|
||||
}
|
||||
|
||||
/// Start reading NFC tags for Cashu tokens.
|
||||
/// Tries IsoDep APDU first (phone-to-phone HCE, bypasses manufacturer
|
||||
/// NFC services), falls back to NDEF (physical tags).
|
||||
static Future<void> startRead({
|
||||
required void Function(String token) onTokenRead,
|
||||
required void Function(String error) onError,
|
||||
}) async {
|
||||
try {
|
||||
await NfcManager.instance.startSession(
|
||||
pollingOptions: {NfcPollingOption.iso14443},
|
||||
onDiscovered: (NfcTag tag) async {
|
||||
try {
|
||||
final diagnostics = <String>[];
|
||||
|
||||
// 1. Try IsoDep first (HCE phone-to-phone)
|
||||
final isoDep = IsoDepAndroid.from(tag);
|
||||
if (isoDep != null) {
|
||||
final (token, isoInfo) = await _readViaIsoDep(isoDep);
|
||||
if (token != null) {
|
||||
onTokenRead(token);
|
||||
await NfcManager.instance.stopSession();
|
||||
return;
|
||||
}
|
||||
diagnostics.add('IsoDep: $isoInfo');
|
||||
} else {
|
||||
diagnostics.add('IsoDep: not available');
|
||||
}
|
||||
|
||||
// 2. Fallback: NDEF (physical tags)
|
||||
final ndef = NdefAndroid.from(tag);
|
||||
if (ndef != null) {
|
||||
final message = ndef.cachedNdefMessage ?? await ndef.getNdefMessage();
|
||||
if (message != null) {
|
||||
final token = _extractToken(message);
|
||||
if (token != null) {
|
||||
onTokenRead(token);
|
||||
await NfcManager.instance.stopSession();
|
||||
return;
|
||||
}
|
||||
diagnostics.add('NDEF: ${message.records.length} records, no Cashu token');
|
||||
} else {
|
||||
diagnostics.add('NDEF: no message');
|
||||
}
|
||||
} else {
|
||||
diagnostics.add('NDEF: not available');
|
||||
}
|
||||
|
||||
onError('No Cashu token found [${diagnostics.join('; ')}]');
|
||||
await NfcManager.instance.stopSession();
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
await NfcManager.instance.stopSession();
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
onError(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Read NDEF from HCE via IsoDep APDU commands (like Numo).
|
||||
/// Bypasses manufacturer NFC services (Xiaomi, Samsung, etc).
|
||||
/// Returns (token, diagnosticInfo) tuple.
|
||||
static Future<(String?, String)> _readViaIsoDep(IsoDepAndroid isoDep) async {
|
||||
String _hex(Uint8List bytes) =>
|
||||
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||
|
||||
try {
|
||||
// Set longer timeout (5s) to avoid TagLostException
|
||||
await isoDep.setTimeout(5000);
|
||||
|
||||
// Try ElCaju proprietary AID first (avoids Xiaomi/Samsung conflicts)
|
||||
final selectElCaju = await isoDep.transceive(Uint8List.fromList([
|
||||
0x00, 0xA4, 0x04, 0x00, 0x07,
|
||||
0xF0, 0x45, 0x43, 0x41, 0x4A, 0x55, 0x00,
|
||||
0x00,
|
||||
]));
|
||||
final bool elCajuSelected = _isOk(selectElCaju);
|
||||
|
||||
// Fallback: standard NDEF Application AID
|
||||
if (!elCajuSelected) {
|
||||
final selectApp = await isoDep.transceive(Uint8List.fromList([
|
||||
0x00, 0xA4, 0x04, 0x00, 0x07,
|
||||
0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01,
|
||||
0x00,
|
||||
]));
|
||||
if (!_isOk(selectApp)) {
|
||||
return (null, 'SELECT APP failed (both AIDs): elcaju=${_hex(selectElCaju)}, ndef=${_hex(selectApp)}');
|
||||
}
|
||||
}
|
||||
|
||||
// SELECT NDEF file (E104)
|
||||
final selectNdef = await isoDep.transceive(Uint8List.fromList([
|
||||
0x00, 0xA4, 0x00, 0x0C, 0x02,
|
||||
0xE1, 0x04,
|
||||
]));
|
||||
if (!_isOk(selectNdef)) {
|
||||
return (null, 'SELECT NDEF failed: ${_hex(selectNdef)}');
|
||||
}
|
||||
|
||||
// READ NLEN (first 2 bytes)
|
||||
final nlenResponse = await isoDep.transceive(Uint8List.fromList([
|
||||
0x00, 0xB0, 0x00, 0x00, 0x02,
|
||||
]));
|
||||
if (nlenResponse.length < 4 || !_isOk(nlenResponse)) {
|
||||
return (null, 'READ NLEN failed: ${_hex(nlenResponse)}');
|
||||
}
|
||||
|
||||
final ndefLen = (nlenResponse[0] << 8) | nlenResponse[1];
|
||||
if (ndefLen == 0) {
|
||||
return (null, 'NLEN=0 (no payload set on emitter)');
|
||||
}
|
||||
|
||||
// READ NDEF message in chunks (max 255 bytes per read)
|
||||
final ndefBytes = BytesBuilder();
|
||||
var offset = 2; // skip NLEN
|
||||
var remaining = ndefLen;
|
||||
|
||||
while (remaining > 0) {
|
||||
final chunkSize = remaining > 255 ? 255 : remaining;
|
||||
final readCmd = Uint8List.fromList([
|
||||
0x00, 0xB0,
|
||||
(offset >> 8) & 0xFF,
|
||||
offset & 0xFF,
|
||||
chunkSize,
|
||||
]);
|
||||
final chunk = await isoDep.transceive(readCmd);
|
||||
if (chunk.length < 2 || !_isOk(chunk)) {
|
||||
return (null, 'READ chunk at offset=$offset failed: ${_hex(chunk)}');
|
||||
}
|
||||
|
||||
// Response = data + SW1 SW2 (last 2 bytes are status)
|
||||
final bytesRead = chunk.length - 2;
|
||||
if (bytesRead == 0) {
|
||||
return (null, 'READ chunk at offset=$offset returned no data');
|
||||
}
|
||||
ndefBytes.add(chunk.sublist(0, bytesRead));
|
||||
offset += bytesRead;
|
||||
remaining -= bytesRead;
|
||||
}
|
||||
|
||||
// Parse NDEF message from raw bytes
|
||||
final raw = ndefBytes.toBytes();
|
||||
if (raw.isEmpty) {
|
||||
return (null, 'read ${ndefLen}B but empty after parsing');
|
||||
}
|
||||
|
||||
final token = _parseNdefAndExtractToken(Uint8List.fromList(raw));
|
||||
if (token != null) {
|
||||
return (token, 'OK');
|
||||
}
|
||||
return (null, 'NDEF parsed (${raw.length}B) but no Cashu token, hex: ${_hex(Uint8List.fromList(raw.length > 40 ? raw.sublist(0, 40) : raw))}...');
|
||||
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('TagLostException')) {
|
||||
return (null, 'connection lost (hold phones together longer)');
|
||||
}
|
||||
return (null, 'error: $msg');
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if APDU response ends with 90 00 (OK).
|
||||
static bool _isOk(Uint8List response) {
|
||||
if (response.length < 2) return false;
|
||||
return response[response.length - 2] == 0x90 &&
|
||||
response[response.length - 1] == 0x00;
|
||||
}
|
||||
|
||||
/// Parse raw NDEF bytes and extract Cashu token.
|
||||
static String? _parseNdefAndExtractToken(Uint8List raw) {
|
||||
if (raw.isEmpty) return null;
|
||||
|
||||
var pos = 0;
|
||||
while (pos < raw.length) {
|
||||
if (pos + 3 > raw.length) break;
|
||||
|
||||
final flags = raw[pos];
|
||||
final tnf = flags & 0x07;
|
||||
final isShortRecord = (flags & 0x10) != 0;
|
||||
final hasIdLength = (flags & 0x08) != 0;
|
||||
|
||||
pos++;
|
||||
final typeLength = raw[pos]; pos++;
|
||||
|
||||
int payloadLength;
|
||||
if (isShortRecord) {
|
||||
payloadLength = raw[pos]; pos++;
|
||||
} else {
|
||||
if (pos + 4 > raw.length) break;
|
||||
payloadLength = (raw[pos] << 24) | (raw[pos+1] << 16) |
|
||||
(raw[pos+2] << 8) | raw[pos+3];
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
int idLength = 0;
|
||||
if (hasIdLength) {
|
||||
idLength = raw[pos]; pos++;
|
||||
}
|
||||
|
||||
// Type
|
||||
if (pos + typeLength > raw.length) break;
|
||||
final type = raw.sublist(pos, pos + typeLength); pos += typeLength;
|
||||
|
||||
// ID (skip)
|
||||
pos += idLength;
|
||||
|
||||
// Payload
|
||||
if (pos + payloadLength > raw.length) break;
|
||||
final payload = raw.sublist(pos, pos + payloadLength);
|
||||
pos += payloadLength;
|
||||
|
||||
// Check: Text Record (TNF=0x01, type='T')
|
||||
if (tnf == 0x01 && typeLength == 1 && type[0] == 0x54) {
|
||||
if (payload.isNotEmpty) {
|
||||
final langLen = payload[0] & 0x3F;
|
||||
if (payload.length > 1 + langLen) {
|
||||
final text = String.fromCharCodes(payload.sublist(1 + langLen));
|
||||
if (_isCashuToken(text)) return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check: URI Record (TNF=0x01, type='U')
|
||||
if (tnf == 0x01 && typeLength == 1 && type[0] == 0x55) {
|
||||
if (payload.isNotEmpty) {
|
||||
const prefixes = ['', 'http://www.', 'https://www.', 'http://', 'https://'];
|
||||
final prefixIdx = payload[0];
|
||||
final prefix = prefixIdx < prefixes.length ? prefixes[prefixIdx] : '';
|
||||
final rest = String.fromCharCodes(payload.sublist(1));
|
||||
final uri = '$prefix$rest';
|
||||
final token = _extractTokenFromUri(uri);
|
||||
if (token != null) return token;
|
||||
}
|
||||
}
|
||||
|
||||
// If ME (Message End) flag set, stop
|
||||
if ((flags & 0x40) != 0) break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Stop any active NFC read session.
|
||||
static Future<void> stopRead() async {
|
||||
await NfcManager.instance.stopSession();
|
||||
}
|
||||
|
||||
/// Extract a Cashu token from an NDEF message.
|
||||
static String? _extractToken(NdefMessage message) {
|
||||
for (final record in message.records) {
|
||||
// Text Record
|
||||
if (record.typeNameFormat == TypeNameFormat.wellKnown &&
|
||||
record.type.length == 1 &&
|
||||
record.type[0] == 0x54) {
|
||||
final text = _decodeTextRecord(record);
|
||||
if (text != null && _isCashuToken(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// URI Record
|
||||
if (record.typeNameFormat == TypeNameFormat.wellKnown &&
|
||||
record.type.length == 1 &&
|
||||
record.type[0] == 0x55) {
|
||||
final uri = _decodeUriRecord(record);
|
||||
if (uri != null) {
|
||||
final token = _extractTokenFromUri(uri);
|
||||
if (token != null) return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Decode NDEF Text Record payload.
|
||||
static String? _decodeTextRecord(NdefRecord record) {
|
||||
if (record.payload.isEmpty) return null;
|
||||
final languageCodeLength = record.payload[0] & 0x3F;
|
||||
if (record.payload.length <= 1 + languageCodeLength) return null;
|
||||
return String.fromCharCodes(
|
||||
record.payload.sublist(1 + languageCodeLength));
|
||||
}
|
||||
|
||||
/// Decode NDEF URI Record payload.
|
||||
static String? _decodeUriRecord(NdefRecord record) {
|
||||
if (record.payload.isEmpty) return null;
|
||||
const prefixes = [
|
||||
'', // 0x00
|
||||
'http://www.', // 0x01
|
||||
'https://www.', // 0x02
|
||||
'http://', // 0x03
|
||||
'https://', // 0x04
|
||||
];
|
||||
final prefixIndex = record.payload[0];
|
||||
final prefix = prefixIndex < prefixes.length ? prefixes[prefixIndex] : '';
|
||||
final rest = String.fromCharCodes(record.payload.sublist(1));
|
||||
return '$prefix$rest';
|
||||
}
|
||||
|
||||
/// Extract token from a URL like https://wallet.com/#token=cashuA...
|
||||
static String? _extractTokenFromUri(String uri) {
|
||||
try {
|
||||
final parsed = Uri.parse(uri);
|
||||
final fragmentParams = Uri.splitQueryString(parsed.fragment);
|
||||
final fragmentToken = fragmentParams['token'];
|
||||
if (fragmentToken != null && _isCashuToken(fragmentToken)) {
|
||||
return fragmentToken;
|
||||
}
|
||||
final tokenParam = parsed.queryParameters['token'];
|
||||
if (tokenParam != null && _isCashuToken(tokenParam)) {
|
||||
return tokenParam;
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if a string looks like a Cashu token.
|
||||
static bool _isCashuToken(String text) {
|
||||
final lower = text.toLowerCase().trim();
|
||||
return lower.startsWith('cashua') ||
|
||||
lower.startsWith('cashub') ||
|
||||
lower.startsWith('creqa');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// Parser para detectar el tipo de dato entrante (QR, clipboard, etc.)
|
||||
// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests
|
||||
// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests, peanut emoji
|
||||
|
||||
import 'peanut_codec.dart';
|
||||
|
||||
/// Modo de escaneo
|
||||
enum ScanMode {
|
||||
@@ -57,6 +59,15 @@ class IncomingDataParser {
|
||||
/// Detecta el tipo de dato y extrae información relevante
|
||||
static ParsedData parse(String data) {
|
||||
final trimmed = data.trim();
|
||||
|
||||
// Peanut-encoded token (🥜 + variation selectors)
|
||||
if (PeanutCodec.isPeanut(trimmed)) {
|
||||
final decoded = PeanutCodec.decode(trimmed);
|
||||
if (decoded != null) {
|
||||
return parse(decoded);
|
||||
}
|
||||
}
|
||||
|
||||
final lower = trimmed.toLowerCase();
|
||||
|
||||
// Token Cashu (cashuA... o cashuB...)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/// Peanut encoding/decoding for Cashu tokens.
|
||||
///
|
||||
/// Encodes Cashu tokens into invisible Unicode Variation Selectors
|
||||
/// appended to a 🥜 emoji, compatible with cashu.me's implementation.
|
||||
///
|
||||
/// Each character of the base64 token is mapped to a Variation Selector:
|
||||
/// - charCode 0-15 → VS1-VS16 (U+FE00 to U+FE0F)
|
||||
/// - charCode 16-255 → VS17-VS256 (U+E0100 to U+E01EF)
|
||||
class PeanutCodec {
|
||||
static const String _peanutEmoji = '🥜';
|
||||
|
||||
/// Encodes a Cashu token string into peanut format (🥜 + variation selectors).
|
||||
static String encode(String token) {
|
||||
final buffer = StringBuffer(_peanutEmoji);
|
||||
|
||||
for (int i = 0; i < token.length; i++) {
|
||||
final byte = token.codeUnitAt(i);
|
||||
|
||||
if (byte < 16) {
|
||||
// VS1-VS16: U+FE00 to U+FE0F
|
||||
buffer.writeCharCode(0xFE00 + byte);
|
||||
} else if (byte < 256) {
|
||||
// VS17-VS256: U+E0100 to U+E01EF
|
||||
buffer.writeCharCode(0xE0100 + (byte - 16));
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Decodes a peanut-encoded string back to the original Cashu token.
|
||||
/// Returns null if the input is not valid peanut format.
|
||||
static String? decode(String peanut) {
|
||||
final trimmed = peanut.trimLeft();
|
||||
if (!trimmed.startsWith(_peanutEmoji)) return null;
|
||||
|
||||
final buffer = StringBuffer();
|
||||
final runes = trimmed.runes.toList();
|
||||
|
||||
// Skip the first rune (🥜 emoji)
|
||||
for (int i = 1; i < runes.length; i++) {
|
||||
final codePoint = runes[i];
|
||||
|
||||
if (codePoint >= 0xFE00 && codePoint <= 0xFE0F) {
|
||||
// VS1-VS16 → byte 0-15
|
||||
buffer.writeCharCode(codePoint - 0xFE00);
|
||||
} else if (codePoint >= 0xE0100 && codePoint <= 0xE01EF) {
|
||||
// VS17-VS256 → byte 16-255
|
||||
buffer.writeCharCode(codePoint - 0xE0100 + 16);
|
||||
}
|
||||
// Skip any other characters (whitespace, etc.)
|
||||
}
|
||||
|
||||
final result = buffer.toString();
|
||||
return result.isNotEmpty ? result : null;
|
||||
}
|
||||
|
||||
/// Returns true if the string starts with the 🥜 emoji.
|
||||
static bool isPeanut(String data) {
|
||||
return data.trimLeft().startsWith(_peanutEmoji);
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.",
|
||||
"tokenCopiedToClipboard": "Token in Zwischenablage kopiert",
|
||||
"copyAsEmoji": "Als Emoji kopieren",
|
||||
"emojiCopiedToClipboard": "Token als Emoji kopiert 🥜",
|
||||
"peanutDecodeError": "Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.",
|
||||
|
||||
"nfcWrite": "Auf NFC-Tag schreiben",
|
||||
"nfcRead": "NFC-Tag lesen",
|
||||
"nfcHoldNear": "Gerät an NFC-Tag halten...",
|
||||
"nfcWriteSuccess": "Token auf NFC-Tag geschrieben",
|
||||
"nfcWriteError": "NFC-Schreibfehler: {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "NFC-Lesefehler: {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC ist deaktiviert. Aktiviere es in den Einstellungen.",
|
||||
"nfcUnsupported": "Dieses Gerät unterstützt kein NFC",
|
||||
|
||||
"amountToDeposit": "Einzuzahlender Betrag:",
|
||||
"descriptionOptional": "Beschreibung (optional):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "Schließen",
|
||||
"aboutDescription": "Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von La Chispa.",
|
||||
"aboutDescription": "Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von LaChispa.",
|
||||
"couldNotOpenLink": "Link konnte nicht geöffnet werden",
|
||||
|
||||
"deleteWalletQuestion": "Wallet löschen?",
|
||||
|
||||
+23
-1
@@ -112,6 +112,28 @@
|
||||
"tokenCashuAnimatedQr": "Cashu Token (animated QR - {fragments} UR fragments)",
|
||||
"keepTokenWarning": "Keep this token until the recipient claims it. If you lose it, you will lose the funds.",
|
||||
"tokenCopiedToClipboard": "Token copied to clipboard",
|
||||
"copyAsEmoji": "Copy as emoji",
|
||||
"emojiCopiedToClipboard": "Token copied as emoji 🥜",
|
||||
"peanutDecodeError": "Could not decode emoji token. It may be corrupted.",
|
||||
|
||||
"nfcWrite": "Write to NFC tag",
|
||||
"nfcRead": "Read NFC tag",
|
||||
"nfcHoldNear": "Hold device near NFC tag...",
|
||||
"nfcWriteSuccess": "Token written to NFC tag",
|
||||
"nfcWriteError": "NFC write error: {error}",
|
||||
"@nfcWriteError": {
|
||||
"placeholders": {
|
||||
"error": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"nfcReadError": "NFC read error: {error}",
|
||||
"@nfcReadError": {
|
||||
"placeholders": {
|
||||
"error": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"nfcDisabled": "NFC is disabled. Enable it in Settings.",
|
||||
"nfcUnsupported": "This device does not support NFC",
|
||||
|
||||
"amountToDeposit": "Amount to deposit:",
|
||||
"descriptionOptional": "Description (optional):",
|
||||
@@ -234,7 +256,7 @@
|
||||
"selectLanguage": "Select language",
|
||||
"languageChanged": "Language changed to {language}",
|
||||
"close": "Close",
|
||||
"aboutDescription": "A Cashu wallet with Cuban DNA for the entire world. Brother of La Chispa.",
|
||||
"aboutDescription": "A Cashu wallet with Cuban DNA for the entire world. Brother of LaChispa.",
|
||||
"couldNotOpenLink": "Could not open link",
|
||||
|
||||
"deleteWalletQuestion": "Delete wallet?",
|
||||
|
||||
+23
-1
@@ -137,6 +137,28 @@
|
||||
},
|
||||
"keepTokenWarning": "Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.",
|
||||
"tokenCopiedToClipboard": "Token copiado al portapapeles",
|
||||
"copyAsEmoji": "Copiar como emoji",
|
||||
"emojiCopiedToClipboard": "Token copiado como emoji 🥜",
|
||||
"peanutDecodeError": "No se pudo decodificar el token emoji. Puede estar corrupto.",
|
||||
|
||||
"nfcWrite": "Escribir en tag NFC",
|
||||
"nfcRead": "Leer tag NFC",
|
||||
"nfcHoldNear": "Acerca el dispositivo al tag NFC...",
|
||||
"nfcWriteSuccess": "Token escrito en tag NFC",
|
||||
"nfcWriteError": "Error NFC al escribir: {error}",
|
||||
"@nfcWriteError": {
|
||||
"placeholders": {
|
||||
"error": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"nfcReadError": "Error NFC al leer: {error}",
|
||||
"@nfcReadError": {
|
||||
"placeholders": {
|
||||
"error": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"nfcDisabled": "NFC está desactivado. Actívalo en Ajustes.",
|
||||
"nfcUnsupported": "Este dispositivo no soporta NFC",
|
||||
|
||||
"amountToDeposit": "Monto a depositar:",
|
||||
"descriptionOptional": "Descripción (opcional):",
|
||||
@@ -296,7 +318,7 @@
|
||||
}
|
||||
},
|
||||
"close": "Cerrar",
|
||||
"aboutDescription": "Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.",
|
||||
"aboutDescription": "Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.",
|
||||
"couldNotOpenLink": "No se pudo abrir el enlace",
|
||||
|
||||
"deleteWalletQuestion": "¿Borrar wallet?",
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "Conservez ce token jusqu'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.",
|
||||
"tokenCopiedToClipboard": "Token copié dans le presse-papiers",
|
||||
"copyAsEmoji": "Copier en emoji",
|
||||
"emojiCopiedToClipboard": "Token copié en emoji 🥜",
|
||||
"peanutDecodeError": "Impossible de décoder le token emoji. Il est peut-être corrompu.",
|
||||
|
||||
"nfcWrite": "Écrire sur tag NFC",
|
||||
"nfcRead": "Lire tag NFC",
|
||||
"nfcHoldNear": "Approchez l'appareil du tag NFC...",
|
||||
"nfcWriteSuccess": "Token écrit sur le tag NFC",
|
||||
"nfcWriteError": "Erreur NFC écriture : {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "Erreur NFC lecture : {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC est désactivé. Activez-le dans les Paramètres.",
|
||||
"nfcUnsupported": "Cet appareil ne prend pas en charge le NFC",
|
||||
|
||||
"amountToDeposit": "Montant à déposer :",
|
||||
"descriptionOptional": "Description (optionnelle) :",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "Fermer",
|
||||
"aboutDescription": "Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de La Chispa.",
|
||||
"aboutDescription": "Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de LaChispa.",
|
||||
"couldNotOpenLink": "Impossible d'ouvrir le lien",
|
||||
|
||||
"deleteWalletQuestion": "Supprimer le portefeuille ?",
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.",
|
||||
"tokenCopiedToClipboard": "Token copiato negli appunti",
|
||||
"copyAsEmoji": "Copia come emoji",
|
||||
"emojiCopiedToClipboard": "Token copiato come emoji 🥜",
|
||||
"peanutDecodeError": "Impossibile decodificare il token emoji. Potrebbe essere corrotto.",
|
||||
|
||||
"nfcWrite": "Scrivi su tag NFC",
|
||||
"nfcRead": "Leggi tag NFC",
|
||||
"nfcHoldNear": "Avvicina il dispositivo al tag NFC...",
|
||||
"nfcWriteSuccess": "Token scritto sul tag NFC",
|
||||
"nfcWriteError": "Errore NFC scrittura: {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "Errore NFC lettura: {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC è disattivato. Attivalo nelle Impostazioni.",
|
||||
"nfcUnsupported": "Questo dispositivo non supporta NFC",
|
||||
|
||||
"amountToDeposit": "Importo da depositare:",
|
||||
"descriptionOptional": "Descrizione (opzionale):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "Chiudi",
|
||||
"aboutDescription": "Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di La Chispa.",
|
||||
"aboutDescription": "Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di LaChispa.",
|
||||
"couldNotOpenLink": "Impossibile aprire il link",
|
||||
|
||||
"deleteWalletQuestion": "Eliminare portafoglio?",
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "受取人が請求するまでこのトークンを保管してください。紛失すると資金を失います。",
|
||||
"tokenCopiedToClipboard": "トークンをクリップボードにコピーしました",
|
||||
"copyAsEmoji": "絵文字としてコピー",
|
||||
"emojiCopiedToClipboard": "トークンを絵文字としてコピーしました 🥜",
|
||||
"peanutDecodeError": "絵文字トークンをデコードできませんでした。破損している可能性があります。",
|
||||
|
||||
"nfcWrite": "NFCタグに書き込む",
|
||||
"nfcRead": "NFCタグを読み取る",
|
||||
"nfcHoldNear": "デバイスをNFCタグに近づけてください...",
|
||||
"nfcWriteSuccess": "トークンをNFCタグに書き込みました",
|
||||
"nfcWriteError": "NFC書き込みエラー: {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "NFC読み取りエラー: {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFCが無効です。設定で有効にしてください。",
|
||||
"nfcUnsupported": "このデバイスはNFCに対応していません",
|
||||
|
||||
"amountToDeposit": "入金額:",
|
||||
"descriptionOptional": "説明(任意):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "閉じる",
|
||||
"aboutDescription": "キューバのDNAを持つ世界のためのCashuウォレット。La Chispaの兄弟。",
|
||||
"aboutDescription": "キューバのDNAを持つ世界のためのCashuウォレット。LaChispaの兄弟。",
|
||||
"couldNotOpenLink": "リンクを開けませんでした",
|
||||
|
||||
"deleteWalletQuestion": "ウォレットを削除しますか?",
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "수신자가 청구할 때까지 이 토큰을 보관하세요. 분실하면 자금을 잃게 됩니다.",
|
||||
"tokenCopiedToClipboard": "토큰이 클립보드에 복사되었습니다",
|
||||
"copyAsEmoji": "이모지로 복사",
|
||||
"emojiCopiedToClipboard": "토큰이 이모지로 복사되었습니다 🥜",
|
||||
"peanutDecodeError": "이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.",
|
||||
|
||||
"nfcWrite": "NFC 태그에 쓰기",
|
||||
"nfcRead": "NFC 태그 읽기",
|
||||
"nfcHoldNear": "기기를 NFC 태그에 가까이 대세요...",
|
||||
"nfcWriteSuccess": "토큰이 NFC 태그에 기록되었습니다",
|
||||
"nfcWriteError": "NFC 쓰기 오류: {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "NFC 읽기 오류: {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC가 비활성화되어 있습니다. 설정에서 활성화하세요.",
|
||||
"nfcUnsupported": "이 기기는 NFC를 지원하지 않습니다",
|
||||
|
||||
"amountToDeposit": "입금할 금액:",
|
||||
"descriptionOptional": "설명 (선택사항):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "닫기",
|
||||
"aboutDescription": "전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. La Chispa의 형제.",
|
||||
"aboutDescription": "전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. LaChispa의 형제.",
|
||||
"couldNotOpenLink": "링크를 열 수 없음",
|
||||
|
||||
"deleteWalletQuestion": "지갑을 삭제하시겠습니까?",
|
||||
|
||||
+113
-24
@@ -71,7 +71,8 @@ import 'app_localizations_zh.dart';
|
||||
/// be consistent with the languages listed in the L10n.supportedLocales
|
||||
/// property.
|
||||
abstract class L10n {
|
||||
L10n(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
L10n(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
@@ -91,12 +92,13 @@ abstract class L10n {
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
@@ -110,7 +112,7 @@ abstract class L10n {
|
||||
Locale('pt'),
|
||||
Locale('ru'),
|
||||
Locale('sw'),
|
||||
Locale('zh')
|
||||
Locale('zh'),
|
||||
];
|
||||
|
||||
/// No description provided for @appName.
|
||||
@@ -707,6 +709,72 @@ abstract class L10n {
|
||||
/// **'Token copiado al portapapeles'**
|
||||
String get tokenCopiedToClipboard;
|
||||
|
||||
/// No description provided for @copyAsEmoji.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Copiar como emoji'**
|
||||
String get copyAsEmoji;
|
||||
|
||||
/// No description provided for @emojiCopiedToClipboard.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Token copiado como emoji 🥜'**
|
||||
String get emojiCopiedToClipboard;
|
||||
|
||||
/// No description provided for @peanutDecodeError.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'No se pudo decodificar el token emoji. Puede estar corrupto.'**
|
||||
String get peanutDecodeError;
|
||||
|
||||
/// No description provided for @nfcWrite.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Escribir en tag NFC'**
|
||||
String get nfcWrite;
|
||||
|
||||
/// No description provided for @nfcRead.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Leer tag NFC'**
|
||||
String get nfcRead;
|
||||
|
||||
/// No description provided for @nfcHoldNear.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Acerca el dispositivo al tag NFC...'**
|
||||
String get nfcHoldNear;
|
||||
|
||||
/// No description provided for @nfcWriteSuccess.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Token escrito en tag NFC'**
|
||||
String get nfcWriteSuccess;
|
||||
|
||||
/// No description provided for @nfcWriteError.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Error NFC al escribir: {error}'**
|
||||
String nfcWriteError(String error);
|
||||
|
||||
/// No description provided for @nfcReadError.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Error NFC al leer: {error}'**
|
||||
String nfcReadError(String error);
|
||||
|
||||
/// No description provided for @nfcDisabled.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'NFC está desactivado. Actívalo en Ajustes.'**
|
||||
String get nfcDisabled;
|
||||
|
||||
/// No description provided for @nfcUnsupported.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Este dispositivo no soporta NFC'**
|
||||
String get nfcUnsupported;
|
||||
|
||||
/// No description provided for @amountToDeposit.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
@@ -1376,7 +1444,7 @@ abstract class L10n {
|
||||
/// No description provided for @aboutDescription.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.'**
|
||||
/// **'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.'**
|
||||
String get aboutDescription;
|
||||
|
||||
/// No description provided for @couldNotOpenLink.
|
||||
@@ -2157,34 +2225,55 @@ class _L10nDelegate extends LocalizationsDelegate<L10n> {
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => <String>['de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'pt', 'ru', 'sw', 'zh'].contains(locale.languageCode);
|
||||
bool isSupported(Locale locale) => <String>[
|
||||
'de',
|
||||
'en',
|
||||
'es',
|
||||
'fr',
|
||||
'it',
|
||||
'ja',
|
||||
'ko',
|
||||
'pt',
|
||||
'ru',
|
||||
'sw',
|
||||
'zh',
|
||||
].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_L10nDelegate old) => false;
|
||||
}
|
||||
|
||||
L10n lookupL10n(Locale locale) {
|
||||
|
||||
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'de': return L10nDe();
|
||||
case 'en': return L10nEn();
|
||||
case 'es': return L10nEs();
|
||||
case 'fr': return L10nFr();
|
||||
case 'it': return L10nIt();
|
||||
case 'ja': return L10nJa();
|
||||
case 'ko': return L10nKo();
|
||||
case 'pt': return L10nPt();
|
||||
case 'ru': return L10nRu();
|
||||
case 'sw': return L10nSw();
|
||||
case 'zh': return L10nZh();
|
||||
case 'de':
|
||||
return L10nDe();
|
||||
case 'en':
|
||||
return L10nEn();
|
||||
case 'es':
|
||||
return L10nEs();
|
||||
case 'fr':
|
||||
return L10nFr();
|
||||
case 'it':
|
||||
return L10nIt();
|
||||
case 'ja':
|
||||
return L10nJa();
|
||||
case 'ko':
|
||||
return L10nKo();
|
||||
case 'pt':
|
||||
return L10nPt();
|
||||
case 'ru':
|
||||
return L10nRu();
|
||||
case 'sw':
|
||||
return L10nSw();
|
||||
case 'zh':
|
||||
return L10nZh();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'L10n.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.'
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -60,7 +60,8 @@ class L10nDe extends L10n {
|
||||
String get generatingSeed => 'Sichere Generierung deiner Seed-Phrase';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'Eine 12-Wort Seed-Phrase wird generiert.\nBewahre sie an einem sicheren Ort auf.';
|
||||
String get createWalletDescription =>
|
||||
'Eine 12-Wort Seed-Phrase wird generiert.\nBewahre sie an einem sicheren Ort auf.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Wallet generieren';
|
||||
@@ -69,10 +70,12 @@ class L10nDe extends L10n {
|
||||
String get walletCreated => 'Wallet erstellt!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Deine Wallet ist bereit. Wir empfehlen, jetzt ein Backup deiner Seed-Phrase zu erstellen.';
|
||||
String get walletCreatedDescription =>
|
||||
'Deine Wallet ist bereit. Wir empfehlen, jetzt ein Backup deiner Seed-Phrase zu erstellen.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Ohne Backup verlierst du den Zugang zu deinen Mitteln, wenn du das Gerät verlierst.';
|
||||
String get backupWarning =>
|
||||
'Ohne Backup verlierst du den Zugang zu deinen Mitteln, wenn du das Gerät verlierst.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Jetzt sichern';
|
||||
@@ -87,13 +90,15 @@ class L10nDe extends L10n {
|
||||
String get seedPhraseTitle => 'Deine Seed-Phrase';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Speichere diese 12 Wörter in der richtigen Reihenfolge. Sie sind der einzige Weg, deine Wallet wiederherzustellen.';
|
||||
String get seedPhraseDescription =>
|
||||
'Speichere diese 12 Wörter in der richtigen Reihenfolge. Sie sind der einzige Weg, deine Wallet wiederherzustellen.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Seed-Phrase anzeigen';
|
||||
|
||||
@override
|
||||
String get tapToReveal => 'Tippe auf den Button, um\ndeine Seed-Phrase anzuzeigen';
|
||||
String get tapToReveal =>
|
||||
'Tippe auf den Button, um\ndeine Seed-Phrase anzuzeigen';
|
||||
|
||||
@override
|
||||
String get copyToClipboard => 'In Zwischenablage kopieren';
|
||||
@@ -105,7 +110,8 @@ class L10nDe extends L10n {
|
||||
String get neverShareSeed => 'Teile deine Seed-Phrase niemals mit anderen.';
|
||||
|
||||
@override
|
||||
String get confirmBackup => 'Ich habe meine Seed-Phrase an einem sicheren Ort gespeichert';
|
||||
String get confirmBackup =>
|
||||
'Ich habe meine Seed-Phrase an einem sicheren Ort gespeichert';
|
||||
|
||||
@override
|
||||
String get continue_ => 'Weiter';
|
||||
@@ -117,7 +123,8 @@ class L10nDe extends L10n {
|
||||
String get enterSeedPhrase => 'Gib deine Seed-Phrase ein';
|
||||
|
||||
@override
|
||||
String get enterSeedDescription => 'Gib die 12 oder 24 Wörter durch Leerzeichen getrennt ein.';
|
||||
String get enterSeedDescription =>
|
||||
'Gib die 12 oder 24 Wörter durch Leerzeichen getrennt ein.';
|
||||
|
||||
@override
|
||||
String get seedPlaceholder => 'wort1 wort2 wort3 ...';
|
||||
@@ -131,7 +138,8 @@ class L10nDe extends L10n {
|
||||
String get needWords => '(du brauchst 12 oder 24)';
|
||||
|
||||
@override
|
||||
String get restoreScanningMint => 'Mint wird nach vorhandenen Token durchsucht...';
|
||||
String get restoreScanningMint =>
|
||||
'Mint wird nach vorhandenen Token durchsucht...';
|
||||
|
||||
@override
|
||||
String restoreError(String error) {
|
||||
@@ -310,11 +318,51 @@ class L10nDe extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.';
|
||||
String get keepTokenWarning =>
|
||||
'Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Token in Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Als Emoji kopieren';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Token als Emoji kopiert 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Auf NFC-Tag schreiben';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'NFC-Tag lesen';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Gerät an NFC-Tag halten...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Token auf NFC-Tag geschrieben';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'NFC-Schreibfehler: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'NFC-Lesefehler: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled =>
|
||||
'NFC ist deaktiviert. Aktiviere es in den Einstellungen.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'Dieses Gerät unterstützt kein NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Einzuzahlender Betrag:';
|
||||
|
||||
@@ -450,7 +498,8 @@ class L10nDe extends L10n {
|
||||
String get noPendingTransactions => 'Keine ausstehenden Transaktionen';
|
||||
|
||||
@override
|
||||
String get allTransactionsCompleted => 'Alle deine Transaktionen sind abgeschlossen';
|
||||
String get allTransactionsCompleted =>
|
||||
'Alle deine Transaktionen sind abgeschlossen';
|
||||
|
||||
@override
|
||||
String get noEcashTransactions => 'Keine Ecash Transaktionen';
|
||||
@@ -462,7 +511,8 @@ class L10nDe extends L10n {
|
||||
String get noLightningTransactions => 'Keine Lightning Transaktionen';
|
||||
|
||||
@override
|
||||
String get depositOrWithdrawLightning => 'Zahle ein oder hebe ab via Lightning';
|
||||
String get depositOrWithdrawLightning =>
|
||||
'Zahle ein oder hebe ab via Lightning';
|
||||
|
||||
@override
|
||||
String get pendingStatus => 'Ausstehend';
|
||||
@@ -665,7 +715,8 @@ class L10nDe extends L10n {
|
||||
String get close => 'Schließen';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'Link konnte nicht geöffnet werden';
|
||||
@@ -677,7 +728,8 @@ class L10nDe extends L10n {
|
||||
String get actionIrreversible => 'Diese Aktion ist unwiderruflich';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'Alle Daten werden gelöscht, einschließlich deiner Seed-Phrase und Tokens. Stelle sicher, dass du ein Backup hast.';
|
||||
String get deleteWalletWarning =>
|
||||
'Alle Daten werden gelöscht, einschließlich deiner Seed-Phrase und Tokens. Stelle sicher, dass du ein Backup hast.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Gib \"LÖSCHEN\" zur Bestätigung ein:';
|
||||
@@ -694,19 +746,22 @@ class L10nDe extends L10n {
|
||||
String get recoverTokensTitle => 'Tokens wiederherstellen';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Mints scannen, um Tokens wiederherzustellen, die mit deiner Seed-Phrase verknüpft sind (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Mints scannen, um Tokens wiederherzustellen, die mit deiner Seed-Phrase verknüpft sind (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Meine aktuelle Seed-Phrase verwenden';
|
||||
|
||||
@override
|
||||
String get scanWithSavedWords => 'Mints mit den gespeicherten 12 Wörtern scannen';
|
||||
String get scanWithSavedWords =>
|
||||
'Mints mit den gespeicherten 12 Wörtern scannen';
|
||||
|
||||
@override
|
||||
String get useOtherSeedPhrase => 'Andere Seed-Phrase verwenden';
|
||||
|
||||
@override
|
||||
String get recoverFromOtherWords => 'Tokens von anderen 12 Wörtern wiederherstellen';
|
||||
String get recoverFromOtherWords =>
|
||||
'Tokens von anderen 12 Wörtern wiederherstellen';
|
||||
|
||||
@override
|
||||
String get mintsToScan => 'Zu scannende Mints:';
|
||||
@@ -720,7 +775,8 @@ class L10nDe extends L10n {
|
||||
String get specificMint => 'Ein bestimmter Mint';
|
||||
|
||||
@override
|
||||
String get enterMnemonicWords => 'Gib die 12 Wörter durch Leerzeichen getrennt ein...';
|
||||
String get enterMnemonicWords =>
|
||||
'Gib die 12 Wörter durch Leerzeichen getrennt ein...';
|
||||
|
||||
@override
|
||||
String get scanMints => 'Mints scannen';
|
||||
@@ -740,7 +796,8 @@ class L10nDe extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanCompleteNoTokens => 'Scan abgeschlossen. Keine neuen Tokens gefunden.';
|
||||
String get scanCompleteNoTokens =>
|
||||
'Scan abgeschlossen. Keine neuen Tokens gefunden.';
|
||||
|
||||
@override
|
||||
String mintsWithError(int count) {
|
||||
@@ -763,7 +820,8 @@ class L10nDe extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'Keine Tokens mit diesem Mnemonic verknüpft gefunden.';
|
||||
String get noTokensForMnemonic =>
|
||||
'Keine Tokens mit diesem Mnemonic verknüpft gefunden.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'Keine verbundenen Mints';
|
||||
@@ -843,7 +901,8 @@ class L10nDe extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Mint löschen';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'Wenn du Guthaben auf diesem Mint hast, geht es verloren. Bist du sicher?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'Wenn du Guthaben auf diesem Mint hast, geht es verloren. Bist du sicher?';
|
||||
|
||||
@override
|
||||
String get delete => 'Löschen';
|
||||
@@ -896,19 +955,24 @@ class L10nDe extends L10n {
|
||||
String get tokenSavedForLater => 'Token zum späteren Einlösen gespeichert';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'Keine Verbindung. Token zum späteren Einlösen gespeichert.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'Keine Verbindung. Token zum späteren Einlösen gespeichert.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'Dieser Token stammt von einem unbekannten Mint. Verbinde dich mit dem Internet, um ihn hinzuzufügen und den Token einzulösen.';
|
||||
String get unknownMintOffline =>
|
||||
'Dieser Token stammt von einem unbekannten Mint. Verbinde dich mit dem Internet, um ihn hinzuzufügen und den Token einzulösen.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Keine Verbindung zum Mint. Versuche es später erneut.';
|
||||
String get noConnectionTryLater =>
|
||||
'Keine Verbindung zum Mint. Versuche es später erneut.';
|
||||
|
||||
@override
|
||||
String get saveTokenError => 'Fehler beim Speichern des Tokens. Bitte erneut versuchen.';
|
||||
String get saveTokenError =>
|
||||
'Fehler beim Speichern des Tokens. Bitte erneut versuchen.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Limit für ausstehende Tokens erreicht (max 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Limit für ausstehende Tokens erreicht (max 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'Zu empfangen';
|
||||
@@ -985,10 +1049,12 @@ class L10nDe extends L10n {
|
||||
String get unrecognizedQrCode => 'Nicht erkannter QR-Code';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Scanne einen Cashu Token (cashuA... oder cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Scanne einen Cashu Token (cashuA... oder cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Scanne eine Lightning Rechnung (lnbc...)';
|
||||
String get scanLightningInvoiceHint =>
|
||||
'Scanne eine Lightning Rechnung (lnbc...)';
|
||||
|
||||
@override
|
||||
String get addMintQuestion => 'Diesen Mint hinzufügen?';
|
||||
@@ -997,7 +1063,8 @@ class L10nDe extends L10n {
|
||||
String get cameraPermissionDenied => 'Kamera-Berechtigung verweigert';
|
||||
|
||||
@override
|
||||
String get paymentRequestNotSupported => 'Zahlungsanfragen werden noch nicht unterstützt';
|
||||
String get paymentRequestNotSupported =>
|
||||
'Zahlungsanfragen werden noch nicht unterstützt';
|
||||
|
||||
@override
|
||||
String get p2pkTitle => 'P2PK-Schlüssel';
|
||||
@@ -1006,10 +1073,12 @@ class L10nDe extends L10n {
|
||||
String get p2pkSettingsDescription => 'Gesperrtes ecash empfangen';
|
||||
|
||||
@override
|
||||
String get p2pkExperimental => 'P2PK ist experimentell. Mit Vorsicht verwenden.';
|
||||
String get p2pkExperimental =>
|
||||
'P2PK ist experimentell. Mit Vorsicht verwenden.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'Du hast einen ausstehenden P2PK-Versand. Gehe zum Verlauf und aktualisiere, nachdem der Empfänger den Token eingelöst hat.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'Du hast einen ausstehenden P2PK-Versand. Gehe zum Verlauf und aktualisiere, nachdem der Empfänger den Token eingelöst hat.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Experimentell';
|
||||
@@ -1066,7 +1135,8 @@ class L10nDe extends L10n {
|
||||
String get p2pkLockedToOther => 'Für anderen Schlüssel gesperrt';
|
||||
|
||||
@override
|
||||
String get p2pkCannotUnlock => 'Du hast nicht den Schlüssel, um diesen Token zu entsperren';
|
||||
String get p2pkCannotUnlock =>
|
||||
'Du hast nicht den Schlüssel, um diesen Token zu entsperren';
|
||||
|
||||
@override
|
||||
String get p2pkEnterPrivateKey => 'Privaten Schlüssel eingeben (nsec)';
|
||||
@@ -1075,13 +1145,15 @@ class L10nDe extends L10n {
|
||||
String get p2pkDeleteTitle => 'Schlüssel löschen';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => 'Diesen Schlüssel löschen? Du kannst keine Token mehr empfangen, die daran gesperrt sind.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'Diesen Schlüssel löschen? Du kannst keine Token mehr empfangen, die daran gesperrt sind.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK erfordert Verbindung zum Mint';
|
||||
|
||||
@override
|
||||
String get p2pkErrorMaxKeysReached => 'Maximale Anzahl importierter Schlüssel erreicht (10)';
|
||||
String get p2pkErrorMaxKeysReached =>
|
||||
'Maximale Anzahl importierter Schlüssel erreicht (10)';
|
||||
|
||||
@override
|
||||
String get p2pkErrorInvalidNsec => 'Ungültiger nsec';
|
||||
@@ -1093,5 +1165,6 @@ class L10nDe extends L10n {
|
||||
String get p2pkErrorKeyNotFound => 'Schlüssel nicht gefunden';
|
||||
|
||||
@override
|
||||
String get p2pkErrorCannotDeletePrimary => 'Primärschlüssel kann nicht gelöscht werden';
|
||||
String get p2pkErrorCannotDeletePrimary =>
|
||||
'Primärschlüssel kann nicht gelöscht werden';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -60,7 +60,8 @@ class L10nEn extends L10n {
|
||||
String get generatingSeed => 'Generating your seed phrase securely';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'A 12-word seed phrase will be generated.\nStore it in a safe place.';
|
||||
String get createWalletDescription =>
|
||||
'A 12-word seed phrase will be generated.\nStore it in a safe place.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Generate wallet';
|
||||
@@ -69,10 +70,12 @@ class L10nEn extends L10n {
|
||||
String get walletCreated => 'Wallet created!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Your wallet is ready. We recommend backing up your seed phrase now.';
|
||||
String get walletCreatedDescription =>
|
||||
'Your wallet is ready. We recommend backing up your seed phrase now.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Without backup, you will lose access to your funds if you lose the device.';
|
||||
String get backupWarning =>
|
||||
'Without backup, you will lose access to your funds if you lose the device.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Backup now';
|
||||
@@ -87,7 +90,8 @@ class L10nEn extends L10n {
|
||||
String get seedPhraseTitle => 'Your seed phrase';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Save these 12 words in order. They are the only way to recover your wallet.';
|
||||
String get seedPhraseDescription =>
|
||||
'Save these 12 words in order. They are the only way to recover your wallet.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Reveal seed phrase';
|
||||
@@ -117,7 +121,8 @@ class L10nEn extends L10n {
|
||||
String get enterSeedPhrase => 'Enter your seed phrase';
|
||||
|
||||
@override
|
||||
String get enterSeedDescription => 'Type the 12 or 24 words separated by spaces.';
|
||||
String get enterSeedDescription =>
|
||||
'Type the 12 or 24 words separated by spaces.';
|
||||
|
||||
@override
|
||||
String get seedPlaceholder => 'word1 word2 word3 ...';
|
||||
@@ -310,11 +315,50 @@ class L10nEn extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Keep this token until the recipient claims it. If you lose it, you will lose the funds.';
|
||||
String get keepTokenWarning =>
|
||||
'Keep this token until the recipient claims it. If you lose it, you will lose the funds.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Token copied to clipboard';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Copy as emoji';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Token copied as emoji 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'Could not decode emoji token. It may be corrupted.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Write to NFC tag';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'Read NFC tag';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Hold device near NFC tag...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Token written to NFC tag';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'NFC write error: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'NFC read error: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC is disabled. Enable it in Settings.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'This device does not support NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Amount to deposit:';
|
||||
|
||||
@@ -665,7 +709,8 @@ class L10nEn extends L10n {
|
||||
String get close => 'Close';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'A Cashu wallet with Cuban DNA for the entire world. Brother of La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'A Cashu wallet with Cuban DNA for the entire world. Brother of LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'Could not open link';
|
||||
@@ -677,7 +722,8 @@ class L10nEn extends L10n {
|
||||
String get actionIrreversible => 'This action is irreversible';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'All data will be deleted including your seed phrase and tokens. Make sure you have a backup.';
|
||||
String get deleteWalletWarning =>
|
||||
'All data will be deleted including your seed phrase and tokens. Make sure you have a backup.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Type \"DELETE\" to confirm:';
|
||||
@@ -694,7 +740,8 @@ class L10nEn extends L10n {
|
||||
String get recoverTokensTitle => 'Recover tokens';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Scan mints to recover tokens associated with your seed phrase (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Scan mints to recover tokens associated with your seed phrase (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Use my current seed phrase';
|
||||
@@ -763,7 +810,8 @@ class L10nEn extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'No tokens found associated with that mnemonic.';
|
||||
String get noTokensForMnemonic =>
|
||||
'No tokens found associated with that mnemonic.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'No connected mints';
|
||||
@@ -843,7 +891,8 @@ class L10nEn extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Delete mint';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'If you have balance in this mint, it will be lost. Are you sure?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'If you have balance in this mint, it will be lost. Are you sure?';
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
@@ -896,10 +945,12 @@ class L10nEn extends L10n {
|
||||
String get tokenSavedForLater => 'Token saved to claim later';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'No connection. Token saved to claim later.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'No connection. Token saved to claim later.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'This token is from an unknown mint. Connect to the internet to add it and claim the token.';
|
||||
String get unknownMintOffline =>
|
||||
'This token is from an unknown mint. Connect to the internet to add it and claim the token.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'No connection to mint. Try again later.';
|
||||
@@ -908,7 +959,8 @@ class L10nEn extends L10n {
|
||||
String get saveTokenError => 'Error saving token. Please try again.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Pending tokens limit reached (max 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Pending tokens limit reached (max 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'To receive';
|
||||
@@ -985,7 +1037,8 @@ class L10nEn extends L10n {
|
||||
String get unrecognizedQrCode => 'Unrecognized QR code';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Scan a Cashu token (cashuA... or cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Scan a Cashu token (cashuA... or cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Scan a Lightning invoice (lnbc...)';
|
||||
@@ -997,7 +1050,8 @@ class L10nEn extends L10n {
|
||||
String get cameraPermissionDenied => 'Camera permission denied';
|
||||
|
||||
@override
|
||||
String get paymentRequestNotSupported => 'Payment requests are not yet supported';
|
||||
String get paymentRequestNotSupported =>
|
||||
'Payment requests are not yet supported';
|
||||
|
||||
@override
|
||||
String get p2pkTitle => 'P2PK Keys';
|
||||
@@ -1009,7 +1063,8 @@ class L10nEn extends L10n {
|
||||
String get p2pkExperimental => 'P2PK is experimental. Use with caution.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'You have a pending P2PK send. Go to history and refresh after the recipient claims the token.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'You have a pending P2PK send. Go to history and refresh after the recipient claims the token.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Experimental';
|
||||
@@ -1075,7 +1130,8 @@ class L10nEn extends L10n {
|
||||
String get p2pkDeleteTitle => 'Delete key';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => 'Delete this key? You won\'t be able to receive tokens locked to it.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'Delete this key? You won\'t be able to receive tokens locked to it.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK requires connection to the mint';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -60,7 +60,8 @@ class L10nEs extends L10n {
|
||||
String get generatingSeed => 'Generando tu frase semilla de forma segura';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'Se generará una frase semilla de 12 palabras.\nGuárdala en un lugar seguro.';
|
||||
String get createWalletDescription =>
|
||||
'Se generará una frase semilla de 12 palabras.\nGuárdala en un lugar seguro.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Generar wallet';
|
||||
@@ -69,10 +70,12 @@ class L10nEs extends L10n {
|
||||
String get walletCreated => '¡Wallet creada!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Tu wallet está lista. Te recomendamos hacer backup de tu frase semilla ahora.';
|
||||
String get walletCreatedDescription =>
|
||||
'Tu wallet está lista. Te recomendamos hacer backup de tu frase semilla ahora.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Sin backup, perderás acceso a tus fondos si pierdes el dispositivo.';
|
||||
String get backupWarning =>
|
||||
'Sin backup, perderás acceso a tus fondos si pierdes el dispositivo.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Hacer backup ahora';
|
||||
@@ -87,7 +90,8 @@ class L10nEs extends L10n {
|
||||
String get seedPhraseTitle => 'Tu frase semilla';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Guarda estas 12 palabras en orden. Son la única forma de recuperar tu wallet.';
|
||||
String get seedPhraseDescription =>
|
||||
'Guarda estas 12 palabras en orden. Son la única forma de recuperar tu wallet.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Revelar frase semilla';
|
||||
@@ -117,7 +121,8 @@ class L10nEs extends L10n {
|
||||
String get enterSeedPhrase => 'Ingresa tu frase semilla';
|
||||
|
||||
@override
|
||||
String get enterSeedDescription => 'Escribe las 12 o 24 palabras separadas por espacios.';
|
||||
String get enterSeedDescription =>
|
||||
'Escribe las 12 o 24 palabras separadas por espacios.';
|
||||
|
||||
@override
|
||||
String get seedPlaceholder => 'palabra1 palabra2 palabra3 ...';
|
||||
@@ -310,11 +315,50 @@ class L10nEs extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.';
|
||||
String get keepTokenWarning =>
|
||||
'Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Token copiado al portapapeles';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Copiar como emoji';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'No se pudo decodificar el token emoji. Puede estar corrupto.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Escribir en tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'Leer tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Acerca el dispositivo al tag NFC...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Token escrito en tag NFC';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'Error NFC al escribir: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'Error NFC al leer: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC está desactivado. Actívalo en Ajustes.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'Este dispositivo no soporta NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Monto a depositar:';
|
||||
|
||||
@@ -450,7 +494,8 @@ class L10nEs extends L10n {
|
||||
String get noPendingTransactions => 'Sin transacciones pendientes';
|
||||
|
||||
@override
|
||||
String get allTransactionsCompleted => 'Todas tus transacciones están completadas';
|
||||
String get allTransactionsCompleted =>
|
||||
'Todas tus transacciones están completadas';
|
||||
|
||||
@override
|
||||
String get noEcashTransactions => 'Sin transacciones Ecash';
|
||||
@@ -665,7 +710,8 @@ class L10nEs extends L10n {
|
||||
String get close => 'Cerrar';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'No se pudo abrir el enlace';
|
||||
@@ -677,7 +723,8 @@ class L10nEs extends L10n {
|
||||
String get actionIrreversible => 'Esta acción es irreversible';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'Se eliminarán todos los datos incluyendo tu seed phrase y tokens. Asegúrate de tener un backup.';
|
||||
String get deleteWalletWarning =>
|
||||
'Se eliminarán todos los datos incluyendo tu seed phrase y tokens. Asegúrate de tener un backup.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Escribe \"BORRAR\" para confirmar:';
|
||||
@@ -694,13 +741,15 @@ class L10nEs extends L10n {
|
||||
String get recoverTokensTitle => 'Recuperar tokens';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Escanea los mints para recuperar tokens asociados a tu seed phrase (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Escanea los mints para recuperar tokens asociados a tu seed phrase (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Usar mi seed phrase actual';
|
||||
|
||||
@override
|
||||
String get scanWithSavedWords => 'Escanear mints con las 12 palabras guardadas';
|
||||
String get scanWithSavedWords =>
|
||||
'Escanear mints con las 12 palabras guardadas';
|
||||
|
||||
@override
|
||||
String get useOtherSeedPhrase => 'Usar otra seed phrase';
|
||||
@@ -720,7 +769,8 @@ class L10nEs extends L10n {
|
||||
String get specificMint => 'Un mint específico';
|
||||
|
||||
@override
|
||||
String get enterMnemonicWords => 'Ingresa las 12 palabras separadas por espacios...';
|
||||
String get enterMnemonicWords =>
|
||||
'Ingresa las 12 palabras separadas por espacios...';
|
||||
|
||||
@override
|
||||
String get scanMints => 'Escanear mints';
|
||||
@@ -740,7 +790,8 @@ class L10nEs extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanCompleteNoTokens => 'Escaneo completado. No se encontraron tokens nuevos.';
|
||||
String get scanCompleteNoTokens =>
|
||||
'Escaneo completado. No se encontraron tokens nuevos.';
|
||||
|
||||
@override
|
||||
String mintsWithError(int count) {
|
||||
@@ -763,7 +814,8 @@ class L10nEs extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'No se encontraron tokens asociados a ese mnemonic.';
|
||||
String get noTokensForMnemonic =>
|
||||
'No se encontraron tokens asociados a ese mnemonic.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'No hay mints conectados';
|
||||
@@ -843,7 +895,8 @@ class L10nEs extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Eliminar mint';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'Si tienes balance en este mint, se perderá. ¿Estás seguro?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'Si tienes balance en este mint, se perderá. ¿Estás seguro?';
|
||||
|
||||
@override
|
||||
String get delete => 'Eliminar';
|
||||
@@ -896,10 +949,12 @@ class L10nEs extends L10n {
|
||||
String get tokenSavedForLater => 'Token guardado para reclamar después';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'Sin conexión. Token guardado para reclamar después.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'Sin conexión. Token guardado para reclamar después.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'Este token es de un mint desconocido. Conéctate a internet para agregarlo y reclamar el token.';
|
||||
String get unknownMintOffline =>
|
||||
'Este token es de un mint desconocido. Conéctate a internet para agregarlo y reclamar el token.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Sin conexión al mint. Intenta más tarde.';
|
||||
@@ -908,7 +963,8 @@ class L10nEs extends L10n {
|
||||
String get saveTokenError => 'Error al guardar el token. Intenta de nuevo.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Límite de tokens pendientes alcanzado (max 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Límite de tokens pendientes alcanzado (max 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'Para recibir';
|
||||
@@ -985,10 +1041,12 @@ class L10nEs extends L10n {
|
||||
String get unrecognizedQrCode => 'Código QR no reconocido';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Escanea un token Cashu (cashuA... o cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Escanea un token Cashu (cashuA... o cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Escanea un invoice Lightning (lnbc...)';
|
||||
String get scanLightningInvoiceHint =>
|
||||
'Escanea un invoice Lightning (lnbc...)';
|
||||
|
||||
@override
|
||||
String get addMintQuestion => '¿Agregar este mint?';
|
||||
@@ -997,7 +1055,8 @@ class L10nEs extends L10n {
|
||||
String get cameraPermissionDenied => 'Permiso de cámara denegado';
|
||||
|
||||
@override
|
||||
String get paymentRequestNotSupported => 'Los payment requests aún no están soportados';
|
||||
String get paymentRequestNotSupported =>
|
||||
'Los payment requests aún no están soportados';
|
||||
|
||||
@override
|
||||
String get p2pkTitle => 'Claves P2PK';
|
||||
@@ -1009,7 +1068,8 @@ class L10nEs extends L10n {
|
||||
String get p2pkExperimental => 'P2PK es experimental. Úsala con precaución.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'Tienes un envío P2PK pendiente. Ve al historial y presiona actualizar después de que el destinatario reclame el token.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'Tienes un envío P2PK pendiente. Ve al historial y presiona actualizar después de que el destinatario reclame el token.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Experimental';
|
||||
@@ -1066,7 +1126,8 @@ class L10nEs extends L10n {
|
||||
String get p2pkLockedToOther => 'Bloqueado para otra clave';
|
||||
|
||||
@override
|
||||
String get p2pkCannotUnlock => 'No tienes la clave para desbloquear este token';
|
||||
String get p2pkCannotUnlock =>
|
||||
'No tienes la clave para desbloquear este token';
|
||||
|
||||
@override
|
||||
String get p2pkEnterPrivateKey => 'Ingresa la clave privada (nsec)';
|
||||
@@ -1075,13 +1136,15 @@ class L10nEs extends L10n {
|
||||
String get p2pkDeleteTitle => 'Eliminar clave';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => '¿Eliminar esta clave? No podrás recibir tokens bloqueados a ella.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'¿Eliminar esta clave? No podrás recibir tokens bloqueados a ella.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK requiere conexión al mint';
|
||||
|
||||
@override
|
||||
String get p2pkErrorMaxKeysReached => 'Máximo de claves importadas alcanzado (10)';
|
||||
String get p2pkErrorMaxKeysReached =>
|
||||
'Máximo de claves importadas alcanzado (10)';
|
||||
|
||||
@override
|
||||
String get p2pkErrorInvalidNsec => 'nsec inválido';
|
||||
@@ -1093,5 +1156,6 @@ class L10nEs extends L10n {
|
||||
String get p2pkErrorKeyNotFound => 'Clave no encontrada';
|
||||
|
||||
@override
|
||||
String get p2pkErrorCannotDeletePrimary => 'No se puede eliminar la clave principal';
|
||||
String get p2pkErrorCannotDeletePrimary =>
|
||||
'No se puede eliminar la clave principal';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -57,10 +57,12 @@ class L10nFr extends L10n {
|
||||
String get creatingWallet => 'Création de votre portefeuille...';
|
||||
|
||||
@override
|
||||
String get generatingSeed => 'Génération sécurisée de votre phrase de récupération';
|
||||
String get generatingSeed =>
|
||||
'Génération sécurisée de votre phrase de récupération';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'Une phrase de récupération de 12 mots sera générée.\nConservez-la dans un endroit sûr.';
|
||||
String get createWalletDescription =>
|
||||
'Une phrase de récupération de 12 mots sera générée.\nConservez-la dans un endroit sûr.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Générer le portefeuille';
|
||||
@@ -69,10 +71,12 @@ class L10nFr extends L10n {
|
||||
String get walletCreated => 'Portefeuille créé !';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Votre portefeuille est prêt. Nous vous recommandons de sauvegarder votre phrase de récupération maintenant.';
|
||||
String get walletCreatedDescription =>
|
||||
'Votre portefeuille est prêt. Nous vous recommandons de sauvegarder votre phrase de récupération maintenant.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Sans sauvegarde, vous perdrez l\'accès à vos fonds si vous perdez l\'appareil.';
|
||||
String get backupWarning =>
|
||||
'Sans sauvegarde, vous perdrez l\'accès à vos fonds si vous perdez l\'appareil.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Sauvegarder maintenant';
|
||||
@@ -87,13 +91,15 @@ class L10nFr extends L10n {
|
||||
String get seedPhraseTitle => 'Votre phrase de récupération';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Conservez ces 12 mots dans l\'ordre. C\'est le seul moyen de récupérer votre portefeuille.';
|
||||
String get seedPhraseDescription =>
|
||||
'Conservez ces 12 mots dans l\'ordre. C\'est le seul moyen de récupérer votre portefeuille.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Révéler la phrase de récupération';
|
||||
|
||||
@override
|
||||
String get tapToReveal => 'Appuyez sur le bouton pour révéler\nvotre phrase de récupération';
|
||||
String get tapToReveal =>
|
||||
'Appuyez sur le bouton pour révéler\nvotre phrase de récupération';
|
||||
|
||||
@override
|
||||
String get copyToClipboard => 'Copier dans le presse-papiers';
|
||||
@@ -102,10 +108,12 @@ class L10nFr extends L10n {
|
||||
String get seedCopied => 'Phrase copiée dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get neverShareSeed => 'Ne partagez jamais votre phrase de récupération avec personne.';
|
||||
String get neverShareSeed =>
|
||||
'Ne partagez jamais votre phrase de récupération avec personne.';
|
||||
|
||||
@override
|
||||
String get confirmBackup => 'J\'ai sauvegardé ma phrase de récupération dans un endroit sûr';
|
||||
String get confirmBackup =>
|
||||
'J\'ai sauvegardé ma phrase de récupération dans un endroit sûr';
|
||||
|
||||
@override
|
||||
String get continue_ => 'Continuer';
|
||||
@@ -117,7 +125,8 @@ class L10nFr extends L10n {
|
||||
String get enterSeedPhrase => 'Entrez votre phrase de récupération';
|
||||
|
||||
@override
|
||||
String get enterSeedDescription => 'Tapez les 12 ou 24 mots séparés par des espaces.';
|
||||
String get enterSeedDescription =>
|
||||
'Tapez les 12 ou 24 mots séparés par des espaces.';
|
||||
|
||||
@override
|
||||
String get seedPlaceholder => 'mot1 mot2 mot3 ...';
|
||||
@@ -310,11 +319,51 @@ class L10nFr extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Conservez ce token jusqu\'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.';
|
||||
String get keepTokenWarning =>
|
||||
'Conservez ce token jusqu\'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Token copié dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Copier en emoji';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Token copié en emoji 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'Impossible de décoder le token emoji. Il est peut-être corrompu.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Écrire sur tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'Lire tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Approchez l\'appareil du tag NFC...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Token écrit sur le tag NFC';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'Erreur NFC écriture : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'Erreur NFC lecture : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled =>
|
||||
'NFC est désactivé. Activez-le dans les Paramètres.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'Cet appareil ne prend pas en charge le NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Montant à déposer :';
|
||||
|
||||
@@ -364,7 +413,8 @@ class L10nFr extends L10n {
|
||||
String get description => 'Description :';
|
||||
|
||||
@override
|
||||
String get invoiceCopiedToClipboard => 'Facture copiée dans le presse-papiers';
|
||||
String get invoiceCopiedToClipboard =>
|
||||
'Facture copiée dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String deposited(String amount, String unit) {
|
||||
@@ -450,7 +500,8 @@ class L10nFr extends L10n {
|
||||
String get noPendingTransactions => 'Aucune transaction en attente';
|
||||
|
||||
@override
|
||||
String get allTransactionsCompleted => 'Toutes vos transactions sont terminées';
|
||||
String get allTransactionsCompleted =>
|
||||
'Toutes vos transactions sont terminées';
|
||||
|
||||
@override
|
||||
String get noEcashTransactions => 'Aucune transaction Ecash';
|
||||
@@ -567,7 +618,8 @@ class L10nFr extends L10n {
|
||||
String get recoverTokens => 'Récupérer les tokens';
|
||||
|
||||
@override
|
||||
String get scanMintsWithSeed => 'Scanner les mints avec la phrase de récupération';
|
||||
String get scanMintsWithSeed =>
|
||||
'Scanner les mints avec la phrase de récupération';
|
||||
|
||||
@override
|
||||
String get appearanceSection => 'LANGUE';
|
||||
@@ -665,7 +717,8 @@ class L10nFr extends L10n {
|
||||
String get close => 'Fermer';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'Impossible d\'ouvrir le lien';
|
||||
@@ -677,7 +730,8 @@ class L10nFr extends L10n {
|
||||
String get actionIrreversible => 'Cette action est irréversible';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'Toutes les données seront supprimées, y compris votre phrase de récupération et vos tokens. Assurez-vous d\'avoir une sauvegarde.';
|
||||
String get deleteWalletWarning =>
|
||||
'Toutes les données seront supprimées, y compris votre phrase de récupération et vos tokens. Assurez-vous d\'avoir une sauvegarde.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Tapez \"SUPPRIMER\" pour confirmer :';
|
||||
@@ -694,13 +748,16 @@ class L10nFr extends L10n {
|
||||
String get recoverTokensTitle => 'Récupérer les tokens';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Scanner les mints pour récupérer les tokens associés à votre phrase de récupération (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Scanner les mints pour récupérer les tokens associés à votre phrase de récupération (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Utiliser ma phrase de récupération actuelle';
|
||||
String get useCurrentSeedPhrase =>
|
||||
'Utiliser ma phrase de récupération actuelle';
|
||||
|
||||
@override
|
||||
String get scanWithSavedWords => 'Scanner les mints avec les 12 mots sauvegardés';
|
||||
String get scanWithSavedWords =>
|
||||
'Scanner les mints avec les 12 mots sauvegardés';
|
||||
|
||||
@override
|
||||
String get useOtherSeedPhrase => 'Utiliser une autre phrase de récupération';
|
||||
@@ -720,7 +777,8 @@ class L10nFr extends L10n {
|
||||
String get specificMint => 'Un mint spécifique';
|
||||
|
||||
@override
|
||||
String get enterMnemonicWords => 'Entrez les 12 mots séparés par des espaces...';
|
||||
String get enterMnemonicWords =>
|
||||
'Entrez les 12 mots séparés par des espaces...';
|
||||
|
||||
@override
|
||||
String get scanMints => 'Scanner les mints';
|
||||
@@ -740,7 +798,8 @@ class L10nFr extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanCompleteNoTokens => 'Scan terminé. Aucun nouveau token trouvé.';
|
||||
String get scanCompleteNoTokens =>
|
||||
'Scan terminé. Aucun nouveau token trouvé.';
|
||||
|
||||
@override
|
||||
String mintsWithError(int count) {
|
||||
@@ -763,7 +822,8 @@ class L10nFr extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'Aucun token trouvé associé à ce mnémonique.';
|
||||
String get noTokensForMnemonic =>
|
||||
'Aucun token trouvé associé à ce mnémonique.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'Aucun mint connecté';
|
||||
@@ -843,7 +903,8 @@ class L10nFr extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Supprimer le mint';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'Si vous avez un solde sur ce mint, il sera perdu. Êtes-vous sûr ?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'Si vous avez un solde sur ce mint, il sera perdu. Êtes-vous sûr ?';
|
||||
|
||||
@override
|
||||
String get delete => 'Supprimer';
|
||||
@@ -896,19 +957,24 @@ class L10nFr extends L10n {
|
||||
String get tokenSavedForLater => 'Token sauvegardé pour réclamer plus tard';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'Pas de connexion. Token sauvegardé pour réclamer plus tard.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'Pas de connexion. Token sauvegardé pour réclamer plus tard.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'Ce token provient d\'un mint inconnu. Connectez-vous à Internet pour l\'ajouter et réclamer le token.';
|
||||
String get unknownMintOffline =>
|
||||
'Ce token provient d\'un mint inconnu. Connectez-vous à Internet pour l\'ajouter et réclamer le token.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Pas de connexion au mint. Réessayez plus tard.';
|
||||
String get noConnectionTryLater =>
|
||||
'Pas de connexion au mint. Réessayez plus tard.';
|
||||
|
||||
@override
|
||||
String get saveTokenError => 'Erreur lors de la sauvegarde du token. Veuillez réessayer.';
|
||||
String get saveTokenError =>
|
||||
'Erreur lors de la sauvegarde du token. Veuillez réessayer.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Limite de tokens en attente atteinte (max 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Limite de tokens en attente atteinte (max 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'À recevoir';
|
||||
@@ -917,7 +983,8 @@ class L10nFr extends L10n {
|
||||
String get noPendingTokens => 'Aucun token en attente';
|
||||
|
||||
@override
|
||||
String get noPendingTokensHint => 'Sauvegardez des tokens pour les réclamer plus tard';
|
||||
String get noPendingTokensHint =>
|
||||
'Sauvegardez des tokens pour les réclamer plus tard';
|
||||
|
||||
@override
|
||||
String get pendingBadge => 'EN ATTENTE';
|
||||
@@ -976,19 +1043,23 @@ class L10nFr extends L10n {
|
||||
String get pointCameraAtQr => 'Pointez la caméra vers le code QR';
|
||||
|
||||
@override
|
||||
String get pointCameraAtCashuQr => 'Pointez la caméra vers le QR du token Cashu';
|
||||
String get pointCameraAtCashuQr =>
|
||||
'Pointez la caméra vers le QR du token Cashu';
|
||||
|
||||
@override
|
||||
String get pointCameraAtInvoiceQr => 'Pointez la caméra vers le QR de la facture';
|
||||
String get pointCameraAtInvoiceQr =>
|
||||
'Pointez la caméra vers le QR de la facture';
|
||||
|
||||
@override
|
||||
String get unrecognizedQrCode => 'Code QR non reconnu';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Scannez un token Cashu (cashuA... ou cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Scannez un token Cashu (cashuA... ou cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Scannez une facture Lightning (lnbc...)';
|
||||
String get scanLightningInvoiceHint =>
|
||||
'Scannez une facture Lightning (lnbc...)';
|
||||
|
||||
@override
|
||||
String get addMintQuestion => 'Ajouter ce mint ?';
|
||||
@@ -997,7 +1068,8 @@ class L10nFr extends L10n {
|
||||
String get cameraPermissionDenied => 'Permission de la caméra refusée';
|
||||
|
||||
@override
|
||||
String get paymentRequestNotSupported => 'Les demandes de paiement ne sont pas encore prises en charge';
|
||||
String get paymentRequestNotSupported =>
|
||||
'Les demandes de paiement ne sont pas encore prises en charge';
|
||||
|
||||
@override
|
||||
String get p2pkTitle => 'Clés P2PK';
|
||||
@@ -1006,10 +1078,12 @@ class L10nFr extends L10n {
|
||||
String get p2pkSettingsDescription => 'Recevoir ecash verrouillé';
|
||||
|
||||
@override
|
||||
String get p2pkExperimental => 'P2PK est expérimental. Utiliser avec prudence.';
|
||||
String get p2pkExperimental =>
|
||||
'P2PK est expérimental. Utiliser avec prudence.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'Vous avez un envoi P2PK en attente. Allez dans l\'historique et actualisez après que le destinataire a réclamé le jeton.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'Vous avez un envoi P2PK en attente. Allez dans l\'historique et actualisez après que le destinataire a réclamé le jeton.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Expérimental';
|
||||
@@ -1066,7 +1140,8 @@ class L10nFr extends L10n {
|
||||
String get p2pkLockedToOther => 'Verrouillé pour une autre clé';
|
||||
|
||||
@override
|
||||
String get p2pkCannotUnlock => 'Vous n\'avez pas la clé pour déverrouiller ce token';
|
||||
String get p2pkCannotUnlock =>
|
||||
'Vous n\'avez pas la clé pour déverrouiller ce token';
|
||||
|
||||
@override
|
||||
String get p2pkEnterPrivateKey => 'Entrer la clé privée (nsec)';
|
||||
@@ -1075,13 +1150,15 @@ class L10nFr extends L10n {
|
||||
String get p2pkDeleteTitle => 'Supprimer la clé';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => 'Supprimer cette clé ? Vous ne pourrez plus recevoir de tokens verrouillés dessus.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'Supprimer cette clé ? Vous ne pourrez plus recevoir de tokens verrouillés dessus.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK nécessite une connexion au mint';
|
||||
|
||||
@override
|
||||
String get p2pkErrorMaxKeysReached => 'Nombre maximum de clés importées atteint (10)';
|
||||
String get p2pkErrorMaxKeysReached =>
|
||||
'Nombre maximum de clés importées atteint (10)';
|
||||
|
||||
@override
|
||||
String get p2pkErrorInvalidNsec => 'nsec invalide';
|
||||
@@ -1093,5 +1170,6 @@ class L10nFr extends L10n {
|
||||
String get p2pkErrorKeyNotFound => 'Clé non trouvée';
|
||||
|
||||
@override
|
||||
String get p2pkErrorCannotDeletePrimary => 'Impossible de supprimer la clé principale';
|
||||
String get p2pkErrorCannotDeletePrimary =>
|
||||
'Impossible de supprimer la clé principale';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -60,7 +60,8 @@ class L10nIt extends L10n {
|
||||
String get generatingSeed => 'Generazione sicura della tua frase seed';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'Verrà generata una frase seed di 12 parole.\nConservala in un luogo sicuro.';
|
||||
String get createWalletDescription =>
|
||||
'Verrà generata una frase seed di 12 parole.\nConservala in un luogo sicuro.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Genera portafoglio';
|
||||
@@ -69,10 +70,12 @@ class L10nIt extends L10n {
|
||||
String get walletCreated => 'Portafoglio creato!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Il tuo portafoglio è pronto. Ti consigliamo di fare il backup della frase seed ora.';
|
||||
String get walletCreatedDescription =>
|
||||
'Il tuo portafoglio è pronto. Ti consigliamo di fare il backup della frase seed ora.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Senza backup, perderai l\'accesso ai tuoi fondi se perdi il dispositivo.';
|
||||
String get backupWarning =>
|
||||
'Senza backup, perderai l\'accesso ai tuoi fondi se perdi il dispositivo.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Backup adesso';
|
||||
@@ -87,7 +90,8 @@ class L10nIt extends L10n {
|
||||
String get seedPhraseTitle => 'La tua frase seed';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Conserva queste 12 parole in ordine. Sono l\'unico modo per recuperare il tuo portafoglio.';
|
||||
String get seedPhraseDescription =>
|
||||
'Conserva queste 12 parole in ordine. Sono l\'unico modo per recuperare il tuo portafoglio.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Mostra frase seed';
|
||||
@@ -102,7 +106,8 @@ class L10nIt extends L10n {
|
||||
String get seedCopied => 'Frase copiata negli appunti';
|
||||
|
||||
@override
|
||||
String get neverShareSeed => 'Non condividere mai la tua frase seed con nessuno.';
|
||||
String get neverShareSeed =>
|
||||
'Non condividere mai la tua frase seed con nessuno.';
|
||||
|
||||
@override
|
||||
String get confirmBackup => 'Ho salvato la mia frase seed in un luogo sicuro';
|
||||
@@ -117,7 +122,8 @@ class L10nIt extends L10n {
|
||||
String get enterSeedPhrase => 'Inserisci la tua frase seed';
|
||||
|
||||
@override
|
||||
String get enterSeedDescription => 'Scrivi le 12 o 24 parole separate da spazi.';
|
||||
String get enterSeedDescription =>
|
||||
'Scrivi le 12 o 24 parole separate da spazi.';
|
||||
|
||||
@override
|
||||
String get seedPlaceholder => 'parola1 parola2 parola3 ...';
|
||||
@@ -310,11 +316,50 @@ class L10nIt extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.';
|
||||
String get keepTokenWarning =>
|
||||
'Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Token copiato negli appunti';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Copia come emoji';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Token copiato come emoji 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'Impossibile decodificare il token emoji. Potrebbe essere corrotto.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Scrivi su tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'Leggi tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Avvicina il dispositivo al tag NFC...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Token scritto sul tag NFC';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'Errore NFC scrittura: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'Errore NFC lettura: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC è disattivato. Attivalo nelle Impostazioni.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'Questo dispositivo non supporta NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Importo da depositare:';
|
||||
|
||||
@@ -450,7 +495,8 @@ class L10nIt extends L10n {
|
||||
String get noPendingTransactions => 'Nessuna transazione in attesa';
|
||||
|
||||
@override
|
||||
String get allTransactionsCompleted => 'Tutte le tue transazioni sono completate';
|
||||
String get allTransactionsCompleted =>
|
||||
'Tutte le tue transazioni sono completate';
|
||||
|
||||
@override
|
||||
String get noEcashTransactions => 'Nessuna transazione Ecash';
|
||||
@@ -665,7 +711,8 @@ class L10nIt extends L10n {
|
||||
String get close => 'Chiudi';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'Impossibile aprire il link';
|
||||
@@ -677,7 +724,8 @@ class L10nIt extends L10n {
|
||||
String get actionIrreversible => 'Questa azione è irreversibile';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'Tutti i dati verranno eliminati, inclusa la frase seed e i token. Assicurati di avere un backup.';
|
||||
String get deleteWalletWarning =>
|
||||
'Tutti i dati verranno eliminati, inclusa la frase seed e i token. Assicurati di avere un backup.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Scrivi \"ELIMINA\" per confermare:';
|
||||
@@ -694,7 +742,8 @@ class L10nIt extends L10n {
|
||||
String get recoverTokensTitle => 'Recupera token';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Scansiona i mint per recuperare i token associati alla tua frase seed (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Scansiona i mint per recuperare i token associati alla tua frase seed (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Usa la mia frase seed attuale';
|
||||
@@ -720,7 +769,8 @@ class L10nIt extends L10n {
|
||||
String get specificMint => 'Un mint specifico';
|
||||
|
||||
@override
|
||||
String get enterMnemonicWords => 'Inserisci le 12 parole separate da spazi...';
|
||||
String get enterMnemonicWords =>
|
||||
'Inserisci le 12 parole separate da spazi...';
|
||||
|
||||
@override
|
||||
String get scanMints => 'Scansiona mint';
|
||||
@@ -740,7 +790,8 @@ class L10nIt extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanCompleteNoTokens => 'Scansione completata. Nessun nuovo token trovato.';
|
||||
String get scanCompleteNoTokens =>
|
||||
'Scansione completata. Nessun nuovo token trovato.';
|
||||
|
||||
@override
|
||||
String mintsWithError(int count) {
|
||||
@@ -763,7 +814,8 @@ class L10nIt extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'Nessun token trovato associato a questo mnemonic.';
|
||||
String get noTokensForMnemonic =>
|
||||
'Nessun token trovato associato a questo mnemonic.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'Nessun mint connesso';
|
||||
@@ -843,7 +895,8 @@ class L10nIt extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Elimina mint';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'Se hai saldo su questo mint, verrà perso. Sei sicuro?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'Se hai saldo su questo mint, verrà perso. Sei sicuro?';
|
||||
|
||||
@override
|
||||
String get delete => 'Elimina';
|
||||
@@ -896,19 +949,23 @@ class L10nIt extends L10n {
|
||||
String get tokenSavedForLater => 'Token salvato per riscattarlo dopo';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'Nessuna connessione. Token salvato per riscattarlo dopo.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'Nessuna connessione. Token salvato per riscattarlo dopo.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'Questo token proviene da un mint sconosciuto. Connettiti a internet per aggiungerlo e riscattare il token.';
|
||||
String get unknownMintOffline =>
|
||||
'Questo token proviene da un mint sconosciuto. Connettiti a internet per aggiungerlo e riscattare il token.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Nessuna connessione al mint. Riprova più tardi.';
|
||||
String get noConnectionTryLater =>
|
||||
'Nessuna connessione al mint. Riprova più tardi.';
|
||||
|
||||
@override
|
||||
String get saveTokenError => 'Errore nel salvare il token. Riprova.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Limite token in attesa raggiunto (max 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Limite token in attesa raggiunto (max 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'Da ricevere';
|
||||
@@ -976,19 +1033,23 @@ class L10nIt extends L10n {
|
||||
String get pointCameraAtQr => 'Punta la fotocamera sul codice QR';
|
||||
|
||||
@override
|
||||
String get pointCameraAtCashuQr => 'Punta la fotocamera sul QR del token Cashu';
|
||||
String get pointCameraAtCashuQr =>
|
||||
'Punta la fotocamera sul QR del token Cashu';
|
||||
|
||||
@override
|
||||
String get pointCameraAtInvoiceQr => 'Punta la fotocamera sul QR della fattura';
|
||||
String get pointCameraAtInvoiceQr =>
|
||||
'Punta la fotocamera sul QR della fattura';
|
||||
|
||||
@override
|
||||
String get unrecognizedQrCode => 'Codice QR non riconosciuto';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Scansiona un token Cashu (cashuA... o cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Scansiona un token Cashu (cashuA... o cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Scansiona una fattura Lightning (lnbc...)';
|
||||
String get scanLightningInvoiceHint =>
|
||||
'Scansiona una fattura Lightning (lnbc...)';
|
||||
|
||||
@override
|
||||
String get addMintQuestion => 'Aggiungere questo mint?';
|
||||
@@ -997,7 +1058,8 @@ class L10nIt extends L10n {
|
||||
String get cameraPermissionDenied => 'Permesso fotocamera negato';
|
||||
|
||||
@override
|
||||
String get paymentRequestNotSupported => 'Le richieste di pagamento non sono ancora supportate';
|
||||
String get paymentRequestNotSupported =>
|
||||
'Le richieste di pagamento non sono ancora supportate';
|
||||
|
||||
@override
|
||||
String get p2pkTitle => 'Chiavi P2PK';
|
||||
@@ -1009,7 +1071,8 @@ class L10nIt extends L10n {
|
||||
String get p2pkExperimental => 'P2PK è sperimentale. Usare con cautela.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'Hai un invio P2PK in sospeso. Vai alla cronologia e aggiorna dopo che il destinatario ha riscattato il token.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'Hai un invio P2PK in sospeso. Vai alla cronologia e aggiorna dopo che il destinatario ha riscattato il token.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Sperimentale';
|
||||
@@ -1075,13 +1138,15 @@ class L10nIt extends L10n {
|
||||
String get p2pkDeleteTitle => 'Elimina chiave';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => 'Eliminare questa chiave? Non potrai ricevere token bloccati ad essa.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'Eliminare questa chiave? Non potrai ricevere token bloccati ad essa.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK richiede connessione al mint';
|
||||
|
||||
@override
|
||||
String get p2pkErrorMaxKeysReached => 'Numero massimo di chiavi importate raggiunto (10)';
|
||||
String get p2pkErrorMaxKeysReached =>
|
||||
'Numero massimo di chiavi importate raggiunto (10)';
|
||||
|
||||
@override
|
||||
String get p2pkErrorInvalidNsec => 'nsec non valido';
|
||||
@@ -1093,5 +1158,6 @@ class L10nIt extends L10n {
|
||||
String get p2pkErrorKeyNotFound => 'Chiave non trovata';
|
||||
|
||||
@override
|
||||
String get p2pkErrorCannotDeletePrimary => 'Impossibile eliminare la chiave principale';
|
||||
String get p2pkErrorCannotDeletePrimary =>
|
||||
'Impossibile eliminare la chiave principale';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -69,7 +69,8 @@ class L10nJa extends L10n {
|
||||
String get walletCreated => 'ウォレット作成完了!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'ウォレットの準備ができました。今すぐシードフレーズをバックアップすることをお勧めします。';
|
||||
String get walletCreatedDescription =>
|
||||
'ウォレットの準備ができました。今すぐシードフレーズをバックアップすることをお勧めします。';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'バックアップがないと、デバイスを紛失した場合に資金にアクセスできなくなります。';
|
||||
@@ -315,6 +316,43 @@ class L10nJa extends L10n {
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'トークンをクリップボードにコピーしました';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => '絵文字としてコピー';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'トークンを絵文字としてコピーしました 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError => '絵文字トークンをデコードできませんでした。破損している可能性があります。';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'NFCタグに書き込む';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'NFCタグを読み取る';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'デバイスをNFCタグに近づけてください...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'トークンをNFCタグに書き込みました';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'NFC書き込みエラー: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'NFC読み取りエラー: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFCが無効です。設定で有効にしてください。';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'このデバイスはNFCに対応していません';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => '入金額:';
|
||||
|
||||
@@ -665,7 +703,7 @@ class L10nJa extends L10n {
|
||||
String get close => '閉じる';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'キューバのDNAを持つ世界のためのCashuウォレット。La Chispaの兄弟。';
|
||||
String get aboutDescription => 'キューバのDNAを持つ世界のためのCashuウォレット。LaChispaの兄弟。';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'リンクを開けませんでした';
|
||||
@@ -677,7 +715,8 @@ class L10nJa extends L10n {
|
||||
String get actionIrreversible => 'この操作は取り消せません';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'シードフレーズとトークンを含むすべてのデータが削除されます。バックアップがあることを確認してください。';
|
||||
String get deleteWalletWarning =>
|
||||
'シードフレーズとトークンを含むすべてのデータが削除されます。バックアップがあることを確認してください。';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => '確認のため「DELETE」と入力:';
|
||||
@@ -694,7 +733,8 @@ class L10nJa extends L10n {
|
||||
String get recoverTokensTitle => 'トークンを復元';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'シードフレーズに関連付けられたトークンを復元するためにMintをスキャン(NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'シードフレーズに関連付けられたトークンを復元するためにMintをスキャン(NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => '現在のシードフレーズを使用';
|
||||
@@ -899,7 +939,8 @@ class L10nJa extends L10n {
|
||||
String get noConnectionTokenSaved => '接続なし。トークンを保存しました。後で請求できます。';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'このトークンは不明なMintからのものです。インターネットに接続してMintを追加し、トークンを請求してください。';
|
||||
String get unknownMintOffline =>
|
||||
'このトークンは不明なMintからのものです。インターネットに接続してMintを追加し、トークンを請求してください。';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Mintに接続できません。後でもう一度お試しください。';
|
||||
@@ -1009,7 +1050,8 @@ class L10nJa extends L10n {
|
||||
String get p2pkExperimental => 'P2PKは実験的機能です。注意してご使用ください。';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => '保留中のP2PK送信があります。受取人がトークンを受け取った後、履歴で更新してください。';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'保留中のP2PK送信があります。受取人がトークンを受け取った後、履歴で更新してください。';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => '実験的';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -60,7 +60,8 @@ class L10nKo extends L10n {
|
||||
String get generatingSeed => '시드 문구를 안전하게 생성 중';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => '12개의 단어로 된 시드 문구가 생성됩니다.\n안전한 곳에 보관하세요.';
|
||||
String get createWalletDescription =>
|
||||
'12개의 단어로 된 시드 문구가 생성됩니다.\n안전한 곳에 보관하세요.';
|
||||
|
||||
@override
|
||||
String get generateWallet => '지갑 생성';
|
||||
@@ -69,7 +70,8 @@ class L10nKo extends L10n {
|
||||
String get walletCreated => '지갑이 생성되었습니다!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => '지갑이 준비되었습니다. 지금 시드 문구를 백업하는 것을 권장합니다.';
|
||||
String get walletCreatedDescription =>
|
||||
'지갑이 준비되었습니다. 지금 시드 문구를 백업하는 것을 권장합니다.';
|
||||
|
||||
@override
|
||||
String get backupWarning => '백업 없이는 기기를 분실하면 자금에 접근할 수 없습니다.';
|
||||
@@ -87,7 +89,8 @@ class L10nKo extends L10n {
|
||||
String get seedPhraseTitle => '시드 문구';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => '이 12개의 단어를 순서대로 저장하세요. 지갑을 복구하는 유일한 방법입니다.';
|
||||
String get seedPhraseDescription =>
|
||||
'이 12개의 단어를 순서대로 저장하세요. 지갑을 복구하는 유일한 방법입니다.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => '시드 문구 보기';
|
||||
@@ -315,6 +318,43 @@ class L10nKo extends L10n {
|
||||
@override
|
||||
String get tokenCopiedToClipboard => '토큰이 클립보드에 복사되었습니다';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => '이모지로 복사';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => '토큰이 이모지로 복사되었습니다 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError => '이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'NFC 태그에 쓰기';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'NFC 태그 읽기';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => '기기를 NFC 태그에 가까이 대세요...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => '토큰이 NFC 태그에 기록되었습니다';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'NFC 쓰기 오류: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'NFC 읽기 오류: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC가 비활성화되어 있습니다. 설정에서 활성화하세요.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => '이 기기는 NFC를 지원하지 않습니다';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => '입금할 금액:';
|
||||
|
||||
@@ -665,7 +705,7 @@ class L10nKo extends L10n {
|
||||
String get close => '닫기';
|
||||
|
||||
@override
|
||||
String get aboutDescription => '전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. La Chispa의 형제.';
|
||||
String get aboutDescription => '전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. LaChispa의 형제.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => '링크를 열 수 없음';
|
||||
@@ -677,7 +717,8 @@ class L10nKo extends L10n {
|
||||
String get actionIrreversible => '이 작업은 되돌릴 수 없습니다';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => '시드 문구와 토큰을 포함한 모든 데이터가 삭제됩니다. 백업이 있는지 확인하세요.';
|
||||
String get deleteWalletWarning =>
|
||||
'시드 문구와 토큰을 포함한 모든 데이터가 삭제됩니다. 백업이 있는지 확인하세요.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => '확인하려면 \"삭제\"를 입력하세요:';
|
||||
@@ -694,7 +735,8 @@ class L10nKo extends L10n {
|
||||
String get recoverTokensTitle => '토큰 복구';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => '시드 문구와 연결된 토큰을 복구하기 위해 mint 스캔 (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'시드 문구와 연결된 토큰을 복구하기 위해 mint 스캔 (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => '현재 시드 문구 사용';
|
||||
@@ -899,7 +941,8 @@ class L10nKo extends L10n {
|
||||
String get noConnectionTokenSaved => '연결 없음. 나중에 청구하기 위해 토큰이 저장되었습니다.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => '이 토큰은 알 수 없는 mint의 것입니다. 인터넷에 연결하여 추가하고 토큰을 청구하세요.';
|
||||
String get unknownMintOffline =>
|
||||
'이 토큰은 알 수 없는 mint의 것입니다. 인터넷에 연결하여 추가하고 토큰을 청구하세요.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Mint에 연결할 수 없음. 나중에 다시 시도하세요.';
|
||||
@@ -1009,7 +1052,8 @@ class L10nKo extends L10n {
|
||||
String get p2pkExperimental => 'P2PK는 실험적 기능입니다. 주의하여 사용하세요.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => '대기 중인 P2PK 전송이 있습니다. 수신자가 토큰을 수령한 후 기록에서 새로고침하세요.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'대기 중인 P2PK 전송이 있습니다. 수신자가 토큰을 수령한 후 기록에서 새로고침하세요.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => '실험적';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -60,7 +60,8 @@ class L10nPt extends L10n {
|
||||
String get generatingSeed => 'Gerando sua frase semente de forma segura';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'Uma frase semente de 12 palavras será gerada.\nGuarde-a em um lugar seguro.';
|
||||
String get createWalletDescription =>
|
||||
'Uma frase semente de 12 palavras será gerada.\nGuarde-a em um lugar seguro.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Gerar wallet';
|
||||
@@ -69,10 +70,12 @@ class L10nPt extends L10n {
|
||||
String get walletCreated => 'Wallet criada!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Sua wallet está pronta. Recomendamos fazer backup da sua frase semente agora.';
|
||||
String get walletCreatedDescription =>
|
||||
'Sua wallet está pronta. Recomendamos fazer backup da sua frase semente agora.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Sem backup, você perderá acesso aos seus fundos se perder o dispositivo.';
|
||||
String get backupWarning =>
|
||||
'Sem backup, você perderá acesso aos seus fundos se perder o dispositivo.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Fazer backup agora';
|
||||
@@ -87,7 +90,8 @@ class L10nPt extends L10n {
|
||||
String get seedPhraseTitle => 'Sua frase semente';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Guarde estas 12 palavras em ordem. Elas são a única forma de recuperar sua wallet.';
|
||||
String get seedPhraseDescription =>
|
||||
'Guarde estas 12 palavras em ordem. Elas são a única forma de recuperar sua wallet.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Revelar frase semente';
|
||||
@@ -102,7 +106,8 @@ class L10nPt extends L10n {
|
||||
String get seedCopied => 'Frase copiada para área de transferência';
|
||||
|
||||
@override
|
||||
String get neverShareSeed => 'Nunca compartilhe sua frase semente com ninguém.';
|
||||
String get neverShareSeed =>
|
||||
'Nunca compartilhe sua frase semente com ninguém.';
|
||||
|
||||
@override
|
||||
String get confirmBackup => 'Guardei minha frase semente em um lugar seguro';
|
||||
@@ -117,7 +122,8 @@ class L10nPt extends L10n {
|
||||
String get enterSeedPhrase => 'Digite sua frase semente';
|
||||
|
||||
@override
|
||||
String get enterSeedDescription => 'Digite as 12 ou 24 palavras separadas por espaços.';
|
||||
String get enterSeedDescription =>
|
||||
'Digite as 12 ou 24 palavras separadas por espaços.';
|
||||
|
||||
@override
|
||||
String get seedPlaceholder => 'palavra1 palavra2 palavra3 ...';
|
||||
@@ -310,10 +316,50 @@ class L10nPt extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.';
|
||||
String get keepTokenWarning =>
|
||||
'Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Token copiado para área de transferência';
|
||||
String get tokenCopiedToClipboard =>
|
||||
'Token copiado para área de transferência';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Copiar como emoji';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'Não foi possível decodificar o token emoji. Pode estar corrompido.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Escrever em tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'Ler tag NFC';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Aproxime o dispositivo da tag NFC...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Token escrito na tag NFC';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'Erro NFC ao escrever: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'Erro NFC ao ler: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC está desativado. Ative nas Configurações.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'Este dispositivo não suporta NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Valor a depositar:';
|
||||
@@ -364,7 +410,8 @@ class L10nPt extends L10n {
|
||||
String get description => 'Descrição:';
|
||||
|
||||
@override
|
||||
String get invoiceCopiedToClipboard => 'Invoice copiado para área de transferência';
|
||||
String get invoiceCopiedToClipboard =>
|
||||
'Invoice copiado para área de transferência';
|
||||
|
||||
@override
|
||||
String deposited(String amount, String unit) {
|
||||
@@ -450,7 +497,8 @@ class L10nPt extends L10n {
|
||||
String get noPendingTransactions => 'Sem transações pendentes';
|
||||
|
||||
@override
|
||||
String get allTransactionsCompleted => 'Todas as suas transações estão completas';
|
||||
String get allTransactionsCompleted =>
|
||||
'Todas as suas transações estão completas';
|
||||
|
||||
@override
|
||||
String get noEcashTransactions => 'Sem transações Ecash';
|
||||
@@ -665,7 +713,8 @@ class L10nPt extends L10n {
|
||||
String get close => 'Fechar';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'Não foi possível abrir o link';
|
||||
@@ -677,7 +726,8 @@ class L10nPt extends L10n {
|
||||
String get actionIrreversible => 'Esta ação é irreversível';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'Todos os dados serão excluídos, incluindo sua frase semente e tokens. Certifique-se de ter um backup.';
|
||||
String get deleteWalletWarning =>
|
||||
'Todos os dados serão excluídos, incluindo sua frase semente e tokens. Certifique-se de ter um backup.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Digite \"APAGAR\" para confirmar:';
|
||||
@@ -694,7 +744,8 @@ class L10nPt extends L10n {
|
||||
String get recoverTokensTitle => 'Recuperar tokens';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Escanear mints para recuperar tokens associados à sua frase semente (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Escanear mints para recuperar tokens associados à sua frase semente (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Usar minha frase semente atual';
|
||||
@@ -720,7 +771,8 @@ class L10nPt extends L10n {
|
||||
String get specificMint => 'Um mint específico';
|
||||
|
||||
@override
|
||||
String get enterMnemonicWords => 'Digite as 12 palavras separadas por espaços...';
|
||||
String get enterMnemonicWords =>
|
||||
'Digite as 12 palavras separadas por espaços...';
|
||||
|
||||
@override
|
||||
String get scanMints => 'Escanear mints';
|
||||
@@ -740,7 +792,8 @@ class L10nPt extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanCompleteNoTokens => 'Escaneamento completo. Nenhum token novo encontrado.';
|
||||
String get scanCompleteNoTokens =>
|
||||
'Escaneamento completo. Nenhum token novo encontrado.';
|
||||
|
||||
@override
|
||||
String mintsWithError(int count) {
|
||||
@@ -763,7 +816,8 @@ class L10nPt extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'Nenhum token encontrado associado a esse mnemônico.';
|
||||
String get noTokensForMnemonic =>
|
||||
'Nenhum token encontrado associado a esse mnemônico.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'Nenhum mint conectado';
|
||||
@@ -843,7 +897,8 @@ class L10nPt extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Excluir mint';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'Se você tiver saldo neste mint, ele será perdido. Tem certeza?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'Se você tiver saldo neste mint, ele será perdido. Tem certeza?';
|
||||
|
||||
@override
|
||||
String get delete => 'Excluir';
|
||||
@@ -896,10 +951,12 @@ class L10nPt extends L10n {
|
||||
String get tokenSavedForLater => 'Token salvo para resgatar depois';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'Sem conexão. Token salvo para resgatar depois.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'Sem conexão. Token salvo para resgatar depois.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'Este token é de um mint desconhecido. Conecte-se à internet para adicioná-lo e resgatar o token.';
|
||||
String get unknownMintOffline =>
|
||||
'Este token é de um mint desconhecido. Conecte-se à internet para adicioná-lo e resgatar o token.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Sem conexão ao mint. Tente mais tarde.';
|
||||
@@ -908,7 +965,8 @@ class L10nPt extends L10n {
|
||||
String get saveTokenError => 'Erro ao salvar o token. Tente novamente.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Limite de tokens pendentes atingido (máx 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Limite de tokens pendentes atingido (máx 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'Para receber';
|
||||
@@ -985,10 +1043,12 @@ class L10nPt extends L10n {
|
||||
String get unrecognizedQrCode => 'Código QR não reconhecido';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Escaneie um token Cashu (cashuA... ou cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Escaneie um token Cashu (cashuA... ou cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Escaneie um invoice Lightning (lnbc...)';
|
||||
String get scanLightningInvoiceHint =>
|
||||
'Escaneie um invoice Lightning (lnbc...)';
|
||||
|
||||
@override
|
||||
String get addMintQuestion => 'Adicionar este mint?';
|
||||
@@ -997,7 +1057,8 @@ class L10nPt extends L10n {
|
||||
String get cameraPermissionDenied => 'Permissão de câmera negada';
|
||||
|
||||
@override
|
||||
String get paymentRequestNotSupported => 'Solicitações de pagamento ainda não são suportadas';
|
||||
String get paymentRequestNotSupported =>
|
||||
'Solicitações de pagamento ainda não são suportadas';
|
||||
|
||||
@override
|
||||
String get p2pkTitle => 'Chaves P2PK';
|
||||
@@ -1009,7 +1070,8 @@ class L10nPt extends L10n {
|
||||
String get p2pkExperimental => 'P2PK é experimental. Use com cautela.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'Você tem um envio P2PK pendente. Vá ao histórico e atualize após o destinatário resgatar o token.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'Você tem um envio P2PK pendente. Vá ao histórico e atualize após o destinatário resgatar o token.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Experimental';
|
||||
@@ -1066,7 +1128,8 @@ class L10nPt extends L10n {
|
||||
String get p2pkLockedToOther => 'Bloqueado para outra chave';
|
||||
|
||||
@override
|
||||
String get p2pkCannotUnlock => 'Você não tem a chave para desbloquear este token';
|
||||
String get p2pkCannotUnlock =>
|
||||
'Você não tem a chave para desbloquear este token';
|
||||
|
||||
@override
|
||||
String get p2pkEnterPrivateKey => 'Digite a chave privada (nsec)';
|
||||
@@ -1075,13 +1138,15 @@ class L10nPt extends L10n {
|
||||
String get p2pkDeleteTitle => 'Excluir chave';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => 'Excluir esta chave? Você não poderá receber tokens bloqueados para ela.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'Excluir esta chave? Você não poderá receber tokens bloqueados para ela.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK requer conexão com o mint';
|
||||
|
||||
@override
|
||||
String get p2pkErrorMaxKeysReached => 'Número máximo de chaves importadas atingido (10)';
|
||||
String get p2pkErrorMaxKeysReached =>
|
||||
'Número máximo de chaves importadas atingido (10)';
|
||||
|
||||
@override
|
||||
String get p2pkErrorInvalidNsec => 'nsec inválido';
|
||||
@@ -1093,5 +1158,6 @@ class L10nPt extends L10n {
|
||||
String get p2pkErrorKeyNotFound => 'Chave não encontrada';
|
||||
|
||||
@override
|
||||
String get p2pkErrorCannotDeletePrimary => 'Não é possível excluir a chave principal';
|
||||
String get p2pkErrorCannotDeletePrimary =>
|
||||
'Não é possível excluir a chave principal';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -33,7 +33,8 @@ class L10nRu extends L10n {
|
||||
String get loadingMessage6 => 'Go full Calle...';
|
||||
|
||||
@override
|
||||
String get loadingMessage7 => 'Cashu + Bitchat = Конфиденциальность + Свобода';
|
||||
String get loadingMessage7 =>
|
||||
'Cashu + Bitchat = Конфиденциальность + Свобода';
|
||||
|
||||
@override
|
||||
String get aboutTagline => 'Конфиденциальность без границ.';
|
||||
@@ -60,7 +61,8 @@ class L10nRu extends L10n {
|
||||
String get generatingSeed => 'Безопасная генерация вашей сид-фразы';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'Будет сгенерирована сид-фраза из 12 слов.\nСохраните её в безопасном месте.';
|
||||
String get createWalletDescription =>
|
||||
'Будет сгенерирована сид-фраза из 12 слов.\nСохраните её в безопасном месте.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Создать кошелёк';
|
||||
@@ -69,10 +71,12 @@ class L10nRu extends L10n {
|
||||
String get walletCreated => 'Кошелёк создан!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Ваш кошелёк готов. Рекомендуем сделать резервную копию сид-фразы сейчас.';
|
||||
String get walletCreatedDescription =>
|
||||
'Ваш кошелёк готов. Рекомендуем сделать резервную копию сид-фразы сейчас.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Без резервной копии вы потеряете доступ к средствам при потере устройства.';
|
||||
String get backupWarning =>
|
||||
'Без резервной копии вы потеряете доступ к средствам при потере устройства.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Сделать резервную копию';
|
||||
@@ -87,7 +91,8 @@ class L10nRu extends L10n {
|
||||
String get seedPhraseTitle => 'Ваша сид-фраза';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Сохраните эти 12 слов по порядку. Это единственный способ восстановить кошелёк.';
|
||||
String get seedPhraseDescription =>
|
||||
'Сохраните эти 12 слов по порядку. Это единственный способ восстановить кошелёк.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Показать сид-фразу';
|
||||
@@ -310,11 +315,50 @@ class L10nRu extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.';
|
||||
String get keepTokenWarning =>
|
||||
'Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Токен скопирован в буфер обмена';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Скопировать как эмодзи';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Токен скопирован как эмодзи 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'Не удалось декодировать эмодзи-токен. Возможно, он повреждён.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Записать на NFC-метку';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'Читать NFC-метку';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Поднесите устройство к NFC-метке...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Токен записан на NFC-метку';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'Ошибка записи NFC: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'Ошибка чтения NFC: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC отключён. Включите в Настройках.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'Это устройство не поддерживает NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Сумма для пополнения:';
|
||||
|
||||
@@ -384,7 +428,8 @@ class L10nRu extends L10n {
|
||||
String get invalidInvoice => 'Недействительный счёт';
|
||||
|
||||
@override
|
||||
String get invalidInvoiceMalformed => 'Недействительный или повреждённый счёт';
|
||||
String get invalidInvoiceMalformed =>
|
||||
'Недействительный или повреждённый счёт';
|
||||
|
||||
@override
|
||||
String get feeReserved => 'Зарезервированная комиссия:';
|
||||
@@ -462,7 +507,8 @@ class L10nRu extends L10n {
|
||||
String get noLightningTransactions => 'Нет Lightning транзакций';
|
||||
|
||||
@override
|
||||
String get depositOrWithdrawLightning => 'Пополните или выведите через Lightning';
|
||||
String get depositOrWithdrawLightning =>
|
||||
'Пополните или выведите через Lightning';
|
||||
|
||||
@override
|
||||
String get pendingStatus => 'Ожидание';
|
||||
@@ -665,7 +711,8 @@ class L10nRu extends L10n {
|
||||
String get close => 'Закрыть';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'Cashu кошелёк с кубинской ДНК для всего мира. Брат La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'Cashu кошелёк с кубинской ДНК для всего мира. Брат LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'Не удалось открыть ссылку';
|
||||
@@ -677,7 +724,8 @@ class L10nRu extends L10n {
|
||||
String get actionIrreversible => 'Это действие необратимо';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'Все данные будут удалены, включая сид-фразу и токены. Убедитесь, что у вас есть резервная копия.';
|
||||
String get deleteWalletWarning =>
|
||||
'Все данные будут удалены, включая сид-фразу и токены. Убедитесь, что у вас есть резервная копия.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Введите \"УДАЛИТЬ\" для подтверждения:';
|
||||
@@ -694,7 +742,8 @@ class L10nRu extends L10n {
|
||||
String get recoverTokensTitle => 'Восстановить токены';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Сканировать mint для восстановления токенов, связанных с вашей сид-фразой (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Сканировать mint для восстановления токенов, связанных с вашей сид-фразой (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Использовать текущую сид-фразу';
|
||||
@@ -729,7 +778,8 @@ class L10nRu extends L10n {
|
||||
String get selectMintToScan => 'Выберите mint для сканирования';
|
||||
|
||||
@override
|
||||
String get mnemonicMustHaveWords => 'Мнемоника должна содержать 12 или 24 слова';
|
||||
String get mnemonicMustHaveWords =>
|
||||
'Мнемоника должна содержать 12 или 24 слова';
|
||||
|
||||
@override
|
||||
String get noConnectedMintsToScan => 'Нет подключённых mint для сканирования';
|
||||
@@ -740,7 +790,8 @@ class L10nRu extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanCompleteNoTokens => 'Сканирование завершено. Новых токенов не найдено.';
|
||||
String get scanCompleteNoTokens =>
|
||||
'Сканирование завершено. Новых токенов не найдено.';
|
||||
|
||||
@override
|
||||
String mintsWithError(int count) {
|
||||
@@ -763,7 +814,8 @@ class L10nRu extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'Токены, связанные с этой мнемоникой, не найдены.';
|
||||
String get noTokensForMnemonic =>
|
||||
'Токены, связанные с этой мнемоникой, не найдены.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'Нет подключённых mint';
|
||||
@@ -843,7 +895,8 @@ class L10nRu extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Удалить mint';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'Если у вас есть баланс на этом mint, он будет потерян. Вы уверены?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'Если у вас есть баланс на этом mint, он будет потерян. Вы уверены?';
|
||||
|
||||
@override
|
||||
String get delete => 'Удалить';
|
||||
@@ -896,10 +949,12 @@ class L10nRu extends L10n {
|
||||
String get tokenSavedForLater => 'Токен сохранён для получения позже';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'Нет соединения. Токен сохранён для получения позже.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'Нет соединения. Токен сохранён для получения позже.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'Этот токен от неизвестного mint. Подключитесь к интернету, чтобы добавить его и получить токен.';
|
||||
String get unknownMintOffline =>
|
||||
'Этот токен от неизвестного mint. Подключитесь к интернету, чтобы добавить его и получить токен.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Нет соединения с mint. Попробуйте позже.';
|
||||
@@ -908,7 +963,8 @@ class L10nRu extends L10n {
|
||||
String get saveTokenError => 'Ошибка сохранения токена. Попробуйте снова.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Достигнут лимит ожидающих токенов (макс 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Достигнут лимит ожидающих токенов (макс 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'К получению';
|
||||
@@ -985,7 +1041,8 @@ class L10nRu extends L10n {
|
||||
String get unrecognizedQrCode => 'Нераспознанный QR-код';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Сканируйте Cashu токен (cashuA... или cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Сканируйте Cashu токен (cashuA... или cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Сканируйте Lightning счёт (lnbc...)';
|
||||
@@ -997,7 +1054,8 @@ class L10nRu extends L10n {
|
||||
String get cameraPermissionDenied => 'Доступ к камере запрещён';
|
||||
|
||||
@override
|
||||
String get paymentRequestNotSupported => 'Запросы на оплату пока не поддерживаются';
|
||||
String get paymentRequestNotSupported =>
|
||||
'Запросы на оплату пока не поддерживаются';
|
||||
|
||||
@override
|
||||
String get p2pkTitle => 'Ключи P2PK';
|
||||
@@ -1006,10 +1064,12 @@ class L10nRu extends L10n {
|
||||
String get p2pkSettingsDescription => 'Получить заблокированный ecash';
|
||||
|
||||
@override
|
||||
String get p2pkExperimental => 'P2PK экспериментальный. Используйте с осторожностью.';
|
||||
String get p2pkExperimental =>
|
||||
'P2PK экспериментальный. Используйте с осторожностью.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'У вас есть ожидающая отправка P2PK. Перейдите в историю и обновите после того, как получатель заберёт токен.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'У вас есть ожидающая отправка P2PK. Перейдите в историю и обновите после того, как получатель заберёт токен.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Экспериментальный';
|
||||
@@ -1066,7 +1126,8 @@ class L10nRu extends L10n {
|
||||
String get p2pkLockedToOther => 'Заблокировано для другого ключа';
|
||||
|
||||
@override
|
||||
String get p2pkCannotUnlock => 'У вас нет ключа для разблокировки этого токена';
|
||||
String get p2pkCannotUnlock =>
|
||||
'У вас нет ключа для разблокировки этого токена';
|
||||
|
||||
@override
|
||||
String get p2pkEnterPrivateKey => 'Введите приватный ключ (nsec)';
|
||||
@@ -1075,13 +1136,15 @@ class L10nRu extends L10n {
|
||||
String get p2pkDeleteTitle => 'Удалить ключ';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => 'Удалить этот ключ? Вы не сможете получать токены заблокированные на него.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'Удалить этот ключ? Вы не сможете получать токены заблокированные на него.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK требует подключения к mint';
|
||||
|
||||
@override
|
||||
String get p2pkErrorMaxKeysReached => 'Достигнуто максимальное количество импортированных ключей (10)';
|
||||
String get p2pkErrorMaxKeysReached =>
|
||||
'Достигнуто максимальное количество импортированных ключей (10)';
|
||||
|
||||
@override
|
||||
String get p2pkErrorInvalidNsec => 'Недействительный nsec';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -60,7 +60,8 @@ class L10nSw extends L10n {
|
||||
String get generatingSeed => 'Kutengeneza maneno yako ya mbegu kwa usalama';
|
||||
|
||||
@override
|
||||
String get createWalletDescription => 'Maneno 12 ya mbegu yatatengenezwa.\nYahifadhi mahali salama.';
|
||||
String get createWalletDescription =>
|
||||
'Maneno 12 ya mbegu yatatengenezwa.\nYahifadhi mahali salama.';
|
||||
|
||||
@override
|
||||
String get generateWallet => 'Tengeneza pochi';
|
||||
@@ -69,10 +70,12 @@ class L10nSw extends L10n {
|
||||
String get walletCreated => 'Pochi imeundwa!';
|
||||
|
||||
@override
|
||||
String get walletCreatedDescription => 'Pochi yako iko tayari. Tunakushauri uhifadhi nakala ya maneno yako ya mbegu sasa.';
|
||||
String get walletCreatedDescription =>
|
||||
'Pochi yako iko tayari. Tunakushauri uhifadhi nakala ya maneno yako ya mbegu sasa.';
|
||||
|
||||
@override
|
||||
String get backupWarning => 'Bila nakala, utapoteza uwezo wa kufikia fedha zako ukipoteza kifaa.';
|
||||
String get backupWarning =>
|
||||
'Bila nakala, utapoteza uwezo wa kufikia fedha zako ukipoteza kifaa.';
|
||||
|
||||
@override
|
||||
String get backupNow => 'Hifadhi nakala sasa';
|
||||
@@ -87,7 +90,8 @@ class L10nSw extends L10n {
|
||||
String get seedPhraseTitle => 'Maneno yako ya mbegu';
|
||||
|
||||
@override
|
||||
String get seedPhraseDescription => 'Hifadhi maneno haya 12 kwa mpangilio. Ndiyo njia pekee ya kurejesha pochi yako.';
|
||||
String get seedPhraseDescription =>
|
||||
'Hifadhi maneno haya 12 kwa mpangilio. Ndiyo njia pekee ya kurejesha pochi yako.';
|
||||
|
||||
@override
|
||||
String get revealSeedPhrase => 'Onyesha maneno ya mbegu';
|
||||
@@ -117,7 +121,8 @@ class L10nSw extends L10n {
|
||||
String get enterSeedPhrase => 'Ingiza maneno yako ya mbegu';
|
||||
|
||||
@override
|
||||
String get enterSeedDescription => 'Andika maneno 12 au 24 yakitengwa na nafasi.';
|
||||
String get enterSeedDescription =>
|
||||
'Andika maneno 12 au 24 yakitengwa na nafasi.';
|
||||
|
||||
@override
|
||||
String get seedPlaceholder => 'neno1 neno2 neno3 ...';
|
||||
@@ -273,7 +278,8 @@ class L10nSw extends L10n {
|
||||
String get noActiveMint => 'Hakuna mint inayofanya kazi';
|
||||
|
||||
@override
|
||||
String get offlineModeMessage => 'Hakuna muunganisho. Kutumia hali ya nje ya mtandao...';
|
||||
String get offlineModeMessage =>
|
||||
'Hakuna muunganisho. Kutumia hali ya nje ya mtandao...';
|
||||
|
||||
@override
|
||||
String get confirmSend => 'Thibitisha kutuma';
|
||||
@@ -310,11 +316,50 @@ class L10nSw extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get keepTokenWarning => 'Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.';
|
||||
String get keepTokenWarning =>
|
||||
'Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.';
|
||||
|
||||
@override
|
||||
String get tokenCopiedToClipboard => 'Tokeni imenakiliwa kwenye ubao';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => 'Nakili kama emoji';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => 'Tokeni imenakiliwa kama emoji 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError =>
|
||||
'Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.';
|
||||
|
||||
@override
|
||||
String get nfcWrite => 'Andika kwenye NFC tag';
|
||||
|
||||
@override
|
||||
String get nfcRead => 'Soma NFC tag';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => 'Karibia kifaa kwenye NFC tag...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => 'Tokeni imeandikwa kwenye NFC tag';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'Hitilafu ya NFC kuandika: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'Hitilafu ya NFC kusoma: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC imezimwa. Iwashe katika Mipangilio.';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => 'Kifaa hiki hakitumii NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => 'Kiasi cha kuweka:';
|
||||
|
||||
@@ -665,7 +710,8 @@ class L10nSw extends L10n {
|
||||
String get close => 'Funga';
|
||||
|
||||
@override
|
||||
String get aboutDescription => 'Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa La Chispa.';
|
||||
String get aboutDescription =>
|
||||
'Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa LaChispa.';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => 'Haikuweza kufungua kiungo';
|
||||
@@ -677,7 +723,8 @@ class L10nSw extends L10n {
|
||||
String get actionIrreversible => 'Kitendo hiki hakiwezi kutenduliwa';
|
||||
|
||||
@override
|
||||
String get deleteWalletWarning => 'Data zote zitafutwa ikiwa ni pamoja na maneno yako ya mbegu na tokeni. Hakikisha una nakala.';
|
||||
String get deleteWalletWarning =>
|
||||
'Data zote zitafutwa ikiwa ni pamoja na maneno yako ya mbegu na tokeni. Hakikisha una nakala.';
|
||||
|
||||
@override
|
||||
String get typeDeleteToConfirm => 'Andika \"FUTA\" kuthibitisha:';
|
||||
@@ -694,13 +741,15 @@ class L10nSw extends L10n {
|
||||
String get recoverTokensTitle => 'Rejesha tokeni';
|
||||
|
||||
@override
|
||||
String get recoverTokensDescription => 'Changanua mint kurejesha tokeni zinazohusiana na maneno yako ya mbegu (NUT-13)';
|
||||
String get recoverTokensDescription =>
|
||||
'Changanua mint kurejesha tokeni zinazohusiana na maneno yako ya mbegu (NUT-13)';
|
||||
|
||||
@override
|
||||
String get useCurrentSeedPhrase => 'Tumia maneno yangu ya mbegu ya sasa';
|
||||
|
||||
@override
|
||||
String get scanWithSavedWords => 'Changanua mint na maneno 12 yaliyohifadhiwa';
|
||||
String get scanWithSavedWords =>
|
||||
'Changanua mint na maneno 12 yaliyohifadhiwa';
|
||||
|
||||
@override
|
||||
String get useOtherSeedPhrase => 'Tumia maneno mengine ya mbegu';
|
||||
@@ -732,7 +781,8 @@ class L10nSw extends L10n {
|
||||
String get mnemonicMustHaveWords => 'Mnemonic lazima iwe na maneno 12 au 24';
|
||||
|
||||
@override
|
||||
String get noConnectedMintsToScan => 'Hakuna mint zilizounganishwa za kuchanganua';
|
||||
String get noConnectedMintsToScan =>
|
||||
'Hakuna mint zilizounganishwa za kuchanganua';
|
||||
|
||||
@override
|
||||
String recoveredTokens(String tokens, int mints) {
|
||||
@@ -740,7 +790,8 @@ class L10nSw extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanCompleteNoTokens => 'Uchanganuzi umekamilika. Hakuna tokeni mpya zilizopatikana.';
|
||||
String get scanCompleteNoTokens =>
|
||||
'Uchanganuzi umekamilika. Hakuna tokeni mpya zilizopatikana.';
|
||||
|
||||
@override
|
||||
String mintsWithError(int count) {
|
||||
@@ -763,7 +814,8 @@ class L10nSw extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get noTokensForMnemonic => 'Hakuna tokeni zilizopatikana zinazohusiana na mnemonic hiyo.';
|
||||
String get noTokensForMnemonic =>
|
||||
'Hakuna tokeni zilizopatikana zinazohusiana na mnemonic hiyo.';
|
||||
|
||||
@override
|
||||
String get noConnectedMints => 'Hakuna mint zilizounganishwa';
|
||||
@@ -843,7 +895,8 @@ class L10nSw extends L10n {
|
||||
String get deleteMintConfirmTitle => 'Futa mint';
|
||||
|
||||
@override
|
||||
String get deleteMintConfirmMessage => 'Ukiwa na salio katika mint hii, itapotea. Una uhakika?';
|
||||
String get deleteMintConfirmMessage =>
|
||||
'Ukiwa na salio katika mint hii, itapotea. Una uhakika?';
|
||||
|
||||
@override
|
||||
String get delete => 'Futa';
|
||||
@@ -896,19 +949,23 @@ class L10nSw extends L10n {
|
||||
String get tokenSavedForLater => 'Tokeni imehifadhiwa kudai baadaye';
|
||||
|
||||
@override
|
||||
String get noConnectionTokenSaved => 'Hakuna muunganisho. Tokeni imehifadhiwa kudai baadaye.';
|
||||
String get noConnectionTokenSaved =>
|
||||
'Hakuna muunganisho. Tokeni imehifadhiwa kudai baadaye.';
|
||||
|
||||
@override
|
||||
String get unknownMintOffline => 'Tokeni hii ni kutoka mint isiyojulikana. Unganisha na mtandao kuongeza na kudai tokeni.';
|
||||
String get unknownMintOffline =>
|
||||
'Tokeni hii ni kutoka mint isiyojulikana. Unganisha na mtandao kuongeza na kudai tokeni.';
|
||||
|
||||
@override
|
||||
String get noConnectionTryLater => 'Hakuna muunganisho na mint. Jaribu baadaye.';
|
||||
String get noConnectionTryLater =>
|
||||
'Hakuna muunganisho na mint. Jaribu baadaye.';
|
||||
|
||||
@override
|
||||
String get saveTokenError => 'Hitilafu ya kuhifadhi tokeni. Jaribu tena.';
|
||||
|
||||
@override
|
||||
String get pendingTokenLimitReached => 'Kikomo cha tokeni zinazosubiri kimefikiwa (upeo 50)';
|
||||
String get pendingTokenLimitReached =>
|
||||
'Kikomo cha tokeni zinazosubiri kimefikiwa (upeo 50)';
|
||||
|
||||
@override
|
||||
String get filterToReceive => 'Za kupokea';
|
||||
@@ -976,7 +1033,8 @@ class L10nSw extends L10n {
|
||||
String get pointCameraAtQr => 'Elekeza kamera kwenye msimbo wa QR';
|
||||
|
||||
@override
|
||||
String get pointCameraAtCashuQr => 'Elekeza kamera kwenye QR ya tokeni ya Cashu';
|
||||
String get pointCameraAtCashuQr =>
|
||||
'Elekeza kamera kwenye QR ya tokeni ya Cashu';
|
||||
|
||||
@override
|
||||
String get pointCameraAtInvoiceQr => 'Elekeza kamera kwenye QR ya ankara';
|
||||
@@ -985,10 +1043,12 @@ class L10nSw extends L10n {
|
||||
String get unrecognizedQrCode => 'Msimbo wa QR haujatambuliwa';
|
||||
|
||||
@override
|
||||
String get scanCashuTokenHint => 'Changanua tokeni ya Cashu (cashuA... au cashuB...)';
|
||||
String get scanCashuTokenHint =>
|
||||
'Changanua tokeni ya Cashu (cashuA... au cashuB...)';
|
||||
|
||||
@override
|
||||
String get scanLightningInvoiceHint => 'Changanua ankara ya Lightning (lnbc...)';
|
||||
String get scanLightningInvoiceHint =>
|
||||
'Changanua ankara ya Lightning (lnbc...)';
|
||||
|
||||
@override
|
||||
String get addMintQuestion => 'Ongeza mint hii?';
|
||||
@@ -1009,7 +1069,8 @@ class L10nSw extends L10n {
|
||||
String get p2pkExperimental => 'P2PK ni ya majaribio. Tumia kwa uangalifu.';
|
||||
|
||||
@override
|
||||
String get p2pkPendingSendWarning => 'Una usafirishaji wa P2PK unaosubiri. Nenda kwenye historia na usasishe baada ya mpokeaji kudai tokeni.';
|
||||
String get p2pkPendingSendWarning =>
|
||||
'Una usafirishaji wa P2PK unaosubiri. Nenda kwenye historia na usasishe baada ya mpokeaji kudai tokeni.';
|
||||
|
||||
@override
|
||||
String get p2pkExperimentalShort => 'Majaribio';
|
||||
@@ -1075,13 +1136,15 @@ class L10nSw extends L10n {
|
||||
String get p2pkDeleteTitle => 'Futa ufunguo';
|
||||
|
||||
@override
|
||||
String get p2pkDeleteConfirm => 'Futa ufunguo huu? Hutaweza kupokea tokeni zilizofungwa kwake.';
|
||||
String get p2pkDeleteConfirm =>
|
||||
'Futa ufunguo huu? Hutaweza kupokea tokeni zilizofungwa kwake.';
|
||||
|
||||
@override
|
||||
String get p2pkRequiresConnection => 'P2PK inahitaji muunganisho kwa mint';
|
||||
|
||||
@override
|
||||
String get p2pkErrorMaxKeysReached => 'Idadi ya juu ya funguo zilizoingizwa imefikiwa (10)';
|
||||
String get p2pkErrorMaxKeysReached =>
|
||||
'Idadi ya juu ya funguo zilizoingizwa imefikiwa (10)';
|
||||
|
||||
@override
|
||||
String get p2pkErrorInvalidNsec => 'nsec batili';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@@ -315,6 +315,43 @@ class L10nZh extends L10n {
|
||||
@override
|
||||
String get tokenCopiedToClipboard => '代币已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get copyAsEmoji => '复制为表情符号';
|
||||
|
||||
@override
|
||||
String get emojiCopiedToClipboard => '代币已复制为表情符号 🥜';
|
||||
|
||||
@override
|
||||
String get peanutDecodeError => '无法解码表情符号代币。可能已损坏。';
|
||||
|
||||
@override
|
||||
String get nfcWrite => '写入NFC标签';
|
||||
|
||||
@override
|
||||
String get nfcRead => '读取NFC标签';
|
||||
|
||||
@override
|
||||
String get nfcHoldNear => '将设备靠近NFC标签...';
|
||||
|
||||
@override
|
||||
String get nfcWriteSuccess => '代币已写入NFC标签';
|
||||
|
||||
@override
|
||||
String nfcWriteError(String error) {
|
||||
return 'NFC写入错误:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String nfcReadError(String error) {
|
||||
return 'NFC读取错误:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nfcDisabled => 'NFC已禁用,请在设置中启用。';
|
||||
|
||||
@override
|
||||
String get nfcUnsupported => '此设备不支持NFC';
|
||||
|
||||
@override
|
||||
String get amountToDeposit => '存入金额:';
|
||||
|
||||
@@ -665,7 +702,7 @@ class L10nZh extends L10n {
|
||||
String get close => '关闭';
|
||||
|
||||
@override
|
||||
String get aboutDescription => '具有古巴基因的 Cashu 钱包,面向全世界。La Chispa 的兄弟项目。';
|
||||
String get aboutDescription => '具有古巴基因的 Cashu 钱包,面向全世界。LaChispa 的兄弟项目。';
|
||||
|
||||
@override
|
||||
String get couldNotOpenLink => '无法打开链接';
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.",
|
||||
"tokenCopiedToClipboard": "Token copiado para área de transferência",
|
||||
"copyAsEmoji": "Copiar como emoji",
|
||||
"emojiCopiedToClipboard": "Token copiado como emoji 🥜",
|
||||
"peanutDecodeError": "Não foi possível decodificar o token emoji. Pode estar corrompido.",
|
||||
|
||||
"nfcWrite": "Escrever em tag NFC",
|
||||
"nfcRead": "Ler tag NFC",
|
||||
"nfcHoldNear": "Aproxime o dispositivo da tag NFC...",
|
||||
"nfcWriteSuccess": "Token escrito na tag NFC",
|
||||
"nfcWriteError": "Erro NFC ao escrever: {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "Erro NFC ao ler: {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC está desativado. Ative nas Configurações.",
|
||||
"nfcUnsupported": "Este dispositivo não suporta NFC",
|
||||
|
||||
"amountToDeposit": "Valor a depositar:",
|
||||
"descriptionOptional": "Descrição (opcional):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "Fechar",
|
||||
"aboutDescription": "Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de La Chispa.",
|
||||
"aboutDescription": "Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de LaChispa.",
|
||||
"couldNotOpenLink": "Não foi possível abrir o link",
|
||||
|
||||
"deleteWalletQuestion": "Apagar wallet?",
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.",
|
||||
"tokenCopiedToClipboard": "Токен скопирован в буфер обмена",
|
||||
"copyAsEmoji": "Скопировать как эмодзи",
|
||||
"emojiCopiedToClipboard": "Токен скопирован как эмодзи 🥜",
|
||||
"peanutDecodeError": "Не удалось декодировать эмодзи-токен. Возможно, он повреждён.",
|
||||
|
||||
"nfcWrite": "Записать на NFC-метку",
|
||||
"nfcRead": "Читать NFC-метку",
|
||||
"nfcHoldNear": "Поднесите устройство к NFC-метке...",
|
||||
"nfcWriteSuccess": "Токен записан на NFC-метку",
|
||||
"nfcWriteError": "Ошибка записи NFC: {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "Ошибка чтения NFC: {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC отключён. Включите в Настройках.",
|
||||
"nfcUnsupported": "Это устройство не поддерживает NFC",
|
||||
|
||||
"amountToDeposit": "Сумма для пополнения:",
|
||||
"descriptionOptional": "Описание (необязательно):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "Закрыть",
|
||||
"aboutDescription": "Cashu кошелёк с кубинской ДНК для всего мира. Брат La Chispa.",
|
||||
"aboutDescription": "Cashu кошелёк с кубинской ДНК для всего мира. Брат LaChispa.",
|
||||
"couldNotOpenLink": "Не удалось открыть ссылку",
|
||||
|
||||
"deleteWalletQuestion": "Удалить кошелёк?",
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.",
|
||||
"tokenCopiedToClipboard": "Tokeni imenakiliwa kwenye ubao",
|
||||
"copyAsEmoji": "Nakili kama emoji",
|
||||
"emojiCopiedToClipboard": "Tokeni imenakiliwa kama emoji 🥜",
|
||||
"peanutDecodeError": "Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.",
|
||||
|
||||
"nfcWrite": "Andika kwenye NFC tag",
|
||||
"nfcRead": "Soma NFC tag",
|
||||
"nfcHoldNear": "Karibia kifaa kwenye NFC tag...",
|
||||
"nfcWriteSuccess": "Tokeni imeandikwa kwenye NFC tag",
|
||||
"nfcWriteError": "Hitilafu ya NFC kuandika: {error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "Hitilafu ya NFC kusoma: {error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC imezimwa. Iwashe katika Mipangilio.",
|
||||
"nfcUnsupported": "Kifaa hiki hakitumii NFC",
|
||||
|
||||
"amountToDeposit": "Kiasi cha kuweka:",
|
||||
"descriptionOptional": "Maelezo (si lazima):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "Funga",
|
||||
"aboutDescription": "Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa La Chispa.",
|
||||
"aboutDescription": "Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa LaChispa.",
|
||||
"couldNotOpenLink": "Haikuweza kufungua kiungo",
|
||||
|
||||
"deleteWalletQuestion": "Futa pochi?",
|
||||
|
||||
+15
-1
@@ -137,6 +137,20 @@
|
||||
},
|
||||
"keepTokenWarning": "请保留此代币直到收款方领取。如果丢失,资金将无法找回。",
|
||||
"tokenCopiedToClipboard": "代币已复制到剪贴板",
|
||||
"copyAsEmoji": "复制为表情符号",
|
||||
"emojiCopiedToClipboard": "代币已复制为表情符号 🥜",
|
||||
"peanutDecodeError": "无法解码表情符号代币。可能已损坏。",
|
||||
|
||||
"nfcWrite": "写入NFC标签",
|
||||
"nfcRead": "读取NFC标签",
|
||||
"nfcHoldNear": "将设备靠近NFC标签...",
|
||||
"nfcWriteSuccess": "代币已写入NFC标签",
|
||||
"nfcWriteError": "NFC写入错误:{error}",
|
||||
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcReadError": "NFC读取错误:{error}",
|
||||
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
|
||||
"nfcDisabled": "NFC已禁用,请在设置中启用。",
|
||||
"nfcUnsupported": "此设备不支持NFC",
|
||||
|
||||
"amountToDeposit": "存入金额:",
|
||||
"descriptionOptional": "描述(可选):",
|
||||
@@ -296,7 +310,7 @@
|
||||
}
|
||||
},
|
||||
"close": "关闭",
|
||||
"aboutDescription": "具有古巴基因的 Cashu 钱包,面向全世界。La Chispa 的兄弟项目。",
|
||||
"aboutDescription": "具有古巴基因的 Cashu 钱包,面向全世界。LaChispa 的兄弟项目。",
|
||||
"couldNotOpenLink": "无法打开链接",
|
||||
|
||||
"deleteWalletQuestion": "删除钱包?",
|
||||
|
||||
@@ -1393,12 +1393,44 @@ class WalletProvider extends ChangeNotifier {
|
||||
/// Llamar en background al iniciar la app.
|
||||
/// También vincula transacciones incoming sin metadata con invoices pendientes.
|
||||
Future<void> checkPendingTransactions() async {
|
||||
for (final wallet in _wallets.values) {
|
||||
try {
|
||||
await wallet.checkPendingTransactions();
|
||||
} catch (e) {
|
||||
// Silencioso - puede fallar offline
|
||||
debugPrint('Check pending failed: $e');
|
||||
// Iterar todos los mints y todas sus unidades, no solo los wallets
|
||||
// ya instanciados. initialize() solo precarga units.first por mint,
|
||||
// así que quotes/sagas/melts en otras unidades se perderían.
|
||||
for (final entry in _mintUnits.entries) {
|
||||
for (final unit in entry.value) {
|
||||
try {
|
||||
final wallet = await getWallet(entry.key, unit);
|
||||
|
||||
try {
|
||||
// Reclamar quotes pagados pero no emitidos (Lightning → proofs)
|
||||
await wallet.checkAllMintQuotes();
|
||||
} catch (e) {
|
||||
debugPrint('Check mint quotes failed: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
// Recuperar sagas incompletas (swaps/mints/melts interrumpidos)
|
||||
await wallet.recoverIncompleteSagas();
|
||||
} catch (e) {
|
||||
debugPrint('Recover incomplete sagas failed: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
// Finalizar retiros Lightning que quedaron a medias
|
||||
await wallet.finalizePendingMelts();
|
||||
} catch (e) {
|
||||
debugPrint('Finalize pending melts failed: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
await wallet.checkPendingTransactions();
|
||||
} catch (e) {
|
||||
// Silencioso - puede fallar offline
|
||||
debugPrint('Check pending failed: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error getting wallet ${entry.key}:$unit: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../core/utils/incoming_data_parser.dart' hide TokenInfo;
|
||||
import '../../core/utils/nostr_utils.dart';
|
||||
import '../../core/utils/peanut_codec.dart';
|
||||
import '../../core/services/nfc_service.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
@@ -46,9 +48,14 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
bool _showManualKey = false; // Toggle para mostrar/ocultar nsec
|
||||
String? _manualKeyError;
|
||||
|
||||
// NFC
|
||||
NfcState _nfcState = NfcState.unsupported;
|
||||
bool _nfcReading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkNfc();
|
||||
// Pre-cargar token inicial si existe
|
||||
if (widget.initialToken != null && widget.initialToken!.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -60,11 +67,19 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_nfcReading) NfcService.stopRead();
|
||||
_tokenController.dispose();
|
||||
_manualKeyController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _checkNfc() async {
|
||||
final state = await NfcService.checkState();
|
||||
if (mounted) {
|
||||
setState(() => _nfcState = state);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GradientBackground(
|
||||
@@ -245,6 +260,9 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
// Botón pegar (expandido)
|
||||
Expanded(child: _buildPasteButton()),
|
||||
const SizedBox(width: 12),
|
||||
// Botón NFC (siempre visible)
|
||||
_buildNfcButton(),
|
||||
const SizedBox(width: 12),
|
||||
// Botón escanear QR
|
||||
_buildScanButton(),
|
||||
],
|
||||
@@ -291,6 +309,134 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNfcButton() {
|
||||
final isNfcEnabled = _nfcState == NfcState.enabled;
|
||||
final isNfcUnsupported = _nfcState == NfcState.unsupported;
|
||||
|
||||
return Tooltip(
|
||||
message: isNfcUnsupported
|
||||
? L10n.of(context)!.nfcUnsupported
|
||||
: isNfcEnabled
|
||||
? L10n.of(context)!.nfcRead
|
||||
: L10n.of(context)!.nfcDisabled,
|
||||
child: GestureDetector(
|
||||
onTap: _toggleNfcRead,
|
||||
child: Opacity(
|
||||
opacity: isNfcUnsupported ? 0.3 : 1.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingSmall + 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _nfcReading
|
||||
? AppColors.primaryAction.withValues(alpha: 0.3)
|
||||
: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _nfcReading
|
||||
? AppColors.primaryAction
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.nfc,
|
||||
color: _nfcReading
|
||||
? AppColors.primaryAction
|
||||
: isNfcEnabled
|
||||
? AppColors.textSecondary
|
||||
: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleNfcRead() async {
|
||||
// Cancel active session first (before state checks, in case NFC was
|
||||
// disabled while a session was running)
|
||||
if (_nfcReading) {
|
||||
NfcService.stopRead();
|
||||
setState(() => _nfcReading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-check state in case user toggled NFC in settings
|
||||
final currentState = await NfcService.checkState();
|
||||
if (mounted) setState(() => _nfcState = currentState);
|
||||
|
||||
if (currentState == NfcState.unsupported) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcUnsupported),
|
||||
backgroundColor: AppColors.textSecondary,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState == NfcState.disabled) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcDisabled),
|
||||
backgroundColor: AppColors.warning,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _nfcReading = true);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcHoldNear),
|
||||
backgroundColor: AppColors.primaryAction,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await NfcService.startRead(
|
||||
onTokenRead: (token) {
|
||||
if (!mounted) return;
|
||||
setState(() => _nfcReading = false);
|
||||
_tokenController.text = token;
|
||||
_onTokenChanged(token);
|
||||
},
|
||||
onError: (error) {
|
||||
if (!mounted) return;
|
||||
setState(() => _nfcReading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcReadError(error)),
|
||||
backgroundColor: AppColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _nfcReading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcReadError(e.toString())),
|
||||
backgroundColor: AppColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildScanButton() {
|
||||
return GestureDetector(
|
||||
onTap: _openScanner,
|
||||
@@ -779,9 +925,29 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
|
||||
Future<void> _pasteFromClipboard() async {
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (!mounted) return;
|
||||
if (clipboardData?.text != null) {
|
||||
_tokenController.text = clipboardData!.text!.trim();
|
||||
_onTokenChanged(clipboardData.text!.trim());
|
||||
var text = clipboardData!.text!.trim();
|
||||
|
||||
// Auto-decode peanut emoji format
|
||||
if (PeanutCodec.isPeanut(text)) {
|
||||
final decoded = PeanutCodec.decode(text);
|
||||
if (decoded != null) {
|
||||
text = decoded;
|
||||
} else {
|
||||
// Malformed peanut: clear field and show specific error
|
||||
_tokenController.clear();
|
||||
setState(() {
|
||||
_isValidToken = false;
|
||||
_tokenInfo = null;
|
||||
_errorMessage = L10n.of(context)!.peanutDecodeError;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_tokenController.text = text;
|
||||
_onTokenChanged(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +955,28 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final p2pkProvider = context.read<P2PKProvider>();
|
||||
|
||||
// Auto-decode peanut emoji format
|
||||
var tokenValue = value.trim();
|
||||
if (PeanutCodec.isPeanut(tokenValue)) {
|
||||
final decoded = PeanutCodec.decode(tokenValue);
|
||||
if (decoded != null) {
|
||||
tokenValue = decoded;
|
||||
// Update the text field with the decoded token
|
||||
_tokenController.text = tokenValue;
|
||||
_tokenController.selection = TextSelection.collapsed(
|
||||
offset: tokenValue.length,
|
||||
);
|
||||
} else {
|
||||
// Malformed peanut: show specific error
|
||||
setState(() {
|
||||
_isValidToken = false;
|
||||
_tokenInfo = null;
|
||||
_errorMessage = L10n.of(context)!.peanutDecodeError;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_manualKeyError = null;
|
||||
@@ -799,24 +987,24 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
_lockedToPubkeyHex = null;
|
||||
_matchingKeyLabel = null;
|
||||
|
||||
if (value.isEmpty) {
|
||||
if (tokenValue.isEmpty) {
|
||||
_isValidToken = false;
|
||||
_tokenInfo = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Parsear token real con cdk-flutter
|
||||
final tokenInfo = walletProvider.parseToken(value.trim());
|
||||
final tokenInfo = walletProvider.parseToken(tokenValue);
|
||||
|
||||
if (tokenInfo != null) {
|
||||
_isValidToken = true;
|
||||
_tokenInfo = tokenInfo;
|
||||
|
||||
// Detectar P2PK
|
||||
_isP2PKLocked = p2pkProvider.isTokenLocked(value.trim());
|
||||
_isP2PKLocked = p2pkProvider.isTokenLocked(tokenValue);
|
||||
if (_isP2PKLocked) {
|
||||
_lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(value.trim());
|
||||
_isLockedToUs = p2pkProvider.isTokenLockedToUs(value.trim());
|
||||
_lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(tokenValue);
|
||||
_isLockedToUs = p2pkProvider.isTokenLockedToUs(tokenValue);
|
||||
|
||||
// Si es nuestra, buscar el label de la clave
|
||||
if (_isLockedToUs && _lockedToPubkeyHex != null) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../core/utils/peanut_codec.dart';
|
||||
import '../../core/services/nfc_service.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
@@ -44,18 +46,31 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
int _currentInterval = _intervalMedium;
|
||||
String _speedLabel = 'M';
|
||||
|
||||
// NFC
|
||||
NfcState _nfcState = NfcState.unsupported;
|
||||
bool _nfcWriting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_encodeTokenToUR();
|
||||
_checkNfc();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationTimer?.cancel();
|
||||
if (_nfcWriting) NfcService.stopEmulating();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _checkNfc() async {
|
||||
final state = await NfcService.checkState();
|
||||
if (mounted) {
|
||||
setState(() => _nfcState = state);
|
||||
}
|
||||
}
|
||||
|
||||
void _encodeTokenToUR() {
|
||||
// Parsear el token string a objeto Token de cdk-flutter
|
||||
final token = cdk.Token.parse(encoded: widget.token);
|
||||
@@ -155,9 +170,14 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Botones copiar y compartir
|
||||
// Botones principales: copiar y compartir
|
||||
_buildActionButtons(context),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
// Botones secundarios: bean (peanut) y NFC
|
||||
_buildSecondaryActions(context),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingLarge),
|
||||
|
||||
// Advertencia
|
||||
@@ -450,6 +470,75 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSecondaryActions(BuildContext context) {
|
||||
final isNfcEnabled = _nfcState == NfcState.enabled;
|
||||
final isNfcUnsupported = _nfcState == NfcState.unsupported;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Copiar como emoji (peanut encoding)
|
||||
Tooltip(
|
||||
message: L10n.of(context)!.copyAsEmoji,
|
||||
child: GestureDetector(
|
||||
onTap: () => _copyPeanut(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.07),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(LucideIcons.bean, color: AppColors.primaryAction, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// NFC - siempre visible con 3 estados
|
||||
Tooltip(
|
||||
message: isNfcUnsupported
|
||||
? L10n.of(context)!.nfcUnsupported
|
||||
: isNfcEnabled
|
||||
? L10n.of(context)!.nfcWrite
|
||||
: L10n.of(context)!.nfcDisabled,
|
||||
child: GestureDetector(
|
||||
onTap: () => _writeNfc(context),
|
||||
child: Opacity(
|
||||
opacity: isNfcUnsupported ? 0.3 : 1.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: _nfcWriting
|
||||
? AppColors.primaryAction.withValues(alpha: 0.3)
|
||||
: Colors.white.withValues(alpha: 0.07),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _nfcWriting
|
||||
? AppColors.primaryAction
|
||||
: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.nfc,
|
||||
color: _nfcWriting
|
||||
? AppColors.primaryAction
|
||||
: isNfcEnabled
|
||||
? AppColors.textSecondary
|
||||
: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWarning() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
@@ -492,6 +581,19 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _copyPeanut(BuildContext context) {
|
||||
final peanut = PeanutCodec.encode(widget.token);
|
||||
Clipboard.setData(ClipboardData(text: peanut));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.emojiCopiedToClipboard),
|
||||
backgroundColor: AppColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _shareToken(BuildContext context) async {
|
||||
final memo = widget.memo != null && widget.memo!.isNotEmpty
|
||||
? '\n"${widget.memo}"'
|
||||
@@ -508,6 +610,68 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _writeNfc(BuildContext context) async {
|
||||
// Toggle off: stop emulating
|
||||
if (_nfcWriting) {
|
||||
await NfcService.stopEmulating();
|
||||
setState(() => _nfcWriting = false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-check state in case user toggled NFC in settings
|
||||
final currentState = await NfcService.checkState();
|
||||
if (mounted) setState(() => _nfcState = currentState);
|
||||
|
||||
if (currentState == NfcState.unsupported) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcUnsupported),
|
||||
backgroundColor: AppColors.textSecondary,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState == NfcState.disabled) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcDisabled),
|
||||
backgroundColor: AppColors.warning,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await NfcService.startEmulating(widget.token);
|
||||
setState(() => _nfcWriting = true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcHoldNear),
|
||||
backgroundColor: AppColors.primaryAction,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _nfcWriting = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.nfcWriteError(e.toString())),
|
||||
backgroundColor: AppColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _goToHome(BuildContext context) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
@@ -24,6 +25,25 @@ class SettingsScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
String _appVersion = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadVersion();
|
||||
}
|
||||
|
||||
Future<void> _loadVersion() async {
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
if (!mounted) return;
|
||||
setState(() => _appVersion = info.version);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _appVersion = '—');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context)!;
|
||||
@@ -124,7 +144,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_buildInfoTile(
|
||||
icon: LucideIcons.tag,
|
||||
title: l10n.version,
|
||||
subtitle: '0.0.1',
|
||||
subtitle: _appVersion,
|
||||
),
|
||||
_buildSettingTile(
|
||||
icon: LucideIcons.info,
|
||||
@@ -620,7 +640,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'v0.0.1',
|
||||
'v$_appVersion',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
@@ -638,43 +658,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
L10n.of(context)!.aboutDescription,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.bitcoin,
|
||||
color: AppColors.secondaryAction,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Cuba Bitcoin',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.secondaryAction,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildAboutDescription(context),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -693,6 +677,72 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutDescription(BuildContext context) {
|
||||
final description = L10n.of(context)!.aboutDescription;
|
||||
const keyword = 'LaChispa';
|
||||
final index = description.indexOf(keyword);
|
||||
if (index == -1) {
|
||||
return Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
final before = description.substring(0, index);
|
||||
final after = description.substring(index + keyword.length);
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: before),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: GestureDetector(
|
||||
onTap: () => _openLaChispa(context),
|
||||
child: Text(
|
||||
keyword,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.secondaryAction,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColors.secondaryAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(text: after),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openLaChispa(BuildContext context) async {
|
||||
final url = Uri.parse('https://app.lachispa.me');
|
||||
try {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.couldNotOpenLink),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 4. Abrir GitHub
|
||||
Future<void> _openGitHub() async {
|
||||
final url = Uri.parse('https://github.com/Forte11Cuba/elcaju');
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||
|
||||
// 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 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'frb_generated.dart';
|
||||
import 'frb_generated.io.dart'
|
||||
if (dart.library.js_interop) 'frb_generated.web.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
|
||||
/// Main entrypoint of the Rust API
|
||||
class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
@internal
|
||||
static final instance = RustLib._();
|
||||
|
||||
RustLib._();
|
||||
|
||||
/// Initialize flutter_rust_bridge
|
||||
static Future<void> init({
|
||||
RustLibApi? api,
|
||||
BaseHandler? handler,
|
||||
ExternalLibrary? externalLibrary,
|
||||
bool forceSameCodegenVersion = true,
|
||||
}) async {
|
||||
await instance.initImpl(
|
||||
api: api,
|
||||
handler: handler,
|
||||
externalLibrary: externalLibrary,
|
||||
forceSameCodegenVersion: forceSameCodegenVersion,
|
||||
);
|
||||
}
|
||||
|
||||
/// Initialize flutter_rust_bridge in mock mode.
|
||||
/// No libraries for FFI are loaded.
|
||||
static void initMock({required RustLibApi api}) {
|
||||
instance.initMockImpl(api: api);
|
||||
}
|
||||
|
||||
/// Dispose flutter_rust_bridge
|
||||
///
|
||||
/// The call to this function is optional, since flutter_rust_bridge (and everything else)
|
||||
/// is automatically disposed when the app stops.
|
||||
static void dispose() => instance.disposeImpl();
|
||||
|
||||
@override
|
||||
ApiImplConstructor<RustLibApiImpl, RustLibWire> get apiImplConstructor =>
|
||||
RustLibApiImpl.new;
|
||||
|
||||
@override
|
||||
WireConstructor<RustLibWire> get wireConstructor =>
|
||||
RustLibWire.fromExternalLibrary;
|
||||
|
||||
@override
|
||||
Future<void> executeRustInitializers() async {}
|
||||
|
||||
@override
|
||||
ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig =>
|
||||
kDefaultExternalLibraryLoaderConfig;
|
||||
|
||||
@override
|
||||
String get codegenVersion => '2.11.1';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -291292710;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
stem: 'elcaju_core',
|
||||
ioDirectory: 'rust/target/release/',
|
||||
webPrefix: 'pkg/',
|
||||
);
|
||||
}
|
||||
|
||||
abstract class RustLibApi extends BaseApi {}
|
||||
|
||||
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
RustLibApiImpl({
|
||||
required super.handler,
|
||||
required super.wire,
|
||||
required super.generalizedFrbRustBinding,
|
||||
required super.portManager,
|
||||
});
|
||||
|
||||
@protected
|
||||
int sse_decode_i_32(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return deserializer.buffer.getInt32();
|
||||
}
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return deserializer.buffer.getUint8() != 0;
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_i_32(int self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
serializer.buffer.putInt32(self);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
serializer.buffer.putUint8(self ? 1 : 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||
|
||||
// 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 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||
|
||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
RustLibApiImplPlatform({
|
||||
required super.handler,
|
||||
required super.wire,
|
||||
required super.generalizedFrbRustBinding,
|
||||
required super.portManager,
|
||||
});
|
||||
|
||||
@protected
|
||||
int sse_decode_i_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
}
|
||||
|
||||
// Section: wire_class
|
||||
|
||||
class RustLibWire implements BaseWire {
|
||||
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
|
||||
RustLibWire(lib.ffiDynamicLibrary);
|
||||
|
||||
/// Holds the symbol lookup function.
|
||||
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||
_lookup;
|
||||
|
||||
/// The symbols are looked up in [dynamicLibrary].
|
||||
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
|
||||
: _lookup = dynamicLibrary.lookup;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||
|
||||
// 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
|
||||
|
||||
// Static analysis wrongly picks the IO variant, thus ignore this
|
||||
// ignore_for_file: argument_type_not_assignable
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||
|
||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
RustLibApiImplPlatform({
|
||||
required super.handler,
|
||||
required super.wire,
|
||||
required super.generalizedFrbRustBinding,
|
||||
required super.portManager,
|
||||
});
|
||||
|
||||
@protected
|
||||
int sse_decode_i_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
bool sse_decode_bool(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||
}
|
||||
|
||||
// Section: wire_class
|
||||
|
||||
class RustLibWire implements BaseWire {
|
||||
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
|
||||
}
|
||||
|
||||
@JS('wasm_bindgen')
|
||||
external RustLibWasmModule get wasmModule;
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
extension type RustLibWasmModule._(JSObject _) implements JSObject {}
|
||||
@@ -7,6 +7,7 @@ import Foundation
|
||||
|
||||
import flutter_secure_storage_macos
|
||||
import mobile_scanner
|
||||
import package_info_plus
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
@@ -16,6 +17,7 @@ import url_launcher_macos
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
|
||||
+66
-34
@@ -94,10 +94,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -118,18 +118,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -174,10 +174,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -237,7 +237,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
flutter_rust_bridge:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_rust_bridge
|
||||
sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
|
||||
@@ -370,10 +370,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -394,26 +394,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -434,26 +434,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -470,6 +470,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.3"
|
||||
ndef_record:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ndef_record
|
||||
sha256: "210ffb12284961cab9e44b99462143316d9a20cd992581170706069ef77d74a6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -478,14 +486,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nfc_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: nfc_manager
|
||||
sha256: "24c78b0e5702da53e7f8794d073624c0bee7cd99924f257cbd11f5d1c5866879"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -743,18 +775,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -783,10 +815,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.10"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -879,10 +911,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -932,5 +964,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
dart: ">=3.11.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
||||
+7
-1
@@ -4,7 +4,7 @@ publish_to: 'none'
|
||||
version: 0.1.0+2
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
sdk: ^3.11.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
@@ -56,8 +56,14 @@ dependencies:
|
||||
# UUID generator for pending tokens
|
||||
uuid: ^4.3.3
|
||||
|
||||
# NFC for token sharing
|
||||
nfc_manager: ^4.1.1
|
||||
ndef_record: ^1.4.2
|
||||
|
||||
# BIP32 for NIP-06 key derivation (P2PK)
|
||||
bip32: ^2.0.0
|
||||
package_info_plus: ^9.0.0
|
||||
flutter_rust_bridge: 2.11.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Generated
+3859
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
[package]
|
||||
name = "elcaju_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
# Cashu — acceso directo, sin intermediarios
|
||||
cdk = { version = "0.15.1", default-features = false, features = ["wallet"] }
|
||||
cdk-common = { version = "0.15.1", default-features = false }
|
||||
cdk-sqlite = { version = "0.15.1", default-features = false, features = ["wallet"] }
|
||||
|
||||
# Bridge Flutter <-> Rust
|
||||
flutter_rust_bridge = { version = "=2.11.1", default-features = false, features = [
|
||||
"anyhow",
|
||||
"dart-opaque",
|
||||
"portable-atomic",
|
||||
"rust-async",
|
||||
"thread-pool",
|
||||
"wasm-start",
|
||||
] }
|
||||
|
||||
# QR multiframe (bc-ur)
|
||||
bc-ur = { version = "0.12.0", default-features = false }
|
||||
|
||||
# Mnemonic
|
||||
bip39 = { version = "2.1", default-features = false, features = ["std"] }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", default-features = false }
|
||||
|
||||
# HTTP (para ping_mint, fetch_keysets)
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"deflate",
|
||||
"gzip",
|
||||
"json",
|
||||
"rustls-tls",
|
||||
"rustls-tls-native-roots",
|
||||
] }
|
||||
|
||||
# Serialización
|
||||
serde_json = { version = "1.0", default-features = false, features = ["std"] }
|
||||
|
||||
# Logging
|
||||
log = { version = "0.4", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.15"
|
||||
|
||||
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
|
||||
oslog = "0.2.0"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
||||
@@ -0,0 +1,45 @@
|
||||
use std::fmt;
|
||||
|
||||
/// Errores unificados de elcaju_core
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Error del protocolo Cashu (cdk)
|
||||
Cdk(String),
|
||||
/// Error de base de datos (SQLite)
|
||||
Database(String),
|
||||
/// Input inválido del usuario
|
||||
InvalidInput,
|
||||
/// Error de red (HTTP, conexión)
|
||||
Network(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Cdk(msg) => write!(f, "Cashu error: {msg}"),
|
||||
Error::Database(msg) => write!(f, "Database error: {msg}"),
|
||||
Error::InvalidInput => write!(f, "Invalid input"),
|
||||
Error::Network(msg) => write!(f, "Network error: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<cdk::error::Error> for Error {
|
||||
fn from(e: cdk::error::Error) -> Self {
|
||||
Error::Cdk(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Error::Network(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Error::Cdk(e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod error;
|
||||
@@ -0,0 +1,146 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
unused,
|
||||
non_snake_case,
|
||||
clippy::needless_return,
|
||||
clippy::redundant_closure_call,
|
||||
clippy::redundant_closure,
|
||||
clippy::useless_conversion,
|
||||
clippy::unit_arg,
|
||||
clippy::unused_unit,
|
||||
clippy::double_parens,
|
||||
clippy::let_and_return,
|
||||
clippy::too_many_arguments,
|
||||
clippy::match_single_binding,
|
||||
clippy::clone_on_copy,
|
||||
clippy::let_unit_value,
|
||||
clippy::deref_addrof,
|
||||
clippy::explicit_auto_deref,
|
||||
clippy::borrow_deref_ref,
|
||||
clippy::needless_borrow
|
||||
)]
|
||||
|
||||
// Section: imports
|
||||
|
||||
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
|
||||
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||
|
||||
// Section: boilerplate
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_stream_sink_codec = SseCodec,
|
||||
default_rust_opaque = RustOpaqueMoi,
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -291292710;
|
||||
|
||||
// Section: executor
|
||||
|
||||
flutter_rust_bridge::frb_generated_default_handler!();
|
||||
|
||||
// Section: dart2rust
|
||||
|
||||
impl SseDecode for i32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
deserializer.cursor.read_i32::<NativeEndian>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
deserializer.cursor.read_u8().unwrap() != 0
|
||||
}
|
||||
}
|
||||
|
||||
fn pde_ffi_dispatcher_primary_impl(
|
||||
func_id: i32,
|
||||
port: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len: i32,
|
||||
data_len: i32,
|
||||
) {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pde_ffi_dispatcher_sync_impl(
|
||||
func_id: i32,
|
||||
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len: i32,
|
||||
data_len: i32,
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Section: rust2dart
|
||||
|
||||
impl SseEncode for i32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
serializer.cursor.write_i32::<NativeEndian>(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
serializer.cursor.write_u8(self as _).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod io {
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||
|
||||
// Section: imports
|
||||
|
||||
use super::*;
|
||||
use flutter_rust_bridge::for_generated::byteorder::{
|
||||
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||
};
|
||||
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||
|
||||
// Section: boilerplate
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate_io!();
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub use io::*;
|
||||
|
||||
/// cbindgen:ignore
|
||||
#[cfg(target_family = "wasm")]
|
||||
mod web {
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||
|
||||
// Section: imports
|
||||
|
||||
use super::*;
|
||||
use flutter_rust_bridge::for_generated::byteorder::{
|
||||
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||
};
|
||||
use flutter_rust_bridge::for_generated::wasm_bindgen;
|
||||
use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*;
|
||||
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||
|
||||
// Section: boilerplate
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate_web!();
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub use web::*;
|
||||
@@ -0,0 +1,2 @@
|
||||
mod frb_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */
|
||||
pub mod api;
|
||||
@@ -0,0 +1,49 @@
|
||||
// Android NDK build integration for elcaju_core Rust crate.
|
||||
|
||||
group 'com.elcaju.rust_builder'
|
||||
version '1.0'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
if (project.android.hasProperty("namespace")) {
|
||||
namespace 'com.elcaju.rust_builder'
|
||||
}
|
||||
|
||||
compileSdkVersion 33
|
||||
|
||||
ndkVersion "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../cargokit/gradle/plugin.gradle"
|
||||
cargokit {
|
||||
manifestDir = "../../rust"
|
||||
libname = "elcaju_core"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
target
|
||||
.dart_tool
|
||||
*.iml
|
||||
!pubspec.lock
|
||||
@@ -0,0 +1,42 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
Copyright 2022 Matej Knopp
|
||||
|
||||
================================================================================
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
|
||||
APACHE LICENSE, VERSION 2.0
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
Experimental repository to provide glue for seamlessly integrating cargo build
|
||||
with flutter plugins and packages.
|
||||
|
||||
See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/
|
||||
for a tutorial on how to use Cargokit.
|
||||
|
||||
Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin.
|
||||
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||
BASEDIR=$(cd "$BASEDIR" ; pwd -P)
|
||||
|
||||
# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project
|
||||
NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"`
|
||||
|
||||
export PATH=${NEW_PATH%?} # remove trailing :
|
||||
|
||||
if [ -n "$CARGOKIT_DEBUG" ]; then
|
||||
env | grep -E '^(CARGOKIT|FLUTTER|PLATFORM_NAME|ARCHS|CONFIGURATION|PODS_)='
|
||||
fi
|
||||
|
||||
# Platform name (macosx, iphoneos, iphonesimulator)
|
||||
export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME
|
||||
|
||||
# Active architectures (arm64, armv7, x86_64), space separated.
|
||||
export CARGOKIT_DARWIN_ARCHS=$ARCHS
|
||||
|
||||
# Current build configuration (Debug, Release)
|
||||
export CARGOKIT_CONFIGURATION=$CONFIGURATION
|
||||
|
||||
# Path to directory containing Cargo.toml.
|
||||
export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1
|
||||
|
||||
# Temporary directory for build artifacts.
|
||||
export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR
|
||||
|
||||
# Output directory for final artifacts.
|
||||
export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME
|
||||
|
||||
# Directory to store built tool artifacts.
|
||||
export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool
|
||||
|
||||
# Directory inside root project. Not necessarily the top level directory of root project.
|
||||
export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT
|
||||
|
||||
FLUTTER_EXPORT_BUILD_ENVIRONMENT=(
|
||||
"$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS
|
||||
"$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS
|
||||
)
|
||||
|
||||
for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}"
|
||||
do
|
||||
if [[ -f "$path" ]]; then
|
||||
source "$path"
|
||||
fi
|
||||
done
|
||||
|
||||
bash "$BASEDIR/run_build_tool.sh" build-pod "$@"
|
||||
|
||||
# Make a symlink from built framework to phony file, which will be used as input to
|
||||
# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate
|
||||
# attribute on custom build phase)
|
||||
ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony"
|
||||
ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out"
|
||||
@@ -0,0 +1,5 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
A sample command-line application with an entrypoint in `bin/`, library code
|
||||
in `lib/`, and example unit test in `test/`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# This is copied from Cargokit (which is the official way to use it currently)
|
||||
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
linter:
|
||||
rules:
|
||||
- prefer_relative_imports
|
||||
- directives_ordering
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -0,0 +1,8 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||
|
||||
void main(List<String> arguments) {
|
||||
build_tool.runMain(arguments);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'src/build_tool.dart' as build_tool;
|
||||
|
||||
Future<void> runMain(List<String> args) async {
|
||||
return build_tool.runMain(args);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:version/version.dart';
|
||||
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class AndroidEnvironment {
|
||||
AndroidEnvironment({
|
||||
required this.sdkPath,
|
||||
required this.ndkVersion,
|
||||
required this.minSdkVersion,
|
||||
required this.targetTempDir,
|
||||
required this.target,
|
||||
});
|
||||
|
||||
static void clangLinkerWrapper(List<String> args) {
|
||||
final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG'];
|
||||
if (clang == null) {
|
||||
throw Exception(
|
||||
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var");
|
||||
}
|
||||
final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET'];
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var");
|
||||
}
|
||||
|
||||
runCommand(clang, [
|
||||
target,
|
||||
...args,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Full path to Android SDK.
|
||||
final String sdkPath;
|
||||
|
||||
/// Full version of Android NDK.
|
||||
final String ndkVersion;
|
||||
|
||||
/// Minimum supported SDK version.
|
||||
final int minSdkVersion;
|
||||
|
||||
/// Target directory for build artifacts.
|
||||
final String targetTempDir;
|
||||
|
||||
/// Target being built.
|
||||
final Target target;
|
||||
|
||||
bool ndkIsInstalled() {
|
||||
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||
final ndkPackageXml = File(path.join(ndkPath, 'package.xml'));
|
||||
return ndkPackageXml.existsSync();
|
||||
}
|
||||
|
||||
void installNdk({
|
||||
required String javaHome,
|
||||
}) {
|
||||
final sdkManagerExtension = Platform.isWindows ? '.bat' : '';
|
||||
final sdkManager = path.join(
|
||||
sdkPath,
|
||||
'cmdline-tools',
|
||||
'latest',
|
||||
'bin',
|
||||
'sdkmanager$sdkManagerExtension',
|
||||
);
|
||||
|
||||
log.info('Installing NDK $ndkVersion');
|
||||
runCommand(sdkManager, [
|
||||
'--install',
|
||||
'ndk;$ndkVersion',
|
||||
], environment: {
|
||||
'JAVA_HOME': javaHome,
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, String>> buildEnvironment() async {
|
||||
final hostArch = Platform.isMacOS
|
||||
? "darwin-x86_64"
|
||||
: (Platform.isLinux ? "linux-x86_64" : "windows-x86_64");
|
||||
|
||||
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||
final toolchainPath = path.join(
|
||||
ndkPath,
|
||||
'toolchains',
|
||||
'llvm',
|
||||
'prebuilt',
|
||||
hostArch,
|
||||
'bin',
|
||||
);
|
||||
|
||||
final minSdkVersion =
|
||||
math.max(target.androidMinSdkVersion!, this.minSdkVersion);
|
||||
|
||||
final exe = Platform.isWindows ? '.exe' : '';
|
||||
|
||||
final arKey = 'AR_${target.rust}';
|
||||
final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe']
|
||||
.map((e) => path.join(toolchainPath, e))
|
||||
.firstWhereOrNull((element) => File(element).existsSync());
|
||||
if (arValue == null) {
|
||||
throw Exception('Failed to find ar for $target in $toolchainPath');
|
||||
}
|
||||
|
||||
final targetArg = '--target=${target.rust}$minSdkVersion';
|
||||
|
||||
final ccKey = 'CC_${target.rust}';
|
||||
final ccValue = path.join(toolchainPath, 'clang$exe');
|
||||
final cfFlagsKey = 'CFLAGS_${target.rust}';
|
||||
final cFlagsValue = targetArg;
|
||||
|
||||
final cxxKey = 'CXX_${target.rust}';
|
||||
final cxxValue = path.join(toolchainPath, 'clang++$exe');
|
||||
final cxxFlagsKey = 'CXXFLAGS_${target.rust}';
|
||||
final cxxFlagsValue = targetArg;
|
||||
|
||||
final linkerKey =
|
||||
'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase();
|
||||
|
||||
final ranlibKey = 'RANLIB_${target.rust}';
|
||||
final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe');
|
||||
|
||||
final ndkVersionParsed = Version.parse(ndkVersion);
|
||||
final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS';
|
||||
final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed);
|
||||
|
||||
final runRustTool =
|
||||
Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh';
|
||||
|
||||
final packagePath = (await Isolate.resolvePackageUri(
|
||||
Uri.parse('package:build_tool/buildtool.dart')))!
|
||||
.toFilePath();
|
||||
final selfPath = path.canonicalize(path.join(
|
||||
packagePath,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
runRustTool,
|
||||
));
|
||||
|
||||
// Make sure that run_build_tool is working properly even initially launched directly
|
||||
// through dart run.
|
||||
final toolTempDir =
|
||||
Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir;
|
||||
|
||||
return {
|
||||
arKey: arValue,
|
||||
ccKey: ccValue,
|
||||
cfFlagsKey: cFlagsValue,
|
||||
cxxKey: cxxValue,
|
||||
cxxFlagsKey: cxxFlagsValue,
|
||||
ranlibKey: ranlibValue,
|
||||
rustFlagsKey: rustFlagsValue,
|
||||
linkerKey: selfPath,
|
||||
// Recognized by main() so we know when we're acting as a wrapper
|
||||
'_CARGOKIT_NDK_LINK_TARGET': targetArg,
|
||||
'_CARGOKIT_NDK_LINK_CLANG': ccValue,
|
||||
'CARGOKIT_TOOL_TEMP_DIR': toolTempDir,
|
||||
};
|
||||
}
|
||||
|
||||
// Workaround for libgcc missing in NDK23, inspired by cargo-ndk
|
||||
String _libGccWorkaround(String buildDir, Version ndkVersion) {
|
||||
final workaroundDir = path.join(
|
||||
buildDir,
|
||||
'cargokit',
|
||||
'libgcc_workaround',
|
||||
'${ndkVersion.major}',
|
||||
);
|
||||
Directory(workaroundDir).createSync(recursive: true);
|
||||
if (ndkVersion.major >= 23) {
|
||||
File(path.join(workaroundDir, 'libgcc.a'))
|
||||
.writeAsStringSync('INPUT(-lunwind)');
|
||||
} else {
|
||||
// Other way around, untested, forward libgcc.a from libunwind once Rust
|
||||
// gets updated for NDK23+.
|
||||
File(path.join(workaroundDir, 'libunwind.a'))
|
||||
.writeAsStringSync('INPUT(-lgcc)');
|
||||
}
|
||||
|
||||
var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? '';
|
||||
if (rustFlags.isNotEmpty) {
|
||||
rustFlags = '$rustFlags\x1f';
|
||||
}
|
||||
rustFlags = '$rustFlags-L\x1f$workaroundDir';
|
||||
return rustFlags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'builder.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class Artifact {
|
||||
/// File system location of the artifact.
|
||||
final String path;
|
||||
|
||||
/// Actual file name that the artifact should have in destination folder.
|
||||
final String finalFileName;
|
||||
|
||||
AritifactType get type {
|
||||
if (finalFileName.endsWith('.dll') ||
|
||||
finalFileName.endsWith('.dll.lib') ||
|
||||
finalFileName.endsWith('.pdb') ||
|
||||
finalFileName.endsWith('.so') ||
|
||||
finalFileName.endsWith('.dylib')) {
|
||||
return AritifactType.dylib;
|
||||
} else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) {
|
||||
return AritifactType.staticlib;
|
||||
} else {
|
||||
throw Exception('Unknown artifact type for $finalFileName');
|
||||
}
|
||||
}
|
||||
|
||||
Artifact({
|
||||
required this.path,
|
||||
required this.finalFileName,
|
||||
});
|
||||
}
|
||||
|
||||
final _log = Logger('artifacts_provider');
|
||||
|
||||
class ArtifactProvider {
|
||||
ArtifactProvider({
|
||||
required this.environment,
|
||||
required this.userOptions,
|
||||
});
|
||||
|
||||
final BuildEnvironment environment;
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<Map<Target, List<Artifact>>> getArtifacts(List<Target> targets) async {
|
||||
final result = await _getPrecompiledArtifacts(targets);
|
||||
|
||||
final pendingTargets = List.of(targets);
|
||||
pendingTargets.removeWhere((element) => result.containsKey(element));
|
||||
|
||||
if (pendingTargets.isEmpty) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final rustup = Rustup();
|
||||
for (final target in targets) {
|
||||
final builder = RustBuilder(target: target, environment: environment);
|
||||
builder.prepare(rustup);
|
||||
_log.info('Building ${environment.crateInfo.packageName} for $target');
|
||||
final targetDir = await builder.build();
|
||||
// For local build accept both static and dynamic libraries.
|
||||
final artifactNames = <String>{
|
||||
...getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
aritifactType: AritifactType.dylib,
|
||||
remote: false,
|
||||
),
|
||||
...getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
aritifactType: AritifactType.staticlib,
|
||||
remote: false,
|
||||
)
|
||||
};
|
||||
final artifacts = artifactNames
|
||||
.map((artifactName) => Artifact(
|
||||
path: path.join(targetDir, artifactName),
|
||||
finalFileName: artifactName,
|
||||
))
|
||||
.where((element) => File(element.path).existsSync())
|
||||
.toList();
|
||||
result[target] = artifacts;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<Target, List<Artifact>>> _getPrecompiledArtifacts(
|
||||
List<Target> targets) async {
|
||||
if (userOptions.usePrecompiledBinaries == false) {
|
||||
_log.info('Precompiled binaries are disabled');
|
||||
return {};
|
||||
}
|
||||
if (environment.crateOptions.precompiledBinaries == null) {
|
||||
_log.fine('Precompiled binaries not enabled for this crate');
|
||||
return {};
|
||||
}
|
||||
|
||||
final start = Stopwatch()..start();
|
||||
final crateHash = CrateHash.compute(environment.manifestDir,
|
||||
tempStorage: environment.targetTempDir);
|
||||
_log.fine(
|
||||
'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms');
|
||||
|
||||
final downloadedArtifactsDir =
|
||||
path.join(environment.targetTempDir, 'precompiled', crateHash);
|
||||
Directory(downloadedArtifactsDir).createSync(recursive: true);
|
||||
|
||||
final res = <Target, List<Artifact>>{};
|
||||
|
||||
for (final target in targets) {
|
||||
final requiredArtifacts = getArtifactNames(
|
||||
target: target,
|
||||
libraryName: environment.crateInfo.packageName,
|
||||
remote: true,
|
||||
);
|
||||
final artifactsForTarget = <Artifact>[];
|
||||
|
||||
for (final artifact in requiredArtifacts) {
|
||||
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||
final downloadedPath = path.join(downloadedArtifactsDir, fileName);
|
||||
if (!File(downloadedPath).existsSync()) {
|
||||
final signatureFileName =
|
||||
PrecompileBinaries.signatureFileName(target, artifact);
|
||||
await _tryDownloadArtifacts(
|
||||
crateHash: crateHash,
|
||||
fileName: fileName,
|
||||
signatureFileName: signatureFileName,
|
||||
finalPath: downloadedPath,
|
||||
);
|
||||
}
|
||||
if (File(downloadedPath).existsSync()) {
|
||||
artifactsForTarget.add(Artifact(
|
||||
path: downloadedPath,
|
||||
finalFileName: artifact,
|
||||
));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only provide complete set of artifacts.
|
||||
if (artifactsForTarget.length == requiredArtifacts.length) {
|
||||
_log.fine('Found precompiled artifacts for $target');
|
||||
res[target] = artifactsForTarget;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static Future<Response> _get(Uri url, {Map<String, String>? headers}) async {
|
||||
int attempt = 0;
|
||||
const maxAttempts = 10;
|
||||
while (true) {
|
||||
try {
|
||||
return await get(url, headers: headers);
|
||||
} on SocketException catch (e) {
|
||||
// Try to detect reset by peer error and retry.
|
||||
if (attempt++ < maxAttempts &&
|
||||
(e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) {
|
||||
_log.severe(
|
||||
'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...');
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
continue;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _tryDownloadArtifacts({
|
||||
required String crateHash,
|
||||
required String fileName,
|
||||
required String signatureFileName,
|
||||
required String finalPath,
|
||||
}) async {
|
||||
final precompiledBinaries = environment.crateOptions.precompiledBinaries!;
|
||||
final prefix = precompiledBinaries.uriPrefix;
|
||||
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||
final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||
_log.fine('Downloading signature from $signatureUrl');
|
||||
final signature = await _get(signatureUrl);
|
||||
if (signature.statusCode == 404) {
|
||||
_log.warning(
|
||||
'Precompiled binaries not available for crate hash $crateHash ($fileName)');
|
||||
return;
|
||||
}
|
||||
if (signature.statusCode != 200) {
|
||||
_log.severe(
|
||||
'Failed to download signature $signatureUrl: status ${signature.statusCode}');
|
||||
return;
|
||||
}
|
||||
_log.fine('Downloading binary from $url');
|
||||
final res = await _get(url);
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe('Failed to download binary $url: status ${res.statusCode}');
|
||||
return;
|
||||
}
|
||||
if (verify(
|
||||
precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) {
|
||||
File(finalPath).writeAsBytesSync(res.bodyBytes);
|
||||
} else {
|
||||
_log.shout('Signature verification failed! Ignoring binary.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AritifactType {
|
||||
staticlib,
|
||||
dylib,
|
||||
}
|
||||
|
||||
AritifactType artifactTypeForTarget(Target target) {
|
||||
if (target.darwinPlatform != null) {
|
||||
return AritifactType.staticlib;
|
||||
} else {
|
||||
return AritifactType.dylib;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> getArtifactNames({
|
||||
required Target target,
|
||||
required String libraryName,
|
||||
required bool remote,
|
||||
AritifactType? aritifactType,
|
||||
}) {
|
||||
aritifactType ??= artifactTypeForTarget(target);
|
||||
if (target.darwinArch != null) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['lib$libraryName.a'];
|
||||
} else {
|
||||
return ['lib$libraryName.dylib'];
|
||||
}
|
||||
} else if (target.rust.contains('-windows-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['$libraryName.lib'];
|
||||
} else {
|
||||
return [
|
||||
'$libraryName.dll',
|
||||
'$libraryName.dll.lib',
|
||||
if (!remote) '$libraryName.pdb'
|
||||
];
|
||||
}
|
||||
} else if (target.rust.contains('-linux-')) {
|
||||
if (aritifactType == AritifactType.staticlib) {
|
||||
return ['lib$libraryName.a'];
|
||||
} else {
|
||||
return ['lib$libraryName.so'];
|
||||
}
|
||||
} else {
|
||||
throw Exception("Unsupported target: ${target.rust}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class BuildCMake {
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
BuildCMake({required this.userOptions});
|
||||
|
||||
Future<void> build() async {
|
||||
final targetPlatform = Environment.targetPlatform;
|
||||
final target = Target.forFlutterName(Environment.targetPlatform);
|
||||
if (target == null) {
|
||||
throw Exception("Unknown target platform: $targetPlatform");
|
||||
}
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts([target]);
|
||||
|
||||
final libs = artifacts[target]!;
|
||||
|
||||
for (final lib in libs) {
|
||||
if (lib.type == AritifactType.dylib) {
|
||||
File(lib.path)
|
||||
.copySync(path.join(Environment.outputDir, lib.finalFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
|
||||
final log = Logger('build_gradle');
|
||||
|
||||
class BuildGradle {
|
||||
BuildGradle({required this.userOptions});
|
||||
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<void> build() async {
|
||||
final targets = Environment.targetPlatforms.map((arch) {
|
||||
final target = Target.forFlutterName(arch);
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||
}
|
||||
return target;
|
||||
}).toList();
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: true);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts(targets);
|
||||
|
||||
for (final target in targets) {
|
||||
final libs = artifacts[target]!;
|
||||
final outputDir = path.join(Environment.outputDir, target.android!);
|
||||
Directory(outputDir).createSync(recursive: true);
|
||||
|
||||
for (final lib in libs) {
|
||||
if (lib.type == AritifactType.dylib) {
|
||||
File(lib.path).copySync(path.join(outputDir, lib.finalFileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
class BuildPod {
|
||||
BuildPod({required this.userOptions});
|
||||
|
||||
final CargokitUserOptions userOptions;
|
||||
|
||||
Future<void> build() async {
|
||||
final targets = Environment.darwinArchs.map((arch) {
|
||||
final target = Target.forDarwin(
|
||||
platformName: Environment.darwinPlatformName, darwinAarch: arch);
|
||||
if (target == null) {
|
||||
throw Exception(
|
||||
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||
}
|
||||
return target;
|
||||
}).toList();
|
||||
|
||||
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||
final provider =
|
||||
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||
final artifacts = await provider.getArtifacts(targets);
|
||||
|
||||
void performLipo(String targetFile, Iterable<String> sourceFiles) {
|
||||
runCommand("lipo", [
|
||||
'-create',
|
||||
...sourceFiles,
|
||||
'-output',
|
||||
targetFile,
|
||||
]);
|
||||
}
|
||||
|
||||
final outputDir = Environment.outputDir;
|
||||
|
||||
Directory(outputDir).createSync(recursive: true);
|
||||
|
||||
final staticLibs = artifacts.values
|
||||
.expand((element) => element)
|
||||
.where((element) => element.type == AritifactType.staticlib)
|
||||
.toList();
|
||||
final dynamicLibs = artifacts.values
|
||||
.expand((element) => element)
|
||||
.where((element) => element.type == AritifactType.dylib)
|
||||
.toList();
|
||||
|
||||
final libName = environment.crateInfo.packageName;
|
||||
|
||||
// If there is static lib, use it and link it with pod
|
||||
if (staticLibs.isNotEmpty) {
|
||||
final finalTargetFile = path.join(outputDir, "lib$libName.a");
|
||||
performLipo(finalTargetFile, staticLibs.map((e) => e.path));
|
||||
} else {
|
||||
// Otherwise try to replace bundle dylib with our dylib
|
||||
final bundlePaths = [
|
||||
'$libName.framework/Versions/A/$libName',
|
||||
'$libName.framework/$libName',
|
||||
];
|
||||
|
||||
for (final bundlePath in bundlePaths) {
|
||||
final targetFile = path.join(outputDir, bundlePath);
|
||||
if (File(targetFile).existsSync()) {
|
||||
performLipo(targetFile, dynamicLibs.map((e) => e.path));
|
||||
|
||||
// Replace absolute id with @rpath one so that it works properly
|
||||
// when moved to Frameworks.
|
||||
runCommand("install_name_tool", [
|
||||
'-id',
|
||||
'@rpath/$bundlePath',
|
||||
targetFile,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw Exception('Unable to find bundle for dynamic library');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:github/github.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'android_environment.dart';
|
||||
import 'build_cmake.dart';
|
||||
import 'build_gradle.dart';
|
||||
import 'build_pod.dart';
|
||||
import 'logging.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
import 'verify_binaries.dart';
|
||||
|
||||
final log = Logger('build_tool');
|
||||
|
||||
abstract class BuildCommand extends Command {
|
||||
Future<void> runBuildCommand(CargokitUserOptions options);
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final options = CargokitUserOptions.load();
|
||||
|
||||
if (options.verboseLogging ||
|
||||
Platform.environment['CARGOKIT_VERBOSE'] == '1') {
|
||||
enableVerboseLogging();
|
||||
}
|
||||
|
||||
await runBuildCommand(options);
|
||||
}
|
||||
}
|
||||
|
||||
class BuildPodCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-pod';
|
||||
|
||||
@override
|
||||
final description = 'Build cocoa pod library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildPod(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildGradleCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-gradle';
|
||||
|
||||
@override
|
||||
final description = 'Build android library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildGradle(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class BuildCMakeCommand extends BuildCommand {
|
||||
@override
|
||||
final name = 'build-cmake';
|
||||
|
||||
@override
|
||||
final description = 'Build CMake library';
|
||||
|
||||
@override
|
||||
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||
final build = BuildCMake(userOptions: options);
|
||||
await build.build();
|
||||
}
|
||||
}
|
||||
|
||||
class GenKeyCommand extends Command {
|
||||
@override
|
||||
final name = 'gen-key';
|
||||
|
||||
@override
|
||||
final description = 'Generate key pair for signing precompiled binaries';
|
||||
|
||||
@override
|
||||
void run() {
|
||||
final kp = generateKey();
|
||||
final private = HEX.encode(kp.privateKey.bytes);
|
||||
final public = HEX.encode(kp.publicKey.bytes);
|
||||
print("Private Key: $private");
|
||||
print("Public Key: $public");
|
||||
}
|
||||
}
|
||||
|
||||
class PrecompileBinariesCommand extends Command {
|
||||
PrecompileBinariesCommand() {
|
||||
argParser
|
||||
..addOption(
|
||||
'repository',
|
||||
mandatory: true,
|
||||
help: 'Github repository slug in format owner/name',
|
||||
)
|
||||
..addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml',
|
||||
)
|
||||
..addMultiOption('target',
|
||||
help: 'Rust target triple of artifact to build.\n'
|
||||
'Can be specified multiple times or omitted in which case\n'
|
||||
'all targets for current platform will be built.')
|
||||
..addOption(
|
||||
'android-sdk-location',
|
||||
help: 'Location of Android SDK (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'android-ndk-version',
|
||||
help: 'Android NDK version (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'android-min-sdk-version',
|
||||
help: 'Android minimum rquired version (if available)',
|
||||
)
|
||||
..addOption(
|
||||
'temp-dir',
|
||||
help: 'Directory to store temporary build artifacts',
|
||||
)
|
||||
..addFlag(
|
||||
"verbose",
|
||||
abbr: "v",
|
||||
defaultsTo: false,
|
||||
help: "Enable verbose logging",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = 'precompile-binaries';
|
||||
|
||||
@override
|
||||
final description = 'Prebuild and upload binaries\n'
|
||||
'Private key must be passed through PRIVATE_KEY environment variable. '
|
||||
'Use gen_key through generate priave key.\n'
|
||||
'Github token must be passed as GITHUB_TOKEN environment variable.\n';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final verbose = argResults!['verbose'] as bool;
|
||||
if (verbose) {
|
||||
enableVerboseLogging();
|
||||
}
|
||||
|
||||
final privateKeyString = Platform.environment['PRIVATE_KEY'];
|
||||
if (privateKeyString == null) {
|
||||
throw ArgumentError('Missing PRIVATE_KEY environment variable');
|
||||
}
|
||||
final githubToken = Platform.environment['GITHUB_TOKEN'];
|
||||
if (githubToken == null) {
|
||||
throw ArgumentError('Missing GITHUB_TOKEN environment variable');
|
||||
}
|
||||
final privateKey = HEX.decode(privateKeyString);
|
||||
if (privateKey.length != 64) {
|
||||
throw ArgumentError('Private key must be 64 bytes long');
|
||||
}
|
||||
final manifestDir = argResults!['manifest-dir'] as String;
|
||||
if (!Directory(manifestDir).existsSync()) {
|
||||
throw ArgumentError('Manifest directory does not exist: $manifestDir');
|
||||
}
|
||||
String? androidMinSdkVersionString =
|
||||
argResults!['android-min-sdk-version'] as String?;
|
||||
int? androidMinSdkVersion;
|
||||
if (androidMinSdkVersionString != null) {
|
||||
androidMinSdkVersion = int.tryParse(androidMinSdkVersionString);
|
||||
if (androidMinSdkVersion == null) {
|
||||
throw ArgumentError(
|
||||
'Invalid android-min-sdk-version: $androidMinSdkVersionString');
|
||||
}
|
||||
}
|
||||
final targetStrigns = argResults!['target'] as List<String>;
|
||||
final targets = targetStrigns.map((target) {
|
||||
final res = Target.forRustTriple(target);
|
||||
if (res == null) {
|
||||
throw ArgumentError('Invalid target: $target');
|
||||
}
|
||||
return res;
|
||||
}).toList(growable: false);
|
||||
final precompileBinaries = PrecompileBinaries(
|
||||
privateKey: PrivateKey(privateKey),
|
||||
githubToken: githubToken,
|
||||
manifestDir: manifestDir,
|
||||
repositorySlug: RepositorySlug.full(argResults!['repository'] as String),
|
||||
targets: targets,
|
||||
androidSdkLocation: argResults!['android-sdk-location'] as String?,
|
||||
androidNdkVersion: argResults!['android-ndk-version'] as String?,
|
||||
androidMinSdkVersion: androidMinSdkVersion,
|
||||
tempDir: argResults!['temp-dir'] as String?,
|
||||
);
|
||||
|
||||
await precompileBinaries.run();
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyBinariesCommand extends Command {
|
||||
VerifyBinariesCommand() {
|
||||
argParser.addOption(
|
||||
'manifest-dir',
|
||||
mandatory: true,
|
||||
help: 'Directory containing Cargo.toml',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final name = "verify-binaries";
|
||||
|
||||
@override
|
||||
final description = 'Verifies published binaries\n'
|
||||
'Checks whether there is a binary published for each targets\n'
|
||||
'and checks the signature.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
final manifestDir = argResults!['manifest-dir'] as String;
|
||||
final verifyBinaries = VerifyBinaries(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
await verifyBinaries.run();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> runMain(List<String> args) async {
|
||||
try {
|
||||
// Init logging before options are loaded
|
||||
initLogging();
|
||||
|
||||
if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) {
|
||||
return AndroidEnvironment.clangLinkerWrapper(args);
|
||||
}
|
||||
|
||||
final runner = CommandRunner('build_tool', 'Cargokit built_tool')
|
||||
..addCommand(BuildPodCommand())
|
||||
..addCommand(BuildGradleCommand())
|
||||
..addCommand(BuildCMakeCommand())
|
||||
..addCommand(GenKeyCommand())
|
||||
..addCommand(PrecompileBinariesCommand())
|
||||
..addCommand(VerifyBinariesCommand());
|
||||
|
||||
await runner.run(args);
|
||||
} on ArgumentError catch (e) {
|
||||
stderr.writeln(e.toString());
|
||||
exit(1);
|
||||
} catch (e, s) {
|
||||
log.severe(kDoubleSeparator);
|
||||
log.severe('Cargokit BuildTool failed with error:');
|
||||
log.severe(kSeparator);
|
||||
log.severe(e);
|
||||
// This tells user to install Rust, there's no need to pollute the log with
|
||||
// stack trace.
|
||||
if (e is! RustupNotFoundException) {
|
||||
log.severe(kSeparator);
|
||||
log.severe(s);
|
||||
log.severe(kSeparator);
|
||||
log.severe('BuildTool arguments: $args');
|
||||
}
|
||||
log.severe(kDoubleSeparator);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'android_environment.dart';
|
||||
import 'cargo.dart';
|
||||
import 'environment.dart';
|
||||
import 'options.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
import 'util.dart';
|
||||
|
||||
final _log = Logger('builder');
|
||||
|
||||
enum BuildConfiguration {
|
||||
debug,
|
||||
release,
|
||||
profile,
|
||||
}
|
||||
|
||||
extension on BuildConfiguration {
|
||||
bool get isDebug => this == BuildConfiguration.debug;
|
||||
String get rustName => switch (this) {
|
||||
BuildConfiguration.debug => 'debug',
|
||||
BuildConfiguration.release => 'release',
|
||||
BuildConfiguration.profile => 'release',
|
||||
};
|
||||
}
|
||||
|
||||
class BuildException implements Exception {
|
||||
final String message;
|
||||
|
||||
BuildException(this.message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BuildException: $message';
|
||||
}
|
||||
}
|
||||
|
||||
class BuildEnvironment {
|
||||
final BuildConfiguration configuration;
|
||||
final CargokitCrateOptions crateOptions;
|
||||
final String targetTempDir;
|
||||
final String manifestDir;
|
||||
final CrateInfo crateInfo;
|
||||
|
||||
final bool isAndroid;
|
||||
final String? androidSdkPath;
|
||||
final String? androidNdkVersion;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? javaHome;
|
||||
|
||||
BuildEnvironment({
|
||||
required this.configuration,
|
||||
required this.crateOptions,
|
||||
required this.targetTempDir,
|
||||
required this.manifestDir,
|
||||
required this.crateInfo,
|
||||
required this.isAndroid,
|
||||
this.androidSdkPath,
|
||||
this.androidNdkVersion,
|
||||
this.androidMinSdkVersion,
|
||||
this.javaHome,
|
||||
});
|
||||
|
||||
static BuildConfiguration parseBuildConfiguration(String value) {
|
||||
// XCode configuration adds the flavor to configuration name.
|
||||
final firstSegment = value.split('-').first;
|
||||
final buildConfiguration = BuildConfiguration.values.firstWhereOrNull(
|
||||
(e) => e.name == firstSegment,
|
||||
);
|
||||
if (buildConfiguration == null) {
|
||||
_log.warning('Unknown build configuraiton $value, will assume release');
|
||||
return BuildConfiguration.release;
|
||||
}
|
||||
return buildConfiguration;
|
||||
}
|
||||
|
||||
static BuildEnvironment fromEnvironment({
|
||||
required bool isAndroid,
|
||||
}) {
|
||||
final buildConfiguration =
|
||||
parseBuildConfiguration(Environment.configuration);
|
||||
final manifestDir = Environment.manifestDir;
|
||||
final crateOptions = CargokitCrateOptions.load(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
return BuildEnvironment(
|
||||
configuration: buildConfiguration,
|
||||
crateOptions: crateOptions,
|
||||
targetTempDir: Environment.targetTempDir,
|
||||
manifestDir: manifestDir,
|
||||
crateInfo: crateInfo,
|
||||
isAndroid: isAndroid,
|
||||
androidSdkPath: isAndroid ? Environment.sdkPath : null,
|
||||
androidNdkVersion: isAndroid ? Environment.ndkVersion : null,
|
||||
androidMinSdkVersion:
|
||||
isAndroid ? int.parse(Environment.minSdkVersion) : null,
|
||||
javaHome: isAndroid ? Environment.javaHome : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RustBuilder {
|
||||
final Target target;
|
||||
final BuildEnvironment environment;
|
||||
|
||||
RustBuilder({
|
||||
required this.target,
|
||||
required this.environment,
|
||||
});
|
||||
|
||||
void prepare(
|
||||
Rustup rustup,
|
||||
) {
|
||||
final toolchain = _toolchain;
|
||||
if (rustup.installedTargets(toolchain) == null) {
|
||||
rustup.installToolchain(toolchain);
|
||||
}
|
||||
if (toolchain == 'nightly') {
|
||||
rustup.installRustSrcForNightly();
|
||||
}
|
||||
if (!rustup.installedTargets(toolchain)!.contains(target.rust)) {
|
||||
rustup.installTarget(target.rust, toolchain: toolchain);
|
||||
}
|
||||
}
|
||||
|
||||
CargoBuildOptions? get _buildOptions =>
|
||||
environment.crateOptions.cargo[environment.configuration];
|
||||
|
||||
String get _toolchain => _buildOptions?.toolchain.name ?? 'stable';
|
||||
|
||||
/// Returns the path of directory containing build artifacts.
|
||||
Future<String> build() async {
|
||||
final extraArgs = _buildOptions?.flags ?? [];
|
||||
final manifestPath = path.join(environment.manifestDir, 'Cargo.toml');
|
||||
runCommand(
|
||||
'rustup',
|
||||
[
|
||||
'run',
|
||||
_toolchain,
|
||||
'cargo',
|
||||
'build',
|
||||
...extraArgs,
|
||||
'--manifest-path',
|
||||
manifestPath,
|
||||
'-p',
|
||||
environment.crateInfo.packageName,
|
||||
if (!environment.configuration.isDebug) '--release',
|
||||
'--target',
|
||||
target.rust,
|
||||
'--target-dir',
|
||||
environment.targetTempDir,
|
||||
],
|
||||
environment: await _buildEnvironment(),
|
||||
);
|
||||
return path.join(
|
||||
environment.targetTempDir,
|
||||
target.rust,
|
||||
environment.configuration.rustName,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _buildEnvironment() async {
|
||||
if (target.android == null) {
|
||||
return {};
|
||||
} else {
|
||||
final sdkPath = environment.androidSdkPath;
|
||||
final ndkVersion = environment.androidNdkVersion;
|
||||
final minSdkVersion = environment.androidMinSdkVersion;
|
||||
if (sdkPath == null) {
|
||||
throw BuildException('androidSdkPath is not set');
|
||||
}
|
||||
if (ndkVersion == null) {
|
||||
throw BuildException('androidNdkVersion is not set');
|
||||
}
|
||||
if (minSdkVersion == null) {
|
||||
throw BuildException('androidMinSdkVersion is not set');
|
||||
}
|
||||
final env = AndroidEnvironment(
|
||||
sdkPath: sdkPath,
|
||||
ndkVersion: ndkVersion,
|
||||
minSdkVersion: minSdkVersion,
|
||||
targetTempDir: environment.targetTempDir,
|
||||
target: target,
|
||||
);
|
||||
if (!env.ndkIsInstalled() && environment.javaHome != null) {
|
||||
env.installNdk(javaHome: environment.javaHome!);
|
||||
}
|
||||
return env.buildEnvironment();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:toml/toml.dart';
|
||||
|
||||
class ManifestException {
|
||||
ManifestException(this.message, {required this.fileName});
|
||||
|
||||
final String? fileName;
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (fileName != null) {
|
||||
return 'Failed to parse package manifest at $fileName: $message';
|
||||
} else {
|
||||
return 'Failed to parse package manifest: $message';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CrateInfo {
|
||||
CrateInfo({required this.packageName});
|
||||
|
||||
final String packageName;
|
||||
|
||||
static CrateInfo parseManifest(String manifest, {final String? fileName}) {
|
||||
final toml = TomlDocument.parse(manifest);
|
||||
final package = toml.toMap()['package'];
|
||||
if (package == null) {
|
||||
throw ManifestException('Missing package section', fileName: fileName);
|
||||
}
|
||||
final name = package['name'];
|
||||
if (name == null) {
|
||||
throw ManifestException('Missing package name', fileName: fileName);
|
||||
}
|
||||
return CrateInfo(packageName: name);
|
||||
}
|
||||
|
||||
static CrateInfo load(String manifestDir) {
|
||||
final manifestFile = File(path.join(manifestDir, 'Cargo.toml'));
|
||||
final manifest = manifestFile.readAsStringSync();
|
||||
return parseManifest(manifest, fileName: manifestFile.path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class CrateHash {
|
||||
/// Computes a hash uniquely identifying crate content. This takes into account
|
||||
/// content all all .rs files inside the src directory, as well as Cargo.toml,
|
||||
/// Cargo.lock, build.rs and cargokit.yaml.
|
||||
///
|
||||
/// If [tempStorage] is provided, computed hash is stored in a file in that directory
|
||||
/// and reused on subsequent calls if the crate content hasn't changed.
|
||||
static String compute(String manifestDir, {String? tempStorage}) {
|
||||
return CrateHash._(
|
||||
manifestDir: manifestDir,
|
||||
tempStorage: tempStorage,
|
||||
)._compute();
|
||||
}
|
||||
|
||||
CrateHash._({
|
||||
required this.manifestDir,
|
||||
required this.tempStorage,
|
||||
});
|
||||
|
||||
String _compute() {
|
||||
final files = getFiles();
|
||||
final tempStorage = this.tempStorage;
|
||||
if (tempStorage != null) {
|
||||
final quickHash = _computeQuickHash(files);
|
||||
final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash'));
|
||||
quickHashFolder.createSync(recursive: true);
|
||||
final quickHashFile = File(path.join(quickHashFolder.path, quickHash));
|
||||
if (quickHashFile.existsSync()) {
|
||||
return quickHashFile.readAsStringSync();
|
||||
}
|
||||
final hash = _computeHash(files);
|
||||
quickHashFile.writeAsStringSync(hash);
|
||||
return hash;
|
||||
} else {
|
||||
return _computeHash(files);
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a quick hash based on files stat (without reading contents). This
|
||||
/// is used to cache the real hash, which is slower to compute since it involves
|
||||
/// reading every single file.
|
||||
String _computeQuickHash(List<File> files) {
|
||||
final output = AccumulatorSink<Digest>();
|
||||
final input = sha256.startChunkedConversion(output);
|
||||
|
||||
final data = ByteData(8);
|
||||
for (final file in files) {
|
||||
input.add(utf8.encode(file.path));
|
||||
final stat = file.statSync();
|
||||
data.setUint64(0, stat.size);
|
||||
input.add(data.buffer.asUint8List());
|
||||
data.setUint64(0, stat.modified.millisecondsSinceEpoch);
|
||||
input.add(data.buffer.asUint8List());
|
||||
}
|
||||
|
||||
input.close();
|
||||
return base64Url.encode(output.events.single.bytes);
|
||||
}
|
||||
|
||||
String _computeHash(List<File> files) {
|
||||
final output = AccumulatorSink<Digest>();
|
||||
final input = sha256.startChunkedConversion(output);
|
||||
|
||||
void addTextFile(File file) {
|
||||
// text Files are hashed by lines in case we're dealing with github checkout
|
||||
// that auto-converts line endings.
|
||||
final splitter = LineSplitter();
|
||||
if (file.existsSync()) {
|
||||
final data = file.readAsStringSync();
|
||||
final lines = splitter.convert(data);
|
||||
for (final line in lines) {
|
||||
input.add(utf8.encode(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final file in files) {
|
||||
addTextFile(file);
|
||||
}
|
||||
|
||||
input.close();
|
||||
final res = output.events.single;
|
||||
|
||||
// Truncate to 128bits.
|
||||
final hash = res.bytes.sublist(0, 16);
|
||||
return hex.encode(hash);
|
||||
}
|
||||
|
||||
List<File> getFiles() {
|
||||
final src = Directory(path.join(manifestDir, 'src'));
|
||||
final files = src
|
||||
.listSync(recursive: true, followLinks: false)
|
||||
.whereType<File>()
|
||||
.toList();
|
||||
files.sortBy((element) => element.path);
|
||||
void addFile(String relative) {
|
||||
final file = File(path.join(manifestDir, relative));
|
||||
if (file.existsSync()) {
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
addFile('Cargo.toml');
|
||||
addFile('Cargo.lock');
|
||||
addFile('build.rs');
|
||||
addFile('cargokit.yaml');
|
||||
return files;
|
||||
}
|
||||
|
||||
final String manifestDir;
|
||||
final String? tempStorage;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
extension on String {
|
||||
String resolveSymlink() => File(this).resolveSymbolicLinksSync();
|
||||
}
|
||||
|
||||
class Environment {
|
||||
/// Current build configuration (debug or release).
|
||||
static String get configuration =>
|
||||
_getEnv("CARGOKIT_CONFIGURATION").toLowerCase();
|
||||
|
||||
static bool get isDebug => configuration == 'debug';
|
||||
static bool get isRelease => configuration == 'release';
|
||||
|
||||
/// Temporary directory where Rust build artifacts are placed.
|
||||
static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR");
|
||||
|
||||
/// Final output directory where the build artifacts are placed.
|
||||
static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR');
|
||||
|
||||
/// Path to the crate manifest (containing Cargo.toml).
|
||||
static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR');
|
||||
|
||||
/// Directory inside root project. Not necessarily root folder. Symlinks are
|
||||
/// not resolved on purpose.
|
||||
static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR');
|
||||
|
||||
// Pod
|
||||
|
||||
/// Platform name (macosx, iphoneos, iphonesimulator).
|
||||
static String get darwinPlatformName =>
|
||||
_getEnv("CARGOKIT_DARWIN_PLATFORM_NAME");
|
||||
|
||||
/// List of architectures to build for (arm64, armv7, x86_64).
|
||||
static List<String> get darwinArchs =>
|
||||
_getEnv("CARGOKIT_DARWIN_ARCHS").split(' ');
|
||||
|
||||
// Gradle
|
||||
static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION");
|
||||
static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION");
|
||||
static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR");
|
||||
static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME");
|
||||
static List<String> get targetPlatforms =>
|
||||
_getEnv("CARGOKIT_TARGET_PLATFORMS").split(',');
|
||||
|
||||
// CMAKE
|
||||
static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM");
|
||||
|
||||
static String _getEnv(String key) {
|
||||
final res = Platform.environment[key];
|
||||
if (res == null) {
|
||||
throw Exception("Missing environment variable $key");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static String _getEnvPath(String key) {
|
||||
final res = _getEnv(key);
|
||||
if (Directory(res).existsSync()) {
|
||||
return res.resolveSymlink();
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const String kSeparator = "--";
|
||||
const String kDoubleSeparator = "==";
|
||||
|
||||
bool _lastMessageWasSeparator = false;
|
||||
|
||||
void _log(LogRecord rec) {
|
||||
final prefix = '${rec.level.name}: ';
|
||||
final out = rec.level == Level.SEVERE ? stderr : stdout;
|
||||
if (rec.message == kSeparator) {
|
||||
if (!_lastMessageWasSeparator) {
|
||||
out.write(prefix);
|
||||
out.writeln('-' * 80);
|
||||
_lastMessageWasSeparator = true;
|
||||
}
|
||||
return;
|
||||
} else if (rec.message == kDoubleSeparator) {
|
||||
out.write(prefix);
|
||||
out.writeln('=' * 80);
|
||||
_lastMessageWasSeparator = true;
|
||||
return;
|
||||
}
|
||||
out.write(prefix);
|
||||
out.writeln(rec.message);
|
||||
_lastMessageWasSeparator = false;
|
||||
}
|
||||
|
||||
void initLogging() {
|
||||
Logger.root.level = Level.INFO;
|
||||
Logger.root.onRecord.listen((LogRecord rec) {
|
||||
final lines = rec.message.split('\n');
|
||||
for (final line in lines) {
|
||||
if (line.isNotEmpty || lines.length == 1 || line != lines.last) {
|
||||
_log(LogRecord(
|
||||
rec.level,
|
||||
line,
|
||||
rec.loggerName,
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void enableVerboseLogging() {
|
||||
Logger.root.level = Level.ALL;
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
import 'builder.dart';
|
||||
import 'environment.dart';
|
||||
import 'rustup.dart';
|
||||
|
||||
final _log = Logger('options');
|
||||
|
||||
/// A class for exceptions that have source span information attached.
|
||||
class SourceSpanException implements Exception {
|
||||
// This is a getter so that subclasses can override it.
|
||||
/// A message describing the exception.
|
||||
String get message => _message;
|
||||
final String _message;
|
||||
|
||||
// This is a getter so that subclasses can override it.
|
||||
/// The span associated with this exception.
|
||||
///
|
||||
/// This may be `null` if the source location can't be determined.
|
||||
SourceSpan? get span => _span;
|
||||
final SourceSpan? _span;
|
||||
|
||||
SourceSpanException(this._message, this._span);
|
||||
|
||||
/// Returns a string representation of `this`.
|
||||
///
|
||||
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
|
||||
/// it indicates an ANSI terminal color escape that should be used to
|
||||
/// highlight the span's text. If it's `true`, it indicates that the text
|
||||
/// should be highlighted using the default color. If it's `false` or `null`,
|
||||
/// it indicates that the text shouldn't be highlighted.
|
||||
@override
|
||||
String toString({Object? color}) {
|
||||
if (span == null) return message;
|
||||
return 'Error on ${span!.message(message, color: color)}';
|
||||
}
|
||||
}
|
||||
|
||||
enum Toolchain {
|
||||
stable,
|
||||
beta,
|
||||
nightly,
|
||||
}
|
||||
|
||||
class CargoBuildOptions {
|
||||
final Toolchain toolchain;
|
||||
final List<String> flags;
|
||||
|
||||
CargoBuildOptions({
|
||||
required this.toolchain,
|
||||
required this.flags,
|
||||
});
|
||||
|
||||
static Toolchain _toolchainFromNode(YamlNode node) {
|
||||
if (node case YamlScalar(value: String name)) {
|
||||
final toolchain =
|
||||
Toolchain.values.firstWhereOrNull((element) => element.name == name);
|
||||
if (toolchain != null) {
|
||||
return toolchain;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.',
|
||||
node.span);
|
||||
}
|
||||
|
||||
static CargoBuildOptions parse(YamlNode node) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||
}
|
||||
Toolchain toolchain = Toolchain.stable;
|
||||
List<String> flags = [];
|
||||
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||||
if (key case YamlScalar(value: 'toolchain')) {
|
||||
toolchain = _toolchainFromNode(value);
|
||||
} else if (key case YamlScalar(value: 'extra_flags')) {
|
||||
if (value case YamlList(nodes: List<YamlNode> list)) {
|
||||
if (list.every((element) {
|
||||
if (element case YamlScalar(value: String _)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
flags = list.map((e) => e.value as String).toList();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Extra flags must be a list of strings', value.span);
|
||||
} else {
|
||||
throw SourceSpanException(
|
||||
'Unknown cargo option type. Must be "toolchain" or "extra_flags".',
|
||||
key.span);
|
||||
}
|
||||
}
|
||||
return CargoBuildOptions(toolchain: toolchain, flags: flags);
|
||||
}
|
||||
}
|
||||
|
||||
extension on YamlMap {
|
||||
/// Map that extracts keys so that we can do map case check on them.
|
||||
Map<dynamic, YamlNode> get valueMap =>
|
||||
nodes.map((key, value) => MapEntry(key.value, value));
|
||||
}
|
||||
|
||||
class PrecompiledBinaries {
|
||||
final String uriPrefix;
|
||||
final PublicKey publicKey;
|
||||
|
||||
PrecompiledBinaries({
|
||||
required this.uriPrefix,
|
||||
required this.publicKey,
|
||||
});
|
||||
|
||||
static PublicKey _publicKeyFromHex(String key, SourceSpan? span) {
|
||||
final bytes = HEX.decode(key);
|
||||
if (bytes.length != 32) {
|
||||
throw SourceSpanException(
|
||||
'Invalid public key. Must be 32 bytes long.', span);
|
||||
}
|
||||
return PublicKey(bytes);
|
||||
}
|
||||
|
||||
static PrecompiledBinaries parse(YamlNode node) {
|
||||
if (node case YamlMap(valueMap: Map<dynamic, YamlNode> map)) {
|
||||
if (map
|
||||
case {
|
||||
'url_prefix': YamlNode urlPrefixNode,
|
||||
'public_key': YamlNode publicKeyNode,
|
||||
}) {
|
||||
final urlPrefix = switch (urlPrefixNode) {
|
||||
YamlScalar(value: String urlPrefix) => urlPrefix,
|
||||
_ => throw SourceSpanException(
|
||||
'Invalid URL prefix value.', urlPrefixNode.span),
|
||||
};
|
||||
final publicKey = switch (publicKeyNode) {
|
||||
YamlScalar(value: String publicKey) =>
|
||||
_publicKeyFromHex(publicKey, publicKeyNode.span),
|
||||
_ => throw SourceSpanException(
|
||||
'Invalid public key value.', publicKeyNode.span),
|
||||
};
|
||||
return PrecompiledBinaries(
|
||||
uriPrefix: urlPrefix,
|
||||
publicKey: publicKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Invalid precompiled binaries value. '
|
||||
'Expected Map with "url_prefix" and "public_key".',
|
||||
node.span);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cargokit options specified for Rust crate.
|
||||
class CargokitCrateOptions {
|
||||
CargokitCrateOptions({
|
||||
this.cargo = const {},
|
||||
this.precompiledBinaries,
|
||||
});
|
||||
|
||||
final Map<BuildConfiguration, CargoBuildOptions> cargo;
|
||||
final PrecompiledBinaries? precompiledBinaries;
|
||||
|
||||
static CargokitCrateOptions parse(YamlNode node) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||
}
|
||||
final options = <BuildConfiguration, CargoBuildOptions>{};
|
||||
PrecompiledBinaries? precompiledBinaries;
|
||||
|
||||
for (final entry in node.nodes.entries) {
|
||||
if (entry
|
||||
case MapEntry(
|
||||
key: YamlScalar(value: 'cargo'),
|
||||
value: YamlNode node,
|
||||
)) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||
}
|
||||
for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) {
|
||||
if (key case YamlScalar(value: String name)) {
|
||||
final configuration = BuildConfiguration.values
|
||||
.firstWhereOrNull((element) => element.name == name);
|
||||
if (configuration != null) {
|
||||
options[configuration] = CargoBuildOptions.parse(value);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.',
|
||||
key.span);
|
||||
}
|
||||
} else if (entry.key case YamlScalar(value: 'precompiled_binaries')) {
|
||||
precompiledBinaries = PrecompiledBinaries.parse(entry.value);
|
||||
} else {
|
||||
throw SourceSpanException(
|
||||
'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".',
|
||||
entry.key.span);
|
||||
}
|
||||
}
|
||||
return CargokitCrateOptions(
|
||||
cargo: options,
|
||||
precompiledBinaries: precompiledBinaries,
|
||||
);
|
||||
}
|
||||
|
||||
static CargokitCrateOptions load({
|
||||
required String manifestDir,
|
||||
}) {
|
||||
final uri = Uri.file(path.join(manifestDir, "cargokit.yaml"));
|
||||
final file = File.fromUri(uri);
|
||||
if (file.existsSync()) {
|
||||
final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri);
|
||||
return parse(contents);
|
||||
} else {
|
||||
return CargokitCrateOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CargokitUserOptions {
|
||||
// When Rustup is installed always build locally unless user opts into
|
||||
// using precompiled binaries.
|
||||
static bool defaultUsePrecompiledBinaries() {
|
||||
return Rustup.executablePath() == null;
|
||||
}
|
||||
|
||||
CargokitUserOptions({
|
||||
required this.usePrecompiledBinaries,
|
||||
required this.verboseLogging,
|
||||
});
|
||||
|
||||
CargokitUserOptions._()
|
||||
: usePrecompiledBinaries = defaultUsePrecompiledBinaries(),
|
||||
verboseLogging = false;
|
||||
|
||||
static CargokitUserOptions parse(YamlNode node) {
|
||||
if (node is! YamlMap) {
|
||||
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||
}
|
||||
bool usePrecompiledBinaries = defaultUsePrecompiledBinaries();
|
||||
bool verboseLogging = false;
|
||||
|
||||
for (final entry in node.nodes.entries) {
|
||||
if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) {
|
||||
if (entry.value case YamlScalar(value: bool value)) {
|
||||
usePrecompiledBinaries = value;
|
||||
continue;
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Invalid value for "use_precompiled_binaries". Must be a boolean.',
|
||||
entry.value.span);
|
||||
} else if (entry.key case YamlScalar(value: 'verbose_logging')) {
|
||||
if (entry.value case YamlScalar(value: bool value)) {
|
||||
verboseLogging = value;
|
||||
continue;
|
||||
}
|
||||
throw SourceSpanException(
|
||||
'Invalid value for "verbose_logging". Must be a boolean.',
|
||||
entry.value.span);
|
||||
} else {
|
||||
throw SourceSpanException(
|
||||
'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".',
|
||||
entry.key.span);
|
||||
}
|
||||
}
|
||||
return CargokitUserOptions(
|
||||
usePrecompiledBinaries: usePrecompiledBinaries,
|
||||
verboseLogging: verboseLogging,
|
||||
);
|
||||
}
|
||||
|
||||
static CargokitUserOptions load() {
|
||||
String fileName = "cargokit_options.yaml";
|
||||
var userProjectDir = Directory(Environment.rootProjectDir);
|
||||
|
||||
while (userProjectDir.parent.path != userProjectDir.path) {
|
||||
final configFile = File(path.join(userProjectDir.path, fileName));
|
||||
if (configFile.existsSync()) {
|
||||
final contents = loadYamlNode(
|
||||
configFile.readAsStringSync(),
|
||||
sourceUrl: configFile.uri,
|
||||
);
|
||||
final res = parse(contents);
|
||||
if (res.verboseLogging) {
|
||||
_log.info('Found user options file at ${configFile.path}');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
userProjectDir = userProjectDir.parent;
|
||||
}
|
||||
return CargokitUserOptions._();
|
||||
}
|
||||
|
||||
final bool usePrecompiledBinaries;
|
||||
final bool verboseLogging;
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:github/github.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'builder.dart';
|
||||
import 'cargo.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'rustup.dart';
|
||||
import 'target.dart';
|
||||
|
||||
final _log = Logger('precompile_binaries');
|
||||
|
||||
class PrecompileBinaries {
|
||||
PrecompileBinaries({
|
||||
required this.privateKey,
|
||||
required this.githubToken,
|
||||
required this.repositorySlug,
|
||||
required this.manifestDir,
|
||||
required this.targets,
|
||||
this.androidSdkLocation,
|
||||
this.androidNdkVersion,
|
||||
this.androidMinSdkVersion,
|
||||
this.tempDir,
|
||||
});
|
||||
|
||||
final PrivateKey privateKey;
|
||||
final String githubToken;
|
||||
final RepositorySlug repositorySlug;
|
||||
final String manifestDir;
|
||||
final List<Target> targets;
|
||||
final String? androidSdkLocation;
|
||||
final String? androidNdkVersion;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? tempDir;
|
||||
|
||||
static String fileName(Target target, String name) {
|
||||
return '${target.rust}_$name';
|
||||
}
|
||||
|
||||
static String signatureFileName(Target target, String name) {
|
||||
return '${target.rust}_$name.sig';
|
||||
}
|
||||
|
||||
Future<void> run() async {
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
|
||||
final targets = List.of(this.targets);
|
||||
if (targets.isEmpty) {
|
||||
targets.addAll([
|
||||
...Target.buildableTargets(),
|
||||
if (androidSdkLocation != null) ...Target.androidTargets(),
|
||||
]);
|
||||
}
|
||||
|
||||
_log.info('Precompiling binaries for $targets');
|
||||
|
||||
final hash = CrateHash.compute(manifestDir);
|
||||
_log.info('Computed crate hash: $hash');
|
||||
|
||||
final String tagName = 'precompiled_$hash';
|
||||
|
||||
final github = GitHub(auth: Authentication.withToken(githubToken));
|
||||
final repo = github.repositories;
|
||||
final release = await _getOrCreateRelease(
|
||||
repo: repo,
|
||||
tagName: tagName,
|
||||
packageName: crateInfo.packageName,
|
||||
hash: hash,
|
||||
);
|
||||
|
||||
final tempDir = this.tempDir != null
|
||||
? Directory(this.tempDir!)
|
||||
: Directory.systemTemp.createTempSync('precompiled_');
|
||||
|
||||
tempDir.createSync(recursive: true);
|
||||
|
||||
final crateOptions = CargokitCrateOptions.load(
|
||||
manifestDir: manifestDir,
|
||||
);
|
||||
|
||||
final buildEnvironment = BuildEnvironment(
|
||||
configuration: BuildConfiguration.release,
|
||||
crateOptions: crateOptions,
|
||||
targetTempDir: tempDir.path,
|
||||
manifestDir: manifestDir,
|
||||
crateInfo: crateInfo,
|
||||
isAndroid: androidSdkLocation != null,
|
||||
androidSdkPath: androidSdkLocation,
|
||||
androidNdkVersion: androidNdkVersion,
|
||||
androidMinSdkVersion: androidMinSdkVersion,
|
||||
);
|
||||
|
||||
final rustup = Rustup();
|
||||
|
||||
for (final target in targets) {
|
||||
final artifactNames = getArtifactNames(
|
||||
target: target,
|
||||
libraryName: crateInfo.packageName,
|
||||
remote: true,
|
||||
);
|
||||
|
||||
if (artifactNames.every((name) {
|
||||
final fileName = PrecompileBinaries.fileName(target, name);
|
||||
return (release.assets ?? []).any((e) => e.name == fileName);
|
||||
})) {
|
||||
_log.info("All artifacts for $target already exist - skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
_log.info('Building for $target');
|
||||
|
||||
final builder =
|
||||
RustBuilder(target: target, environment: buildEnvironment);
|
||||
builder.prepare(rustup);
|
||||
final res = await builder.build();
|
||||
|
||||
final assets = <CreateReleaseAsset>[];
|
||||
for (final name in artifactNames) {
|
||||
final file = File(path.join(res, name));
|
||||
if (!file.existsSync()) {
|
||||
throw Exception('Missing artifact: ${file.path}');
|
||||
}
|
||||
|
||||
final data = file.readAsBytesSync();
|
||||
final create = CreateReleaseAsset(
|
||||
name: PrecompileBinaries.fileName(target, name),
|
||||
contentType: "application/octet-stream",
|
||||
assetData: data,
|
||||
);
|
||||
final signature = sign(privateKey, data);
|
||||
final signatureCreate = CreateReleaseAsset(
|
||||
name: signatureFileName(target, name),
|
||||
contentType: "application/octet-stream",
|
||||
assetData: signature,
|
||||
);
|
||||
bool verified = verify(public(privateKey), data, signature);
|
||||
if (!verified) {
|
||||
throw Exception('Signature verification failed');
|
||||
}
|
||||
assets.add(create);
|
||||
assets.add(signatureCreate);
|
||||
}
|
||||
_log.info('Uploading assets: ${assets.map((e) => e.name)}');
|
||||
for (final asset in assets) {
|
||||
// This seems to be failing on CI so do it one by one
|
||||
int retryCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
await repo.uploadReleaseAssets(release, [asset]);
|
||||
break;
|
||||
} on Exception catch (e) {
|
||||
if (retryCount == 10) {
|
||||
rethrow;
|
||||
}
|
||||
++retryCount;
|
||||
_log.shout(
|
||||
'Upload failed (attempt $retryCount, will retry): ${e.toString()}');
|
||||
await Future.delayed(Duration(seconds: 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_log.info('Cleaning up');
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
Future<Release> _getOrCreateRelease({
|
||||
required RepositoriesService repo,
|
||||
required String tagName,
|
||||
required String packageName,
|
||||
required String hash,
|
||||
}) async {
|
||||
Release release;
|
||||
try {
|
||||
_log.info('Fetching release $tagName');
|
||||
release = await repo.getReleaseByTagName(repositorySlug, tagName);
|
||||
} on ReleaseNotFound {
|
||||
_log.info('Release not found - creating release $tagName');
|
||||
release = await repo.createRelease(
|
||||
repositorySlug,
|
||||
CreateRelease.from(
|
||||
tagName: tagName,
|
||||
name: 'Precompiled binaries ${hash.substring(0, 8)}',
|
||||
targetCommitish: null,
|
||||
isDraft: false,
|
||||
isPrerelease: false,
|
||||
body: 'Precompiled binaries for crate $packageName, '
|
||||
'crate hash $hash.',
|
||||
));
|
||||
}
|
||||
return release;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
class _Toolchain {
|
||||
_Toolchain(
|
||||
this.name,
|
||||
this.targets,
|
||||
);
|
||||
|
||||
final String name;
|
||||
final List<String> targets;
|
||||
}
|
||||
|
||||
class Rustup {
|
||||
List<String>? installedTargets(String toolchain) {
|
||||
final targets = _installedTargets(toolchain);
|
||||
return targets != null ? List.unmodifiable(targets) : null;
|
||||
}
|
||||
|
||||
void installToolchain(String toolchain) {
|
||||
log.info("Installing Rust toolchain: $toolchain");
|
||||
runCommand("rustup", ['toolchain', 'install', toolchain]);
|
||||
_installedToolchains
|
||||
.add(_Toolchain(toolchain, _getInstalledTargets(toolchain)));
|
||||
}
|
||||
|
||||
void installTarget(
|
||||
String target, {
|
||||
required String toolchain,
|
||||
}) {
|
||||
log.info("Installing Rust target: $target");
|
||||
runCommand("rustup", [
|
||||
'target',
|
||||
'add',
|
||||
'--toolchain',
|
||||
toolchain,
|
||||
target,
|
||||
]);
|
||||
_installedTargets(toolchain)?.add(target);
|
||||
}
|
||||
|
||||
final List<_Toolchain> _installedToolchains;
|
||||
|
||||
Rustup() : _installedToolchains = _getInstalledToolchains();
|
||||
|
||||
List<String>? _installedTargets(String toolchain) => _installedToolchains
|
||||
.firstWhereOrNull(
|
||||
(e) => e.name == toolchain || e.name.startsWith('$toolchain-'))
|
||||
?.targets;
|
||||
|
||||
static List<_Toolchain> _getInstalledToolchains() {
|
||||
String extractToolchainName(String line) {
|
||||
// ignore (default) after toolchain name
|
||||
final parts = line.split(' ');
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
final res = runCommand("rustup", ['toolchain', 'list']);
|
||||
|
||||
// To list all non-custom toolchains, we need to filter out lines that
|
||||
// don't start with "stable", "beta", or "nightly".
|
||||
Pattern nonCustom = RegExp(r"^(stable|beta|nightly)");
|
||||
final lines = res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty && e.startsWith(nonCustom))
|
||||
.map(extractToolchainName)
|
||||
.toList(growable: true);
|
||||
|
||||
return lines
|
||||
.map(
|
||||
(name) => _Toolchain(
|
||||
name,
|
||||
_getInstalledTargets(name),
|
||||
),
|
||||
)
|
||||
.toList(growable: true);
|
||||
}
|
||||
|
||||
static List<String> _getInstalledTargets(String toolchain) {
|
||||
final res = runCommand("rustup", [
|
||||
'target',
|
||||
'list',
|
||||
'--toolchain',
|
||||
toolchain,
|
||||
'--installed',
|
||||
]);
|
||||
final lines = res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList(growable: true);
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool _didInstallRustSrcForNightly = false;
|
||||
|
||||
void installRustSrcForNightly() {
|
||||
if (_didInstallRustSrcForNightly) {
|
||||
return;
|
||||
}
|
||||
// Useful for -Z build-std
|
||||
runCommand(
|
||||
"rustup",
|
||||
['component', 'add', 'rust-src', '--toolchain', 'nightly'],
|
||||
);
|
||||
_didInstallRustSrcForNightly = true;
|
||||
}
|
||||
|
||||
static String? executablePath() {
|
||||
final envPath = Platform.environment['PATH'];
|
||||
final envPathSeparator = Platform.isWindows ? ';' : ':';
|
||||
final home = Platform.isWindows
|
||||
? Platform.environment['USERPROFILE']
|
||||
: Platform.environment['HOME'];
|
||||
final paths = [
|
||||
if (home != null) path.join(home, '.cargo', 'bin'),
|
||||
if (envPath != null) ...envPath.split(envPathSeparator),
|
||||
];
|
||||
for (final p in paths) {
|
||||
final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup';
|
||||
final rustupPath = path.join(p, rustup);
|
||||
if (File(rustupPath).existsSync()) {
|
||||
return rustupPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
class Target {
|
||||
Target({
|
||||
required this.rust,
|
||||
this.flutter,
|
||||
this.android,
|
||||
this.androidMinSdkVersion,
|
||||
this.darwinPlatform,
|
||||
this.darwinArch,
|
||||
});
|
||||
|
||||
static final all = [
|
||||
Target(
|
||||
rust: 'armv7-linux-androideabi',
|
||||
flutter: 'android-arm',
|
||||
android: 'armeabi-v7a',
|
||||
androidMinSdkVersion: 16,
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-linux-android',
|
||||
flutter: 'android-arm64',
|
||||
android: 'arm64-v8a',
|
||||
androidMinSdkVersion: 21,
|
||||
),
|
||||
Target(
|
||||
rust: 'i686-linux-android',
|
||||
flutter: 'android-x86',
|
||||
android: 'x86',
|
||||
androidMinSdkVersion: 16,
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-linux-android',
|
||||
flutter: 'android-x64',
|
||||
android: 'x86_64',
|
||||
androidMinSdkVersion: 21,
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-pc-windows-msvc',
|
||||
flutter: 'windows-x64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-unknown-linux-gnu',
|
||||
flutter: 'linux-x64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-unknown-linux-gnu',
|
||||
flutter: 'linux-arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-darwin',
|
||||
darwinPlatform: 'macosx',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-ios',
|
||||
darwinPlatform: 'iphoneos',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'aarch64-apple-ios-sim',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'arm64',
|
||||
),
|
||||
Target(
|
||||
rust: 'x86_64-apple-ios',
|
||||
darwinPlatform: 'iphonesimulator',
|
||||
darwinArch: 'x86_64',
|
||||
),
|
||||
];
|
||||
|
||||
static Target? forFlutterName(String flutterName) {
|
||||
return all.firstWhereOrNull((element) => element.flutter == flutterName);
|
||||
}
|
||||
|
||||
static Target? forDarwin({
|
||||
required String platformName,
|
||||
required String darwinAarch,
|
||||
}) {
|
||||
return all.firstWhereOrNull((element) => //
|
||||
element.darwinPlatform == platformName &&
|
||||
element.darwinArch == darwinAarch);
|
||||
}
|
||||
|
||||
static Target? forRustTriple(String triple) {
|
||||
return all.firstWhereOrNull((element) => element.rust == triple);
|
||||
}
|
||||
|
||||
static List<Target> androidTargets() {
|
||||
return all
|
||||
.where((element) => element.android != null)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
/// Returns buildable targets on current host platform ignoring Android targets.
|
||||
static List<Target> buildableTargets() {
|
||||
if (Platform.isLinux) {
|
||||
// Right now we don't support cross-compiling on Linux. So we just return
|
||||
// the host target.
|
||||
final arch = runCommand('arch', []).stdout as String;
|
||||
if (arch.trim() == 'aarch64') {
|
||||
return [Target.forRustTriple('aarch64-unknown-linux-gnu')!];
|
||||
} else {
|
||||
return [Target.forRustTriple('x86_64-unknown-linux-gnu')!];
|
||||
}
|
||||
}
|
||||
return all.where((target) {
|
||||
if (Platform.isWindows) {
|
||||
return target.rust.contains('-windows-');
|
||||
} else if (Platform.isMacOS) {
|
||||
return target.darwinPlatform != null;
|
||||
}
|
||||
return false;
|
||||
}).toList(growable: false);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return rust;
|
||||
}
|
||||
|
||||
final String? flutter;
|
||||
final String rust;
|
||||
final String? android;
|
||||
final int? androidMinSdkVersion;
|
||||
final String? darwinPlatform;
|
||||
final String? darwinArch;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'logging.dart';
|
||||
import 'rustup.dart';
|
||||
|
||||
final log = Logger("process");
|
||||
|
||||
class CommandFailedException implements Exception {
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final ProcessResult result;
|
||||
|
||||
CommandFailedException({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
required this.result,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final stdout = result.stdout.toString().trim();
|
||||
final stderr = result.stderr.toString().trim();
|
||||
return [
|
||||
"External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}",
|
||||
"Returned Exit Code: ${result.exitCode}",
|
||||
kSeparator,
|
||||
"STDOUT:",
|
||||
if (stdout.isNotEmpty) stdout,
|
||||
kSeparator,
|
||||
"STDERR:",
|
||||
if (stderr.isNotEmpty) stderr,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
class TestRunCommandArgs {
|
||||
final String executable;
|
||||
final List<String> arguments;
|
||||
final String? workingDirectory;
|
||||
final Map<String, String>? environment;
|
||||
final bool includeParentEnvironment;
|
||||
final bool runInShell;
|
||||
final Encoding? stdoutEncoding;
|
||||
final Encoding? stderrEncoding;
|
||||
|
||||
TestRunCommandArgs({
|
||||
required this.executable,
|
||||
required this.arguments,
|
||||
this.workingDirectory,
|
||||
this.environment,
|
||||
this.includeParentEnvironment = true,
|
||||
this.runInShell = false,
|
||||
this.stdoutEncoding,
|
||||
this.stderrEncoding,
|
||||
});
|
||||
}
|
||||
|
||||
class TestRunCommandResult {
|
||||
TestRunCommandResult({
|
||||
this.pid = 1,
|
||||
this.exitCode = 0,
|
||||
this.stdout = '',
|
||||
this.stderr = '',
|
||||
});
|
||||
|
||||
final int pid;
|
||||
final int exitCode;
|
||||
final String stdout;
|
||||
final String stderr;
|
||||
}
|
||||
|
||||
TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride;
|
||||
|
||||
ProcessResult runCommand(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
String? workingDirectory,
|
||||
Map<String, String>? environment,
|
||||
bool includeParentEnvironment = true,
|
||||
bool runInShell = false,
|
||||
Encoding? stdoutEncoding = systemEncoding,
|
||||
Encoding? stderrEncoding = systemEncoding,
|
||||
}) {
|
||||
if (testRunCommandOverride != null) {
|
||||
final result = testRunCommandOverride!(TestRunCommandArgs(
|
||||
executable: executable,
|
||||
arguments: arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
includeParentEnvironment: includeParentEnvironment,
|
||||
runInShell: runInShell,
|
||||
stdoutEncoding: stdoutEncoding,
|
||||
stderrEncoding: stderrEncoding,
|
||||
));
|
||||
return ProcessResult(
|
||||
result.pid,
|
||||
result.exitCode,
|
||||
result.stdout,
|
||||
result.stderr,
|
||||
);
|
||||
}
|
||||
log.finer('Running command $executable ${arguments.join(' ')}');
|
||||
final res = Process.runSync(
|
||||
_resolveExecutable(executable),
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
environment: environment,
|
||||
includeParentEnvironment: includeParentEnvironment,
|
||||
runInShell: runInShell,
|
||||
stderrEncoding: stderrEncoding,
|
||||
stdoutEncoding: stdoutEncoding,
|
||||
);
|
||||
if (res.exitCode != 0) {
|
||||
throw CommandFailedException(
|
||||
executable: executable,
|
||||
arguments: arguments,
|
||||
result: res,
|
||||
);
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
class RustupNotFoundException implements Exception {
|
||||
@override
|
||||
String toString() {
|
||||
return [
|
||||
' ',
|
||||
'rustup not found in PATH.',
|
||||
' ',
|
||||
'Maybe you need to install Rust? It only takes a minute:',
|
||||
' ',
|
||||
if (Platform.isWindows) 'https://www.rust-lang.org/tools/install',
|
||||
if (hasHomebrewRustInPath()) ...[
|
||||
'\$ brew unlink rust # Unlink homebrew Rust from PATH',
|
||||
],
|
||||
if (!Platform.isWindows)
|
||||
"\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
|
||||
' ',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
static bool hasHomebrewRustInPath() {
|
||||
if (!Platform.isMacOS) {
|
||||
return false;
|
||||
}
|
||||
final envPath = Platform.environment['PATH'] ?? '';
|
||||
final paths = envPath.split(':');
|
||||
return paths.any((p) {
|
||||
return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _resolveExecutable(String executable) {
|
||||
if (executable == 'rustup') {
|
||||
final resolved = Rustup.executablePath();
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
throw RustupNotFoundException();
|
||||
} else {
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'artifacts_provider.dart';
|
||||
import 'cargo.dart';
|
||||
import 'crate_hash.dart';
|
||||
import 'options.dart';
|
||||
import 'precompile_binaries.dart';
|
||||
import 'target.dart';
|
||||
|
||||
class VerifyBinaries {
|
||||
VerifyBinaries({
|
||||
required this.manifestDir,
|
||||
});
|
||||
|
||||
final String manifestDir;
|
||||
|
||||
Future<void> run() async {
|
||||
final crateInfo = CrateInfo.load(manifestDir);
|
||||
|
||||
final config = CargokitCrateOptions.load(manifestDir: manifestDir);
|
||||
final precompiledBinaries = config.precompiledBinaries;
|
||||
if (precompiledBinaries == null) {
|
||||
stdout.writeln('Crate does not support precompiled binaries.');
|
||||
} else {
|
||||
final crateHash = CrateHash.compute(manifestDir);
|
||||
stdout.writeln('Crate hash: $crateHash');
|
||||
|
||||
for (final target in Target.all) {
|
||||
final message = 'Checking ${target.rust}...';
|
||||
stdout.write(message.padRight(40));
|
||||
stdout.flush();
|
||||
|
||||
final artifacts = getArtifactNames(
|
||||
target: target,
|
||||
libraryName: crateInfo.packageName,
|
||||
remote: true,
|
||||
);
|
||||
|
||||
final prefix = precompiledBinaries.uriPrefix;
|
||||
|
||||
bool ok = true;
|
||||
|
||||
for (final artifact in artifacts) {
|
||||
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||
final signatureFileName =
|
||||
PrecompileBinaries.signatureFileName(target, artifact);
|
||||
|
||||
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||
final signatureUrl =
|
||||
Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||
|
||||
final signature = await get(signatureUrl);
|
||||
if (signature.statusCode != 200) {
|
||||
stdout.writeln('MISSING');
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
final asset = await get(url);
|
||||
if (asset.statusCode != 200) {
|
||||
stdout.writeln('MISSING');
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!verify(precompiledBinaries.publicKey, asset.bodyBytes,
|
||||
signature.bodyBytes)) {
|
||||
stdout.writeln('INVALID SIGNATURE');
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
stdout.writeln('OK');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "67.0.0"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adaptive_number
|
||||
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
args:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_config
|
||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
convert:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "802bd084fb82e55df091ec8ad1553a7331b61c08251eef19a508b6f3f3a9858d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.13.1"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
ed25519_edwards:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ed25519_edwards
|
||||
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
github:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: github
|
||||
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.17.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hex:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hex
|
||||
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.26.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.11"
|
||||
toml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toml
|
||||
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: version
|
||||
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
yaml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
@@ -0,0 +1,33 @@
|
||||
# This is copied from Cargokit (which is the official way to use it currently)
|
||||
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
name: build_tool
|
||||
description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build.
|
||||
publish_to: none
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
# these are pinned on purpose because the bundle_tool_runner doesn't have
|
||||
# pubspec.lock. See run_build_tool.sh
|
||||
logging: 1.2.0
|
||||
path: 1.8.0
|
||||
version: 3.0.0
|
||||
collection: 1.18.0
|
||||
ed25519_edwards: 0.3.1
|
||||
hex: 0.2.0
|
||||
yaml: 3.1.2
|
||||
source_span: 1.10.0
|
||||
github: 9.17.0
|
||||
args: 2.4.2
|
||||
crypto: 3.0.3
|
||||
convert: 3.1.1
|
||||
http: 1.1.0
|
||||
toml: 0.14.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.1.0
|
||||
test: ^1.24.0
|
||||
@@ -0,0 +1,99 @@
|
||||
SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
|
||||
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||
get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH)
|
||||
|
||||
if(WIN32)
|
||||
# REALPATH does not properly resolve symlinks on windows :-/
|
||||
execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
endif()
|
||||
|
||||
# Arguments
|
||||
# - target: CMAKE target to which rust library is linked
|
||||
# - manifest_dir: relative path from current folder to directory containing cargo manifest
|
||||
# - lib_name: cargo package name
|
||||
# - any_symbol_name: name of any exported symbol from the library.
|
||||
# used on windows to force linking with library.
|
||||
function(apply_cargokit target manifest_dir lib_name any_symbol_name)
|
||||
|
||||
set(CARGOKIT_LIB_NAME "${lib_name}")
|
||||
set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}")
|
||||
if (CMAKE_CONFIGURATION_TYPES)
|
||||
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>")
|
||||
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/${CARGOKIT_LIB_FULL_NAME}")
|
||||
else()
|
||||
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}")
|
||||
endif()
|
||||
set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build")
|
||||
|
||||
if (FLUTTER_TARGET_PLATFORM)
|
||||
set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}")
|
||||
else()
|
||||
set(CARGOKIT_TARGET_PLATFORM "windows-x64")
|
||||
endif()
|
||||
|
||||
set(CARGOKIT_ENV
|
||||
"CARGOKIT_CMAKE=${CMAKE_COMMAND}"
|
||||
"CARGOKIT_CONFIGURATION=$<CONFIG>"
|
||||
"CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}"
|
||||
"CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}"
|
||||
"CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}"
|
||||
"CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}"
|
||||
"CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool"
|
||||
"CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
set(SCRIPT_EXTENSION ".cmd")
|
||||
set(IMPORT_LIB_EXTENSION ".lib")
|
||||
else()
|
||||
set(SCRIPT_EXTENSION ".sh")
|
||||
set(IMPORT_LIB_EXTENSION "")
|
||||
execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}")
|
||||
endif()
|
||||
|
||||
# Using generators in custom command is only supported in CMake 3.20+
|
||||
if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0")
|
||||
foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES)
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||
VERBATIM
|
||||
)
|
||||
endforeach()
|
||||
else()
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
${OUTPUT_LIB}
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE)
|
||||
|
||||
if (TARGET ${target})
|
||||
# If we have actual cmake target provided create target and make existing
|
||||
# target depend on it
|
||||
add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB})
|
||||
add_dependencies("${target}" "${target}_cargokit")
|
||||
target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}")
|
||||
if(WIN32)
|
||||
target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}")
|
||||
endif()
|
||||
else()
|
||||
# Otherwise (FFI) just use ALL to force building always
|
||||
add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB})
|
||||
endif()
|
||||
|
||||
# Allow adding the output library to plugin bundled libraries
|
||||
set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE)
|
||||
|
||||
endfunction()
|
||||
@@ -0,0 +1,27 @@
|
||||
function Resolve-Symlinks {
|
||||
[CmdletBinding()]
|
||||
[OutputType([string])]
|
||||
param(
|
||||
[Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
|
||||
[string] $Path
|
||||
)
|
||||
|
||||
[string] $separator = '/'
|
||||
[string[]] $parts = $Path.Split($separator)
|
||||
|
||||
[string] $realPath = ''
|
||||
foreach ($part in $parts) {
|
||||
if ($realPath -and !$realPath.EndsWith($separator)) {
|
||||
$realPath += $separator
|
||||
}
|
||||
$realPath += $part
|
||||
$item = Get-Item $realPath
|
||||
if ($item.Target) {
|
||||
$realPath = $item.Target.Replace('\', '/')
|
||||
}
|
||||
}
|
||||
$realPath
|
||||
}
|
||||
|
||||
$path=Resolve-Symlinks -Path $args[0]
|
||||
Write-Host $path
|
||||
@@ -0,0 +1,179 @@
|
||||
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||
|
||||
import java.nio.file.Paths
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
CargoKitPlugin.file = buildscript.sourceFile
|
||||
|
||||
apply plugin: CargoKitPlugin
|
||||
|
||||
class CargoKitExtension {
|
||||
String manifestDir; // Relative path to folder containing Cargo.toml
|
||||
String libname; // Library name within Cargo.toml. Must be a cdylib
|
||||
}
|
||||
|
||||
abstract class CargoKitBuildTask extends DefaultTask {
|
||||
|
||||
@Input
|
||||
String buildMode
|
||||
|
||||
@Input
|
||||
String buildDir
|
||||
|
||||
@Input
|
||||
String outputDir
|
||||
|
||||
@Input
|
||||
String ndkVersion
|
||||
|
||||
@Input
|
||||
String sdkDirectory
|
||||
|
||||
@Input
|
||||
int compileSdkVersion;
|
||||
|
||||
@Input
|
||||
int minSdkVersion;
|
||||
|
||||
@Input
|
||||
String pluginFile
|
||||
|
||||
@Input
|
||||
List<String> targetPlatforms
|
||||
|
||||
@TaskAction
|
||||
def build() {
|
||||
if (project.cargokit.manifestDir == null) {
|
||||
throw new GradleException("Property 'manifestDir' must be set on cargokit extension");
|
||||
}
|
||||
|
||||
if (project.cargokit.libname == null) {
|
||||
throw new GradleException("Property 'libname' must be set on cargokit extension");
|
||||
}
|
||||
|
||||
def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh"
|
||||
def path = Paths.get(new File(pluginFile).parent, "..", executableName);
|
||||
|
||||
def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir)
|
||||
|
||||
def rootProjectDir = project.rootProject.projectDir
|
||||
|
||||
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
project.exec {
|
||||
commandLine 'chmod', '+x', path
|
||||
}
|
||||
}
|
||||
|
||||
project.exec {
|
||||
executable path
|
||||
args "build-gradle"
|
||||
environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir
|
||||
environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool"
|
||||
environment "CARGOKIT_MANIFEST_DIR", manifestDir
|
||||
environment "CARGOKIT_CONFIGURATION", buildMode
|
||||
environment "CARGOKIT_TARGET_TEMP_DIR", buildDir
|
||||
environment "CARGOKIT_OUTPUT_DIR", outputDir
|
||||
environment "CARGOKIT_NDK_VERSION", ndkVersion
|
||||
environment "CARGOKIT_SDK_DIR", sdkDirectory
|
||||
environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion
|
||||
environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion
|
||||
environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",")
|
||||
environment "CARGOKIT_JAVA_HOME", System.properties['java.home']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CargoKitPlugin implements Plugin<Project> {
|
||||
|
||||
static String file;
|
||||
|
||||
private Plugin findFlutterPlugin(Project rootProject) {
|
||||
_findFlutterPlugin(rootProject.childProjects)
|
||||
}
|
||||
|
||||
private Plugin _findFlutterPlugin(Map projects) {
|
||||
for (project in projects) {
|
||||
for (plugin in project.value.getPlugins()) {
|
||||
if (plugin.class.name == "com.flutter.gradle.FlutterPlugin") {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
def plugin = _findFlutterPlugin(project.value.childProjects);
|
||||
if (plugin != null) {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
void apply(Project project) {
|
||||
def plugin = findFlutterPlugin(project.rootProject);
|
||||
|
||||
project.extensions.create("cargokit", CargoKitExtension)
|
||||
|
||||
if (plugin == null) {
|
||||
print("Flutter plugin not found, CargoKit plugin will not be applied.")
|
||||
return;
|
||||
}
|
||||
|
||||
def cargoBuildDir = "${project.buildDir}/build"
|
||||
|
||||
// Determine if the project is an application or library
|
||||
def isApplication = plugin.project.plugins.hasPlugin('com.android.application')
|
||||
def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants
|
||||
|
||||
variants.all { variant ->
|
||||
|
||||
final buildType = variant.buildType.name
|
||||
|
||||
def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}";
|
||||
def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs;
|
||||
jniLibs.srcDir(new File(cargoOutputDir))
|
||||
|
||||
def platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect()
|
||||
|
||||
// Same thing addFlutterDependencies does in flutter.gradle
|
||||
if (buildType == "debug") {
|
||||
platforms.add("android-x86")
|
||||
platforms.add("android-x64")
|
||||
}
|
||||
|
||||
// The task name depends on plugin properties, which are not available
|
||||
// at this point
|
||||
project.getGradle().afterProject {
|
||||
def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}";
|
||||
|
||||
if (project.tasks.findByName(taskName)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.project.android.ndkVersion == null) {
|
||||
throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.")
|
||||
}
|
||||
|
||||
def task = project.tasks.create(taskName, CargoKitBuildTask.class) {
|
||||
buildMode = variant.buildType.name
|
||||
buildDir = cargoBuildDir
|
||||
outputDir = cargoOutputDir
|
||||
ndkVersion = plugin.project.android.ndkVersion
|
||||
sdkDirectory = plugin.project.android.sdkDirectory
|
||||
minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int
|
||||
compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int
|
||||
targetPlatforms = platforms
|
||||
pluginFile = CargoKitPlugin.file
|
||||
}
|
||||
def onTask = { newTask ->
|
||||
if (newTask.name == "merge${buildType.capitalize()}NativeLibs") {
|
||||
newTask.dependsOn task
|
||||
// Fix gradle 7.4.2 not picking up JNI library changes
|
||||
newTask.outputs.upToDateWhen { false }
|
||||
}
|
||||
}
|
||||
project.tasks.each onTask
|
||||
project.tasks.whenTaskAdded onTask
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
setlocal ENABLEDELAYEDEXPANSION
|
||||
|
||||
SET BASEDIR=%~dp0
|
||||
|
||||
if not exist "%CARGOKIT_TOOL_TEMP_DIR%" (
|
||||
mkdir "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||
)
|
||||
cd /D "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||
|
||||
SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool
|
||||
SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart
|
||||
|
||||
set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/%
|
||||
|
||||
(
|
||||
echo name: build_tool_runner
|
||||
echo version: 1.0.0
|
||||
echo publish_to: none
|
||||
echo.
|
||||
echo environment:
|
||||
echo sdk: '^>=3.0.0 ^<4.0.0'
|
||||
echo.
|
||||
echo dependencies:
|
||||
echo build_tool:
|
||||
echo path: %BUILD_TOOL_PKG_DIR_POSIX%
|
||||
) >pubspec.yaml
|
||||
|
||||
if not exist bin (
|
||||
mkdir bin
|
||||
)
|
||||
|
||||
(
|
||||
echo import 'package:build_tool/build_tool.dart' as build_tool;
|
||||
echo void main^(List^<String^> args^) ^{
|
||||
echo build_tool.runMain^(args^);
|
||||
echo ^}
|
||||
) >bin\build_tool_runner.dart
|
||||
|
||||
SET PRECOMPILED=bin\build_tool_runner.dill
|
||||
|
||||
REM To detect changes in package we compare output of DIR /s (recursive)
|
||||
set PREV_PACKAGE_INFO=.dart_tool\package_info.prev
|
||||
set CUR_PACKAGE_INFO=.dart_tool\package_info.cur
|
||||
|
||||
DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig"
|
||||
|
||||
REM Last line in dir output is free space on harddrive. That is bound to
|
||||
REM change between invocation so we need to remove it
|
||||
(
|
||||
Set "Line="
|
||||
For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do (
|
||||
SetLocal EnableDelayedExpansion
|
||||
If Defined Line Echo !Line!
|
||||
EndLocal
|
||||
Set "Line=%%A")
|
||||
) >"%CUR_PACKAGE_INFO%"
|
||||
DEL "%CUR_PACKAGE_INFO%_orig"
|
||||
|
||||
REM Compare current directory listing with previous
|
||||
FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1
|
||||
|
||||
If %ERRORLEVEL% neq 0 (
|
||||
REM Changed - copy current to previous and remove precompiled kernel
|
||||
if exist "%PREV_PACKAGE_INFO%" (
|
||||
DEL "%PREV_PACKAGE_INFO%"
|
||||
)
|
||||
MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%"
|
||||
if exist "%PRECOMPILED%" (
|
||||
DEL "%PRECOMPILED%"
|
||||
)
|
||||
)
|
||||
|
||||
REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO%
|
||||
REM which means we need to do pub get and precompile
|
||||
if not exist "%PRECOMPILED%" (
|
||||
echo Running pub get in "%cd%"
|
||||
"%DART%" pub get --no-precompile
|
||||
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||
)
|
||||
|
||||
"%DART%" "%PRECOMPILED%" %*
|
||||
|
||||
REM 253 means invalid snapshot version.
|
||||
If %ERRORLEVEL% equ 253 (
|
||||
"%DART%" pub get --no-precompile
|
||||
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||
"%DART%" "%PRECOMPILED%" %*
|
||||
)
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
BASEDIR=$(dirname "$0")
|
||||
|
||||
mkdir -p "$CARGOKIT_TOOL_TEMP_DIR"
|
||||
|
||||
cd "$CARGOKIT_TOOL_TEMP_DIR"
|
||||
|
||||
# Write a very simple bin package in temp folder that depends on build_tool package
|
||||
# from Cargokit. This is done to ensure that we don't pollute Cargokit folder
|
||||
# with .dart_tool contents.
|
||||
|
||||
BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool"
|
||||
|
||||
if [[ -z $FLUTTER_ROOT ]]; then # not defined
|
||||
DART=dart
|
||||
else
|
||||
DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart"
|
||||
fi
|
||||
|
||||
cat << EOF > "pubspec.yaml"
|
||||
name: build_tool_runner
|
||||
version: 1.0.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
build_tool:
|
||||
path: "$BUILD_TOOL_PKG_DIR"
|
||||
EOF
|
||||
|
||||
mkdir -p "bin"
|
||||
|
||||
cat << EOF > "bin/build_tool_runner.dart"
|
||||
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||
void main(List<String> args) {
|
||||
build_tool.runMain(args);
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create alias for `shasum` if it does not exist and `sha1sum` exists
|
||||
if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then
|
||||
shopt -s expand_aliases
|
||||
alias shasum="sha1sum"
|
||||
fi
|
||||
|
||||
# Dart run will not cache any package that has a path dependency, which
|
||||
# is the case for our build_tool_runner. So instead we precompile the package
|
||||
# ourselves.
|
||||
# To invalidate the cached kernel we use the hash of ls -LR of the build_tool
|
||||
# package directory. This should be good enough, as the build_tool package
|
||||
# itself is not meant to have any path dependencies.
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||
else
|
||||
PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||
fi
|
||||
|
||||
PACKAGE_HASH_FILE=".package_hash"
|
||||
|
||||
if [ -f "$PACKAGE_HASH_FILE" ]; then
|
||||
EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE")
|
||||
if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then
|
||||
rm "$PACKAGE_HASH_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run pub get if needed.
|
||||
if [ ! -f "$PACKAGE_HASH_FILE" ]; then
|
||||
"$DART" pub get --no-precompile
|
||||
"$DART" compile kernel bin/build_tool_runner.dart
|
||||
echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE"
|
||||
fi
|
||||
|
||||
set +e
|
||||
|
||||
"$DART" bin/build_tool_runner.dill "$@"
|
||||
|
||||
exit_code=$?
|
||||
|
||||
# 253 means invalid snapshot version.
|
||||
if [ $exit_code == 253 ]; then
|
||||
"$DART" pub get --no-precompile
|
||||
"$DART" compile kernel bin/build_tool_runner.dart
|
||||
"$DART" bin/build_tool_runner.dill "$@"
|
||||
exit_code=$?
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
@@ -0,0 +1 @@
|
||||
// Empty file required by CocoaPods to create a framework.
|
||||
@@ -0,0 +1,28 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'elcaju_core'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'Rust core for El Caju Cashu wallet.'
|
||||
s.description = 'Flutter bridge to elcaju_core Rust crate via flutter_rust_bridge.'
|
||||
s.homepage = 'https://github.com/AlejandroCastillejo/elcaju'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Javier Forte' => 'forte11cuba@gmail.com' }
|
||||
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'Flutter'
|
||||
s.platform = :ios, '11.0'
|
||||
s.swift_version = '5.0'
|
||||
|
||||
s.script_phase = {
|
||||
:name => 'Build Rust library',
|
||||
:script => 'bash "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust elcaju_core',
|
||||
:execution_position => :before_compile,
|
||||
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||
:output_files => ["${BUILT_PRODUCTS_DIR}/libelcaju_core.a"],
|
||||
}
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||
'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libelcaju_core.a',
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(PROJECT_NAME "rust_builder")
|
||||
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||
|
||||
include("../cargokit/cmake/cargokit.cmake")
|
||||
apply_cargokit(${PROJECT_NAME} ../../rust elcaju_core "")
|
||||
|
||||
set(rust_builder_bundled_libraries
|
||||
"${${PROJECT_NAME}_cargokit_lib}"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
// Empty file required by CocoaPods to create a framework.
|
||||
@@ -0,0 +1,28 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'elcaju_core'
|
||||
s.version = '0.1.0'
|
||||
s.summary = 'Rust core for El Caju Cashu wallet.'
|
||||
s.description = 'Flutter bridge to elcaju_core Rust crate via flutter_rust_bridge.'
|
||||
s.homepage = 'https://github.com/AlejandroCastillejo/elcaju'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Javier Forte' => 'forte11cuba@gmail.com' }
|
||||
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'FlutterMacOS'
|
||||
s.platform = :osx, '10.15'
|
||||
s.swift_version = '5.0'
|
||||
|
||||
s.script_phase = {
|
||||
:name => 'Build Rust library',
|
||||
:script => 'bash "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust elcaju_core',
|
||||
:execution_position => :before_compile,
|
||||
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||
:output_files => ["${BUILT_PRODUCTS_DIR}/libelcaju_core.a"],
|
||||
}
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||
'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libelcaju_core.a',
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
name: rust_builder
|
||||
description: Build bridge for elcaju_core Rust crate.
|
||||
version: 0.1.0
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
flutter: '>=3.3.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_rust_bridge: 2.11.1
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
platforms:
|
||||
android:
|
||||
ffiPlugin: true
|
||||
ios:
|
||||
ffiPlugin: true
|
||||
linux:
|
||||
ffiPlugin: true
|
||||
macos:
|
||||
ffiPlugin: true
|
||||
windows:
|
||||
ffiPlugin: true
|
||||
@@ -0,0 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(PROJECT_NAME "rust_builder")
|
||||
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||
|
||||
include("../cargokit/cmake/cargokit.cmake")
|
||||
apply_cargokit(${PROJECT_NAME} ../../rust elcaju_core "")
|
||||
|
||||
set(rust_builder_bundled_libraries
|
||||
"${${PROJECT_NAME}_cargokit_lib}"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
Reference in New Issue
Block a user