diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index 860cba69..11198cad 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -79,3 +79,19 @@ jobs: with: name: linux-debian path: chameleonultragui/debian/packages + + build-macos: + runs-on: macos-latest + defaults: + run: + working-directory: ./chameleonultragui + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Enable macOS + run: flutter config --enable-macos-desktop + - name: Install tools + run: brew install automake libtool create-dmg + - run: flutter build macos --release \ No newline at end of file diff --git a/.github/workflows/publish-app.yml b/.github/workflows/publish-app.yml index 36ac7c03..2775eabb 100644 --- a/.github/workflows/publish-app.yml +++ b/.github/workflows/publish-app.yml @@ -6,17 +6,6 @@ branches: - main workflow_dispatch: - inputs: - publish_track: - description: 'Publishing Track' - required: true - default: 'production' - type: choice - options: - - 'production' - - 'beta' - - 'alpha' - - 'internal' jobs: build-android: @@ -85,8 +74,8 @@ fetch-depth: 100 - uses: subosito/flutter-action@v2 with: - flutter-version: '3.10.6' # https://github.com/flutter/flutter/issues/132725 - channel: 'any' + channel: 'stable' + - run: sudo chown -R $USER . && sudo chmod -R a+rwx . - name: Enable macOS run: flutter config --enable-macos-desktop - name: Install tools @@ -114,7 +103,9 @@ keychain initialize keychain add-certificates xcode-project use-profiles + sudo chown -R $USER . && sudo chmod -R a+rwx . flutter build macos --release --build-number ${{ github.run_number }} --build-name "1.0.${{ github.run_number }}" + sudo chown -R $USER . && sudo chmod -R a+rwx . APP_NAME=$(find $(pwd) -name "*.app") PACKAGE_NAME=$(basename "$APP_NAME" .app).pkg xcrun productbuild --component "$APP_NAME" /Applications/ unsigned.pkg @@ -125,6 +116,7 @@ | .common_name][0]' \ | xargs) xcrun productsign --sign "$INSTALLER_CERT_NAME" unsigned.pkg "$PACKAGE_NAME" + sudo chown -R $USER . && sudo chmod -R a+rwx . rm -f unsigned.pkg app-store-connect publish --path "$PACKAGE_NAME" while [[ -z "$BUILD_ID" ]]; do diff --git a/README.md b/README.md index aacf7eaa..d4549b4b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Chameleon Ultra GUI A GUI for the Chameleon Ultra/Chameleon Lite written in Flutter for cross platform operation -[![Auto build](https://github.com/GameTec-live/ChameleonUltraGUI/actions/workflows/buildapp.yml/badge.svg)](https://github.com/GameTec-live/ChameleonUltraGUI/actions/workflows/buildapp.yml) +[![Auto build](https://github.com/GameTec-live/ChameleonUltraGUI/actions/workflows/build-app.yml/badge.svg)](https://github.com/GameTec-live/ChameleonUltraGUI/actions/workflows/build-app.yml) [![Open collective](https://opencollective.com/chameleon-ultra-gui/tiers/badge.svg)](https://opencollective.com/chameleon-ultra-gui#support) ## Installation @@ -10,7 +10,7 @@ You can download the latest builds from GitHub Actions [here](https://github.com App available in those stores: - Google Play: https://play.google.com/store/apps/details?id=io.chameleon.ultra - F-Store: not yet -- App Store: https://apps.apple.com/app/chameleon-ultra-gui/id6462919364 (macOS only) +- App Store: https://apps.apple.com/app/chameleon-ultra-gui/id6462919364 - Arch Linux (AUR): https://aur.archlinux.org/packages/chameleonultragui / https://aur.archlinux.org/packages/chameleonultragui-git - Flathub: not yet - Chocolatey (Windows): not yet @@ -39,7 +39,7 @@ Contributions are welcome, most stuff that needs to be done can either be found ## Translations -If you want to collaborate by adding your language to the application, you can do it through [our Crowdin project](https://crowdin.com/project/chameleon-ultra-gui). +If you want to collaborate by adding your language to the application, you can do it through [our Crowdin project](https://crowdin.com/project/chameleon-ultra-gui). Do not contribute files into `chameleonultragui/lib/l100n/app_*.arb`. All translations should be added only to Crowdin. If your language is missing, you can create issue and ask to enable it. "Chameleon Ultra GUI", "Chameleon" and other trademarks should not be translated. ## Screenshots ![Connect Page](/screenshots/1.png) diff --git a/chameleonultragui/lib/bridge/dfu.dart b/chameleonultragui/lib/bridge/dfu.dart index 404cda98..5a5724fe 100644 --- a/chameleonultragui/lib/bridge/dfu.dart +++ b/chameleonultragui/lib/bridge/dfu.dart @@ -255,7 +255,7 @@ class DFUCommunicator { for (var offset = 0; offset < firmwareBytes.length; offset += length) { var tries = 0; var crcBackup = crc; - for (; tries < 10; tries++) { + for (; tries < ((Platform.isIOS) ? 50 : 10); tries++) { await createObject( objectType, min(firmwareBytes.length - offset, length)); diff --git a/chameleonultragui/lib/connector/serial_abstract.dart b/chameleonultragui/lib/connector/serial_abstract.dart index f9ffdbab..dafa31bb 100644 --- a/chameleonultragui/lib/connector/serial_abstract.dart +++ b/chameleonultragui/lib/connector/serial_abstract.dart @@ -24,6 +24,7 @@ class AbstractSerial { bool connected = false; bool isOpen = false; bool isDFU = false; + bool pendingConnection = false; String portName = "None"; ConnectionType connectionType = ConnectionType.none; dynamic messageCallback; diff --git a/chameleonultragui/lib/connector/serial_android.dart b/chameleonultragui/lib/connector/serial_android.dart index 5a04cf5b..a7c92b49 100644 --- a/chameleonultragui/lib/connector/serial_android.dart +++ b/chameleonultragui/lib/connector/serial_android.dart @@ -33,7 +33,6 @@ class AndroidSerial extends AbstractSerial { @override Future connectSpecificDevice(devicePort) async { if (devicePort.contains(":")) { - log.d(devicePort); return bleSerial.connectSpecificDevice(devicePort); } else { return mobileSerial.connectSpecificDevice(devicePort); @@ -103,4 +102,11 @@ class AndroidSerial extends AbstractSerial { @override bool get isDFU => (bleSerial.isDFU || mobileSerial.isDFU); + + @override + bool get pendingConnection => bleSerial.pendingConnection; + + @override + set pendingConnection(pendingConnection) => + {bleSerial.pendingConnection = pendingConnection}; } diff --git a/chameleonultragui/lib/connector/serial_ble.dart b/chameleonultragui/lib/connector/serial_ble.dart index b6fefd8a..1d4d42e4 100644 --- a/chameleonultragui/lib/connector/serial_ble.dart +++ b/chameleonultragui/lib/connector/serial_ble.dart @@ -28,8 +28,8 @@ class BLESerial extends AbstractSerial { @override Future availableDevices() async { - if (inSearch && Platform.isIOS) { - log.w("Multiple searches in one time not allowed on iOS"); + if (inSearch) { + log.w("Multiple searches in one time not allowed! FIXME"); return []; } @@ -140,6 +140,7 @@ class BLESerial extends AbstractSerial { } await performDisconnect(); + pendingConnection = true; connection = flutterReactiveBle .connectToAdvertisingDevice( id: devicePort, @@ -149,9 +150,9 @@ class BLESerial extends AbstractSerial { .listen((connectionState) async { log.w(connectionState); if (connectionState.connectionState == DeviceConnectionState.connected) { - connected = true; - if (chameleonMap[devicePort]!.dfu) { + connected = true; + pendingConnection = false; txCharacteristic = QualifiedCharacteristic( serviceId: dfuUUID, characteristicId: dfuControl, @@ -167,7 +168,8 @@ class BLESerial extends AbstractSerial { "Received unexpected data: ${bytesToHex(Uint8List.fromList(data))}"); } } - }, onError: (dynamic error) { + }, onError: (dynamic error) async { + await performDisconnect(); log.e(error); }); @@ -185,6 +187,7 @@ class BLESerial extends AbstractSerial { device = chameleonMap[devicePort]!.device; isDFU = true; + completer.complete(true); } else { txCharacteristic = QualifiedCharacteristic( serviceId: nrfUUID, @@ -201,7 +204,8 @@ class BLESerial extends AbstractSerial { "Received unexpected data: ${bytesToHex(Uint8List.fromList(data))}"); } } - }, onError: (dynamic error) { + }, onError: (dynamic error) async { + await performDisconnect(); log.e(error); }); @@ -210,17 +214,31 @@ class BLESerial extends AbstractSerial { characteristicId: uartRX, deviceId: connectionState.deviceId); - portName = devicePort; - device = chameleonMap[devicePort]!.device; + try { + await flutterReactiveBle.writeCharacteristicWithResponse( + rxCharacteristic!, + value: Uint8List(0)); - connectionType = ConnectionType.ble; - isDFU = false; - } + connected = true; + portName = devicePort; + device = chameleonMap[devicePort]!.device; - completer.complete(true); + connectionType = ConnectionType.ble; + isDFU = false; + + completer.complete(true); + } catch (_) { + try { + completer.complete(false); + } catch (_) {} + } + } } else if (connectionState.connectionState == DeviceConnectionState.disconnected) { - completer.complete(false); + await performDisconnect(); + try { + completer.complete(false); + } catch (_) {} } }, onError: (Object error) { log.e(error); @@ -236,6 +254,7 @@ class BLESerial extends AbstractSerial { connectionType = ConnectionType.none; isOpen = false; messageCallback = null; + pendingConnection = false; if (connection != null) { await connection!.cancel(); connected = false; diff --git a/chameleonultragui/lib/gui/page/connect.dart b/chameleonultragui/lib/gui/page/connect.dart index 4243a1c0..6a4c61e1 100644 --- a/chameleonultragui/lib/gui/page/connect.dart +++ b/chameleonultragui/lib/gui/page/connect.dart @@ -17,9 +17,10 @@ class ConnectPage extends StatelessWidget { var appState = context.watch(); // Get State var localizations = AppLocalizations.of(context)!; return FutureBuilder( - future: appState.connector.connected - ? Future.value([]) - : appState.connector.availableChameleons(false), + future: + (appState.connector.connected || appState.connector.pendingConnection) + ? Future.value([]) + : appState.connector.availableChameleons(false), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Scaffold( @@ -114,11 +115,17 @@ class ConnectPage extends StatelessWidget { ), ); } else { + if (chameleonDevice.type == + ConnectionType.ble) { + appState.connector.pendingConnection = true; + appState.changesMade(); + } await appState.connector .connectSpecificDevice( chameleonDevice.port); appState.communicator = ChameleonCommunicator( port: appState.connector); + appState.connector.pendingConnection = false; appState.changesMade(); } }, diff --git a/chameleonultragui/lib/gui/page/home.dart b/chameleonultragui/lib/gui/page/home.dart index b0b0a2ad..0210d0b2 100644 --- a/chameleonultragui/lib/gui/page/home.dart +++ b/chameleonultragui/lib/gui/page/home.dart @@ -28,11 +28,11 @@ class HomePageState extends State { Future<(Icon, String, List, bool)> getFutureData() async { var appState = context.read(); - List<(TagType, TagType)> usedSlots; + List<(TagType, TagType)> usedSlots = []; try { usedSlots = await appState.communicator!.getUsedSlots(); - } catch (_) { - usedSlots = []; + } catch (e) { + appState.log.e(e); } return ( diff --git a/chameleonultragui/lib/gui/page/pending_connection.dart b/chameleonultragui/lib/gui/page/pending_connection.dart new file mode 100644 index 00000000..e9f4dac9 --- /dev/null +++ b/chameleonultragui/lib/gui/page/pending_connection.dart @@ -0,0 +1,42 @@ +import 'package:chameleonultragui/main.dart'; +import 'package:flutter/material.dart'; + +// Localizations +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +class PendingConnectionPage extends StatelessWidget { + const PendingConnectionPage({super.key}); + + @override + Widget build(BuildContext context) { + var appState = context.watch(); + var localizations = AppLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + title: Text(localizations.connect), + ), + body: Center( + child: + Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + const CircularProgressIndicator(), + const SizedBox(height: 25), + Text( + localizations.connecting_to_ble, + ), + const SizedBox(height: 10), + if (!appState.connector.connected) ...[ + Text( + localizations.default_ble_password, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Text( + localizations.connection_might_take_some_time, + style: const TextStyle(fontWeight: FontWeight.bold), + ) + ], + ]))); + } +} diff --git a/chameleonultragui/lib/gui/page/saved_cards.dart b/chameleonultragui/lib/gui/page/saved_cards.dart index a1384eeb..891cb0fb 100644 --- a/chameleonultragui/lib/gui/page/saved_cards.dart +++ b/chameleonultragui/lib/gui/page/saved_cards.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:chameleonultragui/gui/widget/staggered_grid_view.dart'; import 'package:chameleonultragui/helpers/general.dart'; import 'package:chameleonultragui/helpers/mifare_classic.dart'; import 'package:chameleonultragui/main.dart'; @@ -10,7 +11,6 @@ import 'package:file_picker/file_picker.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:uuid/uuid.dart'; import 'package:chameleonultragui/gui/menu/card_edit.dart'; import 'package:chameleonultragui/gui/menu/dictionary_edit.dart'; @@ -154,7 +154,8 @@ class SavedCardsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text(localizations.correct_tag_deta), + title: + Text(localizations.correct_tag_data), content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -168,21 +169,24 @@ class SavedCardsPageState extends State { controller: uid4Controller, decoration: InputDecoration( labelText: localizations.uid, - hintText: localizations.enter_something("UID")), + hintText: localizations + .enter_something("UID")), ), const SizedBox(height: 20), TextFormField( controller: sak4Controller, decoration: InputDecoration( labelText: localizations.sak, - hintText: localizations.enter_something("SAK")), + hintText: localizations + .enter_something("SAK")), ), const SizedBox(height: 20), TextFormField( controller: atqa4Controller, decoration: InputDecoration( labelText: localizations.atqa, - hintText: localizations.enter_something("ATQA")), + hintText: localizations + .enter_something("ATQA")), ), const SizedBox(height: 40), ]), @@ -193,22 +197,26 @@ class SavedCardsPageState extends State { controller: uid7Controller, decoration: InputDecoration( labelText: localizations.uid, - hintText: localizations.enter_something("UID")), + hintText: localizations + .enter_something("UID")), ), const SizedBox(height: 20), TextFormField( controller: sak7Controller, decoration: InputDecoration( labelText: localizations.sak, - hintText: localizations.enter_something("SAK (08)")), + hintText: localizations + .enter_something( + "SAK (08)")), ), const SizedBox(height: 20), TextFormField( controller: atqa7Controller, decoration: InputDecoration( labelText: localizations.atqa, - hintText: - localizations.enter_something("ATQA (00 44)")), + hintText: localizations + .enter_something( + "ATQA (00 44)")), ), const SizedBox(height: 40) ]), @@ -216,7 +224,8 @@ class SavedCardsPageState extends State { controller: nameController, decoration: InputDecoration( labelText: localizations.name, - hintText: localizations.enter_name), + hintText: + localizations.enter_name), ), DropdownButton( value: selectedType, @@ -282,8 +291,8 @@ class SavedCardsPageState extends State { appState.changesMade(); Navigator.pop(context); }, - child: - Text(localizations.save_as("4 byte UID")), + child: Text(localizations + .save_as("4 byte UID")), ), ElevatedButton( onPressed: () async { @@ -319,8 +328,8 @@ class SavedCardsPageState extends State { Navigator.pop( context); // Close the modal after saving }, - child: - Text(localizations.save_as("7 byte UID")), + child: Text(localizations + .save_as("7 byte UID")), ), ElevatedButton( onPressed: () { @@ -365,9 +374,9 @@ class SavedCardsPageState extends State { Text( "${localizations.tag_type}: ${chameleonTagToString(tag.tag)}"), Text( - "${localizations.sak}: ${tag.sak == 0 ? localizations.unavialable : tag.sak}"), + "${localizations.sak}: ${tag.sak == 0 ? localizations.unavailable : tag.sak}"), Text( - "${localizations.atqa}: ${tag.atqa.asMap().containsKey(0) ? tag.atqa[0] : ""} ${tag.atqa.asMap().containsKey(1) ? tag.atqa[1] : localizations.unavialable}"), + "${localizations.atqa}: ${tag.atqa.asMap().containsKey(0) ? tag.atqa[0] : ""} ${tag.atqa.asMap().containsKey(1) ? tag.atqa[1] : localizations.unavailable}"), ], ), actions: [ @@ -388,8 +397,8 @@ class SavedCardsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text( - localizations.select_save_format), + title: Text(localizations + .select_save_format), actions: [ ElevatedButton( onPressed: () async { @@ -397,8 +406,8 @@ class SavedCardsPageState extends State { tag, appState, true); Navigator.pop(context); }, - child: Text( - localizations.save_as(".bin")), + child: Text(localizations + .save_as(".bin")), ), ElevatedButton( onPressed: () async { @@ -406,9 +415,8 @@ class SavedCardsPageState extends State { tag, appState, false); Navigator.pop(context); }, - child: Text( - localizations - .save_as(".json")), + child: Text(localizations + .save_as(".json")), ), ], ); @@ -521,8 +529,8 @@ class SavedCardsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text( - localizations.select_save_format), + title: Text(localizations + .select_save_format), actions: [ ElevatedButton( onPressed: () async { @@ -530,8 +538,8 @@ class SavedCardsPageState extends State { tag, appState, true); Navigator.pop(context); }, - child: Text( - localizations.save_as(".bin")), + child: Text(localizations + .save_as(".bin")), ), ElevatedButton( onPressed: () async { @@ -539,8 +547,8 @@ class SavedCardsPageState extends State { tag, appState, false); Navigator.pop(context); }, - child: Text( - localizations.save_as(".json")), + child: Text(localizations + .save_as(".json")), ), ], ); diff --git a/chameleonultragui/lib/gui/page/settings.dart b/chameleonultragui/lib/gui/page/settings.dart index de8c5f32..f70490f8 100644 --- a/chameleonultragui/lib/gui/page/settings.dart +++ b/chameleonultragui/lib/gui/page/settings.dart @@ -239,8 +239,7 @@ class SettingsMainPageState extends State { }, items: AppLocalizations.supportedLocales.map((locale) { return DropdownMenuItem( - value: - locale.toLanguageTag(), + value: locale.toLanguageTag(), child: Text(appState.sharedPreferencesProvider.getFlag(locale)), ); @@ -296,7 +295,13 @@ class SettingsMainPageState extends State { child: const Text( 'https://github.com/GameTec-live/ChameleonUltraGUI')), const SizedBox(height: 30), - Text(localizations.thanks_for_support), + GestureDetector( + onTap: () async { + await launchUrl(Uri.parse( + 'https://opencollective.com/chameleon-ultra-gui')); + }, + child: + Text(localizations.thanks_for_support)), const SizedBox(height: 10), Text(names, style: const TextStyle( diff --git a/chameleonultragui/lib/gui/page/slot_manager.dart b/chameleonultragui/lib/gui/page/slot_manager.dart index 9b8b0603..f2ec184f 100644 --- a/chameleonultragui/lib/gui/page/slot_manager.dart +++ b/chameleonultragui/lib/gui/page/slot_manager.dart @@ -13,6 +13,52 @@ import 'package:provider/provider.dart'; // Localizations import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +class Tile extends StatelessWidget { + const Tile({ + Key? key, + required this.index, + this.extent, + this.backgroundColor, + this.bottomSpace, + }) : super(key: key); + + final int index; + final double? extent; + final double? bottomSpace; + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + final child = Container( + color: backgroundColor, + height: extent, + child: Center( + child: CircleAvatar( + minRadius: 20, + maxRadius: 20, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + child: Text('$index', style: const TextStyle(fontSize: 20)), + ), + ), + ); + + if (bottomSpace == null) { + return child; + } + + return Column( + children: [ + Expanded(child: child), + Container( + height: bottomSpace, + color: Colors.green, + ) + ], + ); + } +} + class SlotManagerPage extends StatefulWidget { const SlotManagerPage({super.key}); @@ -138,93 +184,90 @@ class SlotManagerPageState extends State { future: executeNextFunction(), builder: (BuildContext context, AsyncSnapshot snapshot) { return Expanded( - child: StaggeredGridView.countBuilder( - padding: const EdgeInsets.all(20), - crossAxisCount: - MediaQuery.of(context).size.width >= 600 ? 2 : 1, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - itemCount: 8, - itemBuilder: (BuildContext context, int index) { - return Container( - constraints: const BoxConstraints(maxHeight: 120), - child: ElevatedButton( - onPressed: () { - cardSelectDialog(context, index); - }, - style: ButtonStyle( - shape: MaterialStateProperty.all< - RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), + child: AlignedGridView.count( + padding: const EdgeInsets.all(20), + crossAxisCount: + MediaQuery.of(context).size.width >= 600 ? 2 : 1, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + itemCount: 8, + itemBuilder: (BuildContext context, int index) { + return Container( + constraints: const BoxConstraints(maxHeight: 120), + child: ElevatedButton( + onPressed: () { + cardSelectDialog(context, index); + }, + style: ButtonStyle( + shape: MaterialStateProperty.all< + RoundedRectangleBorder>( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), ), ), - ), - child: Padding( - padding: const EdgeInsets.only( - top: 8.0, left: 8.0, bottom: 6.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - Icon(Icons.nfc, - color: enabledSlots[index] - ? Colors.green - : Colors.deepOrange), - const SizedBox(width: 5), - Text("${localizations.slot} ${index + 1}") - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Icon(Icons.credit_card), - const SizedBox(width: 5), - Text( - "${slotData[index]['hfName'] ?? localizations.unknown} (${chameleonTagToString(usedSlots[index].$1)})") - ], - ), - Row( - children: [ - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.start, - children: [ - const Icon(Icons.wifi), - const SizedBox(width: 5), - Text( - "${slotData[index]['lfName'] ?? localizations.unknown} (${chameleonTagToString(usedSlots[index].$2)})", - ), - ], + child: Padding( + padding: const EdgeInsets.only( + top: 8.0, left: 8.0, bottom: 6.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Icon(Icons.nfc, + color: enabledSlots[index] + ? Colors.green + : Colors.deepOrange), + const SizedBox(width: 5), + Text("${localizations.slot} ${index + 1}") + ], + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Icon(Icons.credit_card), + const SizedBox(width: 5), + Text( + "${slotData[index]['hfName'] ?? localizations.unknown} (${chameleonTagToString(usedSlots[index].$1)})") + ], + ), + Row( + children: [ + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + const Icon(Icons.wifi), + const SizedBox(width: 5), + Text( + "${slotData[index]['lfName'] ?? localizations.unknown} (${chameleonTagToString(usedSlots[index].$2)})", + ), + ], + ), ), - ), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return SlotSettings( - slot: index, - refresh: refreshSlot); - }, - ); - }, - icon: const Icon(Icons.settings), - ), - ], - ), - ], + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SlotSettings( + slot: index, + refresh: refreshSlot); + }, + ); + }, + icon: const Icon(Icons.settings), + ), + ], + ), + ], + ), ), ), - ), - ); - }, - staggeredTileBuilder: (int index) => - const StaggeredTile.fit(1), - ), + ); + }), ); }, ), @@ -446,8 +489,10 @@ class CardSearchDelegate extends SearchDelegate { setUploadState(100); - await appState.communicator!.setSlotTagName(gridPosition, - (card.name.isEmpty) ? localizations.no_name : card.name, TagFrequency.hf); + await appState.communicator!.setSlotTagName( + gridPosition, + (card.name.isEmpty) ? localizations.no_name : card.name, + TagFrequency.hf); await appState.communicator!.saveSlotData(); appState.changesMade(); refresh(gridPosition); @@ -461,8 +506,10 @@ class CardSearchDelegate extends SearchDelegate { .setDefaultDataToSlot(gridPosition, card.tag); await appState.communicator!.setEM410XEmulatorID( hexToBytes(card.uid.replaceAll(" ", ""))); - await appState.communicator!.setSlotTagName(gridPosition, - (card.name.isEmpty) ? localizations.no_name : card.name, TagFrequency.lf); + await appState.communicator!.setSlotTagName( + gridPosition, + (card.name.isEmpty) ? localizations.no_name : card.name, + TagFrequency.lf); await appState.communicator!.saveSlotData(); appState.changesMade(); refresh(gridPosition); diff --git a/chameleonultragui/lib/gui/widget/staggered_grid_view.dart b/chameleonultragui/lib/gui/widget/staggered_grid_view.dart new file mode 100644 index 00000000..57faf8c8 --- /dev/null +++ b/chameleonultragui/lib/gui/widget/staggered_grid_view.dart @@ -0,0 +1,2556 @@ +import 'dart:collection'; +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// A scrollable, 2D array of widgets with variable sizes. +/// +/// The main axis direction of a grid is the direction in which it scrolls (the +/// [scrollDirection]). +/// +/// The most commonly used grid layouts are [StaggeredGridView.count], which +/// creates a layout with a fixed number of tiles in the cross axis, and +/// [StaggeredGridView.extent], which creates a layout with tiles that have a maximum +/// cross-axis extent. A custom [SliverStaggeredGridDelegate] can produce an +/// arbitrary 2D arrangement of children. +/// +/// To create a grid with a large (or infinite) number of children, use the +/// [StaggeredGridView.builder] constructor with either a +/// [SliverStaggeredGridDelegateWithFixedCrossAxisCount] or a +/// [SliverStaggeredGridDelegateWithMaxCrossAxisExtent] for the [gridDelegate]. +/// You can also use the [StaggeredGridView.countBuilder] or +/// [StaggeredGridView.extentBuilder] constructors. +/// +/// To use a custom [SliverVariableSizeChildDelegate], use [StaggeredGridView.custom]. +/// +/// To create a linear array of children, use a [ListView]. +/// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// +/// ### Sample code +/// +/// Here are two brief snippets showing a [StaggeredGridView] and its equivalent using +/// [CustomScrollView]: +/// +/// ```dart +/// StaggeredGridView.count( +/// primary: false, +/// crossAxisCount: 4, +/// mainAxisSpacing: 4.0, +/// crossAxisSpacing: 4.0, +/// children: const [ +/// const Text('1'), +/// const Text('2'), +/// const Text('3'), +/// const Text('4'), +/// const Text('5'), +/// const Text('6'), +/// const Text('7'), +/// const Text('8'), +/// ], +/// staggeredTiles: const [ +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// ], +/// ) +/// ``` +/// +/// ```dart +/// CustomScrollView( +/// primary: false, +/// slivers: [ +/// SliverStaggeredGrid.count( +/// crossAxisCount: 4, +/// mainAxisSpacing: 4.0, +/// crossAxisSpacing: 4.0, +/// children: const [ +/// const Text('1'), +/// const Text('2'), +/// const Text('3'), +/// const Text('4'), +/// const Text('5'), +/// const Text('6'), +/// const Text('7'), +/// const Text('8'), +/// ], +/// staggeredTiles: const [ +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// ], +/// ) +/// ], +/// ) +/// ``` +/// +/// See also: +/// +/// * [SingleChildScrollView], which is a scrollable widget that has a single +/// child. +/// * [ListView], which is scrollable, linear list of widgets. +/// * [PageView], which is a scrolling list of child widgets that are each the +/// size of the viewport. +/// * [CustomScrollView], which is a scrollable widget that creates custom +/// scroll effects using slivers. +/// * [SliverStaggeredGridDelegateWithFixedCrossAxisCount], which creates a +/// layout with a fixed number of tiles in the cross axis. +/// * [SliverStaggeredGridDelegateWithMaxCrossAxisExtent], which creates a +/// layout with tiles that have a maximum cross-axis extent. +/// * [ScrollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. +class StaggeredGridView extends BoxScrollView { + /// Creates a scrollable, 2D array of widgets with a custom + /// [SliverStaggeredGridDelegate]. + /// + /// The [gridDelegate] argument must not be null. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + StaggeredGridView({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + EdgeInsetsGeometry? padding, + required this.gridDelegate, + this.addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + List children = const [], + String? restorationId, + }) : childrenDelegate = SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + restorationId: restorationId, + ); + + /// Creates a scrollable, 2D array of widgets that are created on demand. + /// + /// This constructor is appropriate for grid views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Providing a non-null [itemCount] improves the ability of the + /// [SliverStaggeredGridDelegate] to estimate the maximum scroll extent. + /// + /// [itemBuilder] will be called only with indices greater than or equal to + /// zero and less than [itemCount]. + /// + /// The [gridDelegate] argument must not be null. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverVariableSizeChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverVariableSizeChildBuilderDelegate.addRepaintBoundaries] property. Both must not + /// be null. + StaggeredGridView.builder({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + EdgeInsetsGeometry? padding, + required this.gridDelegate, + required IndexedWidgetBuilder itemBuilder, + int? itemCount, + this.addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + String? restorationId, + }) : childrenDelegate = SliverChildBuilderDelegate( + itemBuilder, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + restorationId: restorationId, + ); + + /// Creates a scrollable, 2D array of widgets with both a custom + /// [SliverStaggeredGridDelegate] and a custom [SliverVariableSizeChildDelegate]. + /// + /// To use an [IndexedWidgetBuilder] callback to build children, either use + /// a [SliverVariableSizeChildBuilderDelegate] or use the + /// [SliverStaggeredGridDelegate.builder] constructor. + /// + /// The [gridDelegate] and [childrenDelegate] arguments must not be null. + const StaggeredGridView.custom({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + EdgeInsetsGeometry? padding, + String? restorationId, + required this.gridDelegate, + required this.childrenDelegate, + this.addAutomaticKeepAlives = true, + }) : super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + restorationId: restorationId, + ); + + /// Creates a scrollable, 2D array of widgets of variable sizes with a fixed + /// number of tiles in the cross axis. + /// + /// Uses a [SliverStaggeredGridDelegateWithFixedCrossAxisCount] as the + /// [gridDelegate]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + /// + /// See also: + /// + /// * [SliverGrid.count], the equivalent constructor for [SliverGrid]. + StaggeredGridView.count({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + EdgeInsetsGeometry? padding, + required int crossAxisCount, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + this.addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + List children = const [], + List staggeredTiles = const [], + String? restorationId, + }) : gridDelegate = SliverStaggeredGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: (i) => staggeredTiles[i], + staggeredTileCount: staggeredTiles.length, + ), + childrenDelegate = SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + restorationId: restorationId, + ); + + /// Creates a scrollable, 2D array of widgets of variable sizes with a fixed + /// number of tiles in the cross axis that are created on demand. + /// + /// This constructor is appropriate for grid views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Uses a [SliverStaggeredGridDelegateWithFixedCrossAxisCount] as the + /// [gridDelegate]. + /// + /// Providing a non-null [itemCount] improves the ability of the + /// [SliverStaggeredGridDelegate] to estimate the maximum scroll extent. + /// + /// [itemBuilder] and [staggeredTileBuilder] will be called only with + /// indices greater than or equal to + /// zero and less than [itemCount]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + StaggeredGridView.countBuilder({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + EdgeInsetsGeometry? padding, + required int crossAxisCount, + required IndexedWidgetBuilder itemBuilder, + required IndexedStaggeredTileBuilder staggeredTileBuilder, + int? itemCount, + double? cacheExtent, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + this.addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + String? restorationId, + }) : gridDelegate = SliverStaggeredGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: staggeredTileBuilder, + staggeredTileCount: itemCount, + ), + childrenDelegate = SliverChildBuilderDelegate( + itemBuilder, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + restorationId: restorationId, + cacheExtent: cacheExtent, + ); + + /// Creates a scrollable, 2D array of widgets of variable sizes with tiles + /// that each have a maximum cross-axis extent. + /// + /// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate]. + /// + /// Providing a non-null [itemCount] improves the ability of the + /// [SliverStaggeredGridDelegate] to estimate the maximum scroll extent. + /// + /// [itemBuilder] and [staggeredTileBuilder] will be called only with + /// indices greater than or equal to + /// zero and less than [itemCount]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + /// + /// See also: + /// + /// * [SliverGrid.extent], the equivalent constructor for [SliverGrid]. + StaggeredGridView.extent({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + EdgeInsetsGeometry? padding, + required double maxCrossAxisExtent, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + this.addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + List children = const [], + List staggeredTiles = const [], + String? restorationId, + }) : gridDelegate = SliverStaggeredGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: (i) => staggeredTiles[i], + staggeredTileCount: staggeredTiles.length, + ), + childrenDelegate = SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + restorationId: restorationId, + ); + + /// Creates a scrollable, 2D array of widgets of variable sizes with tiles + /// that each have a maximum cross-axis extent that are created on demand. + /// + /// This constructor is appropriate for grid views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverVariableSizeChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + /// + /// See also: + /// + /// * [SliverGrid.extent], the equivalent constructor for [SliverGrid]. + StaggeredGridView.extentBuilder({ + Key? key, + Axis scrollDirection = Axis.vertical, + bool reverse = false, + ScrollController? controller, + bool? primary, + ScrollPhysics? physics, + bool shrinkWrap = false, + EdgeInsetsGeometry? padding, + required double maxCrossAxisExtent, + required IndexedWidgetBuilder itemBuilder, + required IndexedStaggeredTileBuilder staggeredTileBuilder, + int? itemCount, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + this.addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + String? restorationId, + }) : gridDelegate = SliverStaggeredGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: staggeredTileBuilder, + staggeredTileCount: itemCount, + ), + childrenDelegate = SliverChildBuilderDelegate( + itemBuilder, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + ), + super( + key: key, + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + restorationId: restorationId, + ); + + /// A delegate that controls the layout of the children within the + /// [StaggeredGridView]. + /// + /// The [StaggeredGridView] and [StaggeredGridView.custom] constructors let you specify this + /// delegate explicitly. The other constructors create a [gridDelegate] + /// implicitly. + final SliverStaggeredGridDelegate gridDelegate; + + /// A delegate that provides the children for the [StaggeredGridView]. + /// + /// The [StaggeredGridView.custom] constructor lets you specify this delegate + /// explicitly. The other constructors create a [childrenDelegate] that wraps + /// the given child list. + final SliverChildDelegate childrenDelegate; + + /// Whether to add keepAlives to children + final bool addAutomaticKeepAlives; + + @override + Widget buildChildLayout(BuildContext context) { + return SliverStaggeredGrid( + delegate: childrenDelegate, + gridDelegate: gridDelegate, + addAutomaticKeepAlives: addAutomaticKeepAlives, + ); + } +} + +//dependencies: + +/// Signature for a function that creates [StaggeredTile] for a given index. +typedef IndexedStaggeredTileBuilder = StaggeredTile? Function(int index); + +/// Specifies how a staggered grid is configured. +@immutable +class StaggeredGridConfiguration { + /// Creates an object that holds the configuration of a staggered grid. + const StaggeredGridConfiguration({ + required this.crossAxisCount, + required this.staggeredTileBuilder, + required this.cellExtent, + required this.mainAxisSpacing, + required this.crossAxisSpacing, + required this.reverseCrossAxis, + required this.staggeredTileCount, + this.mainAxisOffsetsCacheSize = 3, + }) : assert(crossAxisCount > 0), + assert(cellExtent >= 0), + assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0), + assert(mainAxisOffsetsCacheSize > 0), + cellStride = cellExtent + crossAxisSpacing; + + /// The maximum number of children in the cross axis. + final int crossAxisCount; + + /// The number of pixels from the leading edge of one cell to the trailing + /// edge of the same cell in both axis. + final double cellExtent; + + /// The number of logical pixels between each child along the main axis. + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + final double crossAxisSpacing; + + /// Called to get the tile at the specified index for the + /// [SliverGridStaggeredTileLayout]. + final IndexedStaggeredTileBuilder staggeredTileBuilder; + + /// The total number of tiles this delegate can provide. + /// + /// If null, the number of tiles is determined by the least index for which + /// [builder] returns null. + final int? staggeredTileCount; + + /// Whether the children should be placed in the opposite order of increasing + /// coordinates in the cross axis. + /// + /// For example, if the cross axis is horizontal, the children are placed from + /// left to right when [reverseCrossAxis] is false and from right to left when + /// [reverseCrossAxis] is true. + /// + /// Typically set to the return value of [axisDirectionIsReversed] applied to + /// the [SliverConstraints.crossAxisDirection]. + final bool reverseCrossAxis; + + final double cellStride; + + /// The number of pages necessary to cache a mainAxisOffsets value. + final int mainAxisOffsetsCacheSize; + + List generateMainAxisOffsets() => + List.generate(crossAxisCount, (i) => 0.0); + + /// Gets a normalized tile for the given index. + StaggeredTile? getStaggeredTile(int index) { + StaggeredTile? tile; + if (staggeredTileCount == null || index < staggeredTileCount!) { + // There is maybe a tile for this index. + tile = _normalizeStaggeredTile(staggeredTileBuilder(index)); + } + return tile; + } + + /// Computes the main axis extent of any staggered tile. + double _getStaggeredTileMainAxisExtent(StaggeredTile tile) { + return tile.mainAxisExtent ?? + (tile.mainAxisCellCount! * cellExtent) + + (tile.mainAxisCellCount! - 1) * mainAxisSpacing; + } + + /// Creates a staggered tile with the computed extent from the given tile. + StaggeredTile? _normalizeStaggeredTile(StaggeredTile? staggeredTile) { + if (staggeredTile == null) { + return null; + } else { + final crossAxisCellCount = + staggeredTile.crossAxisCellCount.clamp(0, crossAxisCount).toInt(); + if (staggeredTile.fitContent) { + return StaggeredTile.fit(crossAxisCellCount); + } else { + return StaggeredTile.extent( + crossAxisCellCount, _getStaggeredTileMainAxisExtent(staggeredTile)); + } + } + } +} + +class _Block { + const _Block(this.index, this.crossAxisCount, this.minOffset, this.maxOffset); + + final int index; + final int crossAxisCount; + final double minOffset; + final double maxOffset; +} + +const double _epsilon = 0.0001; + +bool _nearEqual(double d1, double d2) { + return (d1 - d2).abs() < _epsilon; +} + +/// Describes the placement of a child in a [RenderSliverStaggeredGrid]. +/// +/// See also: +/// +/// * [RenderSliverStaggeredGrid], which uses this class during its +/// [RenderSliverStaggeredGrid.performLayout] method. +@immutable +class SliverStaggeredGridGeometry { + /// Creates an object that describes the placement of a child in a [RenderSliverStaggeredGrid]. + const SliverStaggeredGridGeometry({ + required this.scrollOffset, + required this.crossAxisOffset, + required this.mainAxisExtent, + required this.crossAxisExtent, + required this.crossAxisCellCount, + required this.blockIndex, + }); + + /// The scroll offset of the leading edge of the child relative to the leading + /// edge of the parent. + final double scrollOffset; + + /// The offset of the child in the non-scrolling axis. + /// + /// If the scroll axis is vertical, this offset is from the left-most edge of + /// the parent to the left-most edge of the child. If the scroll axis is + /// horizontal, this offset is from the top-most edge of the parent to the + /// top-most edge of the child. + final double crossAxisOffset; + + /// The extent of the child in the scrolling axis. + /// + /// If the scroll axis is vertical, this extent is the child's height. If the + /// scroll axis is horizontal, this extent is the child's width. + final double? mainAxisExtent; + + /// The extent of the child in the non-scrolling axis. + /// + /// If the scroll axis is vertical, this extent is the child's width. If the + /// scroll axis is horizontal, this extent is the child's height. + final double crossAxisExtent; + + final int crossAxisCellCount; + + final int blockIndex; + + bool get hasTrailingScrollOffset => mainAxisExtent != null; + + /// The scroll offset of the trailing edge of the child relative to the + /// leading edge of the parent. + double get trailingScrollOffset => scrollOffset + (mainAxisExtent ?? 0); + + SliverStaggeredGridGeometry copyWith({ + double? scrollOffset, + double? crossAxisOffset, + double? mainAxisExtent, + double? crossAxisExtent, + int? crossAxisCellCount, + int? blockIndex, + }) { + return SliverStaggeredGridGeometry( + scrollOffset: scrollOffset ?? this.scrollOffset, + crossAxisOffset: crossAxisOffset ?? this.crossAxisOffset, + mainAxisExtent: mainAxisExtent ?? this.mainAxisExtent, + crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent, + crossAxisCellCount: crossAxisCellCount ?? this.crossAxisCellCount, + blockIndex: blockIndex ?? this.blockIndex, + ); + } + + /// Returns a tight [BoxConstraints] that forces the child to have the + /// required size. + BoxConstraints getBoxConstraints(SliverConstraints constraints) { + return constraints.asBoxConstraints( + minExtent: mainAxisExtent ?? 0.0, + maxExtent: mainAxisExtent ?? double.infinity, + crossAxisExtent: crossAxisExtent, + ); + } + + @override + String toString() { + return 'SliverStaggeredGridGeometry(' + 'scrollOffset: $scrollOffset, ' + 'crossAxisOffset: $crossAxisOffset, ' + 'mainAxisExtent: $mainAxisExtent, ' + 'crossAxisExtent: $crossAxisExtent, ' + 'crossAxisCellCount: $crossAxisCellCount, ' + 'startIndex: $blockIndex)'; + } +} + +/// A sliver that places multiple box children in a two dimensional arrangement. +/// +/// [RenderSliverGrid] places its children in arbitrary positions determined by +/// [gridDelegate]. Each child is forced to have the size specified by the +/// [gridDelegate]. +/// +/// See also: +/// +/// * [RenderSliverList], which places its children in a linear +/// array. +/// * [RenderSliverFixedExtentList], which places its children in a linear +/// array with a fixed extent in the main axis. +class RenderSliverStaggeredGrid extends RenderSliverVariableSizeBoxAdaptor { + /// Creates a sliver that contains multiple box children that whose size and + /// position are determined by a delegate. + /// + /// The [configuration] and [childManager] arguments must not be null. + RenderSliverStaggeredGrid({ + required RenderSliverVariableSizeBoxChildManager childManager, + required SliverStaggeredGridDelegate gridDelegate, + }) : _gridDelegate = gridDelegate, + _pageSizeToViewportOffsets = + HashMap>(), + super(childManager: childManager); + + @override + void setupParentData(RenderObject child) { + if (child.parentData is! SliverVariableSizeBoxAdaptorParentData) { + final data = SliverVariableSizeBoxAdaptorParentData(); + + // By default we will keep it true. + //data.keepAlive = true; + child.parentData = data; + } + } + + /// The delegate that controls the configuration of the staggered grid. + SliverStaggeredGridDelegate get gridDelegate => _gridDelegate; + SliverStaggeredGridDelegate _gridDelegate; + set gridDelegate(SliverStaggeredGridDelegate value) { + if (_gridDelegate == value) { + return; + } + if (value.runtimeType != _gridDelegate.runtimeType || + value.shouldRelayout(_gridDelegate)) { + markNeedsLayout(); + } + _gridDelegate = value; + } + + final HashMap> + _pageSizeToViewportOffsets; + + @override + void performLayout() { + childManager.didStartLayout(); + childManager.setDidUnderflow(false); + + final double scrollOffset = + constraints.scrollOffset + constraints.cacheOrigin; + assert(scrollOffset >= 0.0); + final double remainingExtent = constraints.remainingCacheExtent; + assert(remainingExtent >= 0.0); + final double targetEndScrollOffset = scrollOffset + remainingExtent; + + bool reachedEnd = false; + double trailingScrollOffset = 0; + double leadingScrollOffset = double.infinity; + bool visible = false; + int firstIndex = 0; + int lastIndex = 0; + + final configuration = _gridDelegate.getConfiguration(constraints); + + final pageSize = configuration.mainAxisOffsetsCacheSize * + constraints.viewportMainAxisExtent; + if (pageSize == 0.0) { + geometry = SliverGeometry.zero; + childManager.didFinishLayout(); + return; + } + final pageIndex = scrollOffset ~/ pageSize; + assert(pageIndex >= 0); + + // If the viewport is resized, we keep the in memory the old offsets caches. (Useful if only the orientation changes multiple times). + final viewportOffsets = _pageSizeToViewportOffsets.putIfAbsent( + pageSize, () => SplayTreeMap()); + + _ViewportOffsets? viewportOffset; + if (viewportOffsets.isEmpty) { + viewportOffset = + _ViewportOffsets(configuration.generateMainAxisOffsets(), pageSize); + viewportOffsets[0] = viewportOffset; + } else { + final smallestKey = viewportOffsets.lastKeyBefore(pageIndex + 1); + viewportOffset = viewportOffsets[smallestKey!]; + } + + // A staggered grid always have to layout the child from the zero-index based one to the last visible. + final mainAxisOffsets = viewportOffset!.mainAxisOffsets.toList(); + final visibleIndices = HashSet(); + + // Iterate through all children while they can be visible. + for (var index = viewportOffset.firstChildIndex; + mainAxisOffsets.any((o) => o <= targetEndScrollOffset); + index++) { + SliverStaggeredGridGeometry? geometry = + getSliverStaggeredGeometry(index, configuration, mainAxisOffsets); + if (geometry == null) { + // There are either no children, or we are past the end of all our children. + reachedEnd = true; + break; + } + + final bool hasTrailingScrollOffset = geometry.hasTrailingScrollOffset; + RenderBox? child; + if (!hasTrailingScrollOffset) { + // Layout the child to compute its tailingScrollOffset. + final constraints = + BoxConstraints.tightFor(width: geometry.crossAxisExtent); + child = addAndLayoutChild(index, constraints, parentUsesSize: true); + geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child!)); + } + + if (!visible && + targetEndScrollOffset >= geometry.scrollOffset && + scrollOffset <= geometry.trailingScrollOffset) { + visible = true; + leadingScrollOffset = geometry.scrollOffset; + firstIndex = index; + } + + if (visible && hasTrailingScrollOffset) { + child = + addAndLayoutChild(index, geometry.getBoxConstraints(constraints)); + } + + if (child != null) { + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + childParentData.layoutOffset = geometry.scrollOffset; + childParentData.crossAxisOffset = geometry.crossAxisOffset; + assert(childParentData.index == index); + } + + if (visible && indices.contains(index)) { + visibleIndices.add(index); + } + + if (geometry.trailingScrollOffset >= + viewportOffset!.trailingScrollOffset) { + final nextPageIndex = viewportOffset.pageIndex + 1; + final nextViewportOffset = _ViewportOffsets(mainAxisOffsets, + (nextPageIndex + 1) * pageSize, nextPageIndex, index); + viewportOffsets[nextPageIndex] = nextViewportOffset; + viewportOffset = nextViewportOffset; + } + + final double endOffset = + geometry.trailingScrollOffset + configuration.mainAxisSpacing; + for (var i = 0; i < geometry.crossAxisCellCount; i++) { + mainAxisOffsets[i + geometry.blockIndex] = endOffset; + } + + trailingScrollOffset = mainAxisOffsets.reduce(math.max); + lastIndex = index; + } + + collectGarbage(visibleIndices); + + if (!visible) { + if (scrollOffset > viewportOffset!.trailingScrollOffset) { + // We are outside the bounds, we have to correct the scroll. + final viewportOffsetScrollOffset = pageSize * viewportOffset.pageIndex; + final correction = viewportOffsetScrollOffset - scrollOffset; + geometry = SliverGeometry( + scrollOffsetCorrection: correction, + ); + } else { + geometry = SliverGeometry.zero; + childManager.didFinishLayout(); + } + return; + } + + double estimatedMaxScrollOffset; + if (reachedEnd) { + estimatedMaxScrollOffset = trailingScrollOffset; + } else { + estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset( + constraints, + firstIndex: firstIndex, + lastIndex: lastIndex, + leadingScrollOffset: leadingScrollOffset, + trailingScrollOffset: trailingScrollOffset, + ); + assert(estimatedMaxScrollOffset >= + trailingScrollOffset - leadingScrollOffset); + } + + final double paintExtent = calculatePaintOffset( + constraints, + from: leadingScrollOffset, + to: trailingScrollOffset, + ); + final double cacheExtent = calculateCacheOffset( + constraints, + from: leadingScrollOffset, + to: trailingScrollOffset, + ); + + geometry = SliverGeometry( + scrollExtent: estimatedMaxScrollOffset, + paintExtent: paintExtent, + cacheExtent: cacheExtent, + maxPaintExtent: estimatedMaxScrollOffset, + // Conservative to avoid flickering away the clip during scroll. + hasVisualOverflow: trailingScrollOffset > targetEndScrollOffset || + constraints.scrollOffset > 0.0, + ); + + // We may have started the layout while scrolled to the end, which would not + // expose a child. + if (estimatedMaxScrollOffset == trailingScrollOffset) { + childManager.setDidUnderflow(true); + } + childManager.didFinishLayout(); + } + + static SliverStaggeredGridGeometry? getSliverStaggeredGeometry(int index, + StaggeredGridConfiguration configuration, List offsets) { + final tile = configuration.getStaggeredTile(index); + if (tile == null) { + return null; + } + + final block = _findFirstAvailableBlockWithCrossAxisCount( + tile.crossAxisCellCount, offsets); + + final scrollOffset = block.minOffset; + var blockIndex = block.index; + if (configuration.reverseCrossAxis) { + blockIndex = + configuration.crossAxisCount - tile.crossAxisCellCount - blockIndex; + } + final crossAxisOffset = blockIndex * configuration.cellStride; + final geometry = SliverStaggeredGridGeometry( + scrollOffset: scrollOffset, + crossAxisOffset: crossAxisOffset, + mainAxisExtent: tile.mainAxisExtent, + crossAxisExtent: configuration.cellStride * tile.crossAxisCellCount - + configuration.crossAxisSpacing, + crossAxisCellCount: tile.crossAxisCellCount, + blockIndex: block.index, + ); + return geometry; + } + + /// Finds the first available block with at least the specified [crossAxisCount] in the [offsets] list. + static _Block _findFirstAvailableBlockWithCrossAxisCount( + int crossAxisCount, List offsets) { + return _findFirstAvailableBlockWithCrossAxisCountAndOffsets( + crossAxisCount, List.from(offsets)); + } + + /// Finds the first available block with at least the specified [crossAxisCount]. + static _Block _findFirstAvailableBlockWithCrossAxisCountAndOffsets( + int crossAxisCount, List offsets) { + final block = _findFirstAvailableBlock(offsets); + if (block.crossAxisCount < crossAxisCount) { + // Not enough space for the specified cross axis count. + // We have to fill this block and try again. + for (var i = 0; i < block.crossAxisCount; ++i) { + offsets[i + block.index] = block.maxOffset; + } + return _findFirstAvailableBlockWithCrossAxisCountAndOffsets( + crossAxisCount, offsets); + } else { + return block; + } + } + + /// Finds the first available block for the specified [offsets] list. + static _Block _findFirstAvailableBlock(List offsets) { + int index = 0; + double minBlockOffset = double.infinity; + double maxBlockOffset = double.infinity; + int crossAxisCount = 1; + bool contiguous = false; + + // We have to use the _nearEqual function because of floating-point arithmetic. + // Ex: 0.1 + 0.2 = 0.30000000000000004 and not 0.3. + + for (var i = index; i < offsets.length; ++i) { + final offset = offsets[i]; + if (offset < minBlockOffset && !_nearEqual(offset, minBlockOffset)) { + index = i; + maxBlockOffset = minBlockOffset; + minBlockOffset = offset; + crossAxisCount = 1; + contiguous = true; + } else if (_nearEqual(offset, minBlockOffset) && contiguous) { + crossAxisCount++; + } else if (offset < maxBlockOffset && + offset > minBlockOffset && + !_nearEqual(offset, minBlockOffset)) { + contiguous = false; + maxBlockOffset = offset; + } else { + contiguous = false; + } + } + + return _Block(index, crossAxisCount, minBlockOffset, maxBlockOffset); + } +} + +class _ViewportOffsets { + _ViewportOffsets( + List mainAxisOffsets, + this.trailingScrollOffset, [ + this.pageIndex = 0, + this.firstChildIndex = 0, + ]) : mainAxisOffsets = mainAxisOffsets.toList(); + + final int pageIndex; + + final int firstChildIndex; + + final double trailingScrollOffset; + + final List mainAxisOffsets; + + @override + String toString() => + '[$pageIndex-$trailingScrollOffset] ($firstChildIndex, $mainAxisOffsets)'; +} + +/// Creates staggered grid layouts. +/// +/// This delegate creates grids with variable sized but equally spaced tiles. +/// +/// See also: +/// +/// * [StaggeredGridView], which can use this delegate to control the layout of its +/// tiles. +/// * [SliverStaggeredGrid], which can use this delegate to control the layout of its +/// tiles. +/// * [RenderSliverStaggeredGrid], which can use this delegate to control the layout of +/// its tiles. +abstract class SliverStaggeredGridDelegate { + /// Creates a delegate that makes staggered grid layouts + /// + /// All of the arguments must not be null. The [mainAxisSpacing] and + /// [crossAxisSpacing] arguments must not be negative. + const SliverStaggeredGridDelegate({ + required this.staggeredTileBuilder, + this.mainAxisSpacing = 0, + this.crossAxisSpacing = 0, + this.staggeredTileCount, + }) : assert(mainAxisSpacing >= 0), + assert(crossAxisSpacing >= 0); + + /// The number of logical pixels between each child along the main axis. + final double mainAxisSpacing; + + /// The number of logical pixels between each child along the cross axis. + final double crossAxisSpacing; + + /// Called to get the tile at the specified index for the + /// [RenderSliverStaggeredGrid]. + final IndexedStaggeredTileBuilder staggeredTileBuilder; + + /// The total number of tiles this delegate can provide. + /// + /// If null, the number of tiles is determined by the least index for which + /// [builder] returns null. + final int? staggeredTileCount; + + bool _debugAssertIsValid() { + assert(mainAxisSpacing >= 0); + assert(crossAxisSpacing >= 0); + return true; + } + + /// Returns information about the staggered grid configuration. + StaggeredGridConfiguration getConfiguration(SliverConstraints constraints); + + /// Override this method to return true when the children need to be + /// laid out. + /// + /// This should compare the fields of the current delegate and the given + /// `oldDelegate` and return true if the fields are such that the layout would + /// be different. + bool shouldRelayout(SliverStaggeredGridDelegate oldDelegate) { + return oldDelegate.mainAxisSpacing != mainAxisSpacing || + oldDelegate.crossAxisSpacing != crossAxisSpacing || + oldDelegate.staggeredTileCount != staggeredTileCount || + oldDelegate.staggeredTileBuilder != staggeredTileBuilder; + } +} + +/// Creates staggered grid layouts with a fixed number of cells in the cross +/// axis. +/// +/// For example, if the grid is vertical, this delegate will create a layout +/// with a fixed number of columns. If the grid is horizontal, this delegate +/// will create a layout with a fixed number of rows. +/// +/// This delegate creates grids with variable sized but equally spaced tiles. +/// +/// See also: +/// +/// * [SliverStaggeredGridDelegate], which creates staggered grid layouts. +/// * [StaggeredGridView], which can use this delegate to control the layout of its +/// tiles. +/// * [SliverStaggeredGrid], which can use this delegate to control the layout of its +/// tiles. +/// * [RenderSliverStaggeredGrid], which can use this delegate to control the layout of +/// its tiles. +class SliverStaggeredGridDelegateWithFixedCrossAxisCount + extends SliverStaggeredGridDelegate { + /// Creates a delegate that makes staggered grid layouts with a fixed number + /// of tiles in the cross axis. + /// + /// All of the arguments must not be null. The [mainAxisSpacing] and + /// [crossAxisSpacing] arguments must not be negative. The [crossAxisCount] + /// argument must be greater than zero. + const SliverStaggeredGridDelegateWithFixedCrossAxisCount({ + required this.crossAxisCount, + required IndexedStaggeredTileBuilder staggeredTileBuilder, + double mainAxisSpacing = 0, + double crossAxisSpacing = 0, + int? staggeredTileCount, + }) : assert(crossAxisCount > 0), + super( + staggeredTileBuilder: staggeredTileBuilder, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileCount: staggeredTileCount, + ); + + /// The number of children in the cross axis. + final int crossAxisCount; + + @override + bool _debugAssertIsValid() { + assert(crossAxisCount > 0); + return super._debugAssertIsValid(); + } + + @override + StaggeredGridConfiguration getConfiguration(SliverConstraints constraints) { + assert(_debugAssertIsValid()); + final double usableCrossAxisExtent = + constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1); + final double cellExtent = usableCrossAxisExtent / crossAxisCount; + return StaggeredGridConfiguration( + crossAxisCount: crossAxisCount, + staggeredTileBuilder: staggeredTileBuilder, + staggeredTileCount: staggeredTileCount, + cellExtent: cellExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), + ); + } + + @override + bool shouldRelayout( + covariant SliverStaggeredGridDelegateWithFixedCrossAxisCount + oldDelegate) { + return oldDelegate.crossAxisCount != crossAxisCount || + super.shouldRelayout(oldDelegate); + } +} + +/// Creates staggered grid layouts with tiles that each have a maximum +/// cross-axis extent. +/// +/// This delegate will select a cross-axis extent for the tiles that is as +/// large as possible subject to the following conditions: +/// +/// - The extent evenly divides the cross-axis extent of the grid. +/// - The extent is at most [maxCrossAxisExtent]. +/// +/// For example, if the grid is vertical, the grid is 500.0 pixels wide, and +/// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4 +/// columns that are 125.0 pixels wide. +/// +/// This delegate creates grids with variable sized but equally spaced tiles. +/// +/// See also: +/// +/// * [SliverStaggeredGridDelegate], which creates staggered grid layouts. +/// * [StaggeredGridView], which can use this delegate to control the layout of its +/// tiles. +/// * [SliverStaggeredGrid], which can use this delegate to control the layout of its +/// tiles. +/// * [RenderSliverStaggeredGrid], which can use this delegate to control the layout of +/// its tiles. +class SliverStaggeredGridDelegateWithMaxCrossAxisExtent + extends SliverStaggeredGridDelegate { + /// Creates a delegate that makes staggered grid layouts with tiles that + /// have a maximum cross-axis extent. + /// + /// All of the arguments must not be null. The [maxCrossAxisExtent], + /// [mainAxisSpacing] and [crossAxisSpacing] arguments must not be negative. + const SliverStaggeredGridDelegateWithMaxCrossAxisExtent({ + required this.maxCrossAxisExtent, + required IndexedStaggeredTileBuilder staggeredTileBuilder, + double mainAxisSpacing = 0, + double crossAxisSpacing = 0, + int? staggeredTileCount, + }) : assert(maxCrossAxisExtent > 0), + super( + staggeredTileBuilder: staggeredTileBuilder, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileCount: staggeredTileCount, + ); + + /// The maximum extent of tiles in the cross axis. + /// + /// This delegate will select a cross-axis extent for the tiles that is as + /// large as possible subject to the following conditions: + /// + /// - The extent evenly divides the cross-axis extent of the grid. + /// - The extent is at most [maxCrossAxisExtent]. + /// + /// For example, if the grid is vertical, the grid is 500.0 pixels wide, and + /// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4 + /// columns that are 125.0 pixels wide. + final double maxCrossAxisExtent; + + @override + bool _debugAssertIsValid() { + assert(maxCrossAxisExtent >= 0); + return super._debugAssertIsValid(); + } + + @override + StaggeredGridConfiguration getConfiguration(SliverConstraints constraints) { + assert(_debugAssertIsValid()); + final int crossAxisCount = + ((constraints.crossAxisExtent + crossAxisSpacing) / + (maxCrossAxisExtent + crossAxisSpacing)) + .ceil(); + + final double usableCrossAxisExtent = + constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1); + + final double cellExtent = usableCrossAxisExtent / crossAxisCount; + return StaggeredGridConfiguration( + crossAxisCount: crossAxisCount, + staggeredTileBuilder: staggeredTileBuilder, + staggeredTileCount: staggeredTileCount, + cellExtent: cellExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), + ); + } + + @override + bool shouldRelayout( + covariant SliverStaggeredGridDelegateWithMaxCrossAxisExtent oldDelegate) { + return oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || + super.shouldRelayout(oldDelegate); + } +} + +/// Holds the dimensions of a [StaggeredGridView]'s tile. +/// +/// A [StaggeredTile] always overlaps an exact number of cells in the cross +/// axis of a [StaggeredGridView]. +/// The main axis extent can either be a number of pixels or a number of +/// cells to overlap. +class StaggeredTile { + /// Creates a [StaggeredTile] with the given [crossAxisCellCount] and + /// [mainAxisCellCount]. + /// + /// The main axis extent of this tile will be the length of + /// [mainAxisCellCount] cells (inner spacings included). + const StaggeredTile.count(this.crossAxisCellCount, this.mainAxisCellCount) + : assert(crossAxisCellCount >= 0), + assert(mainAxisCellCount != null && mainAxisCellCount >= 0), + mainAxisExtent = null; + + /// Creates a [StaggeredTile] with the given [crossAxisCellCount] and + /// [mainAxisExtent]. + /// + /// This tile will have a fixed main axis extent. + const StaggeredTile.extent(this.crossAxisCellCount, this.mainAxisExtent) + : assert(crossAxisCellCount >= 0), + assert(mainAxisExtent != null && mainAxisExtent >= 0), + mainAxisCellCount = null; + + /// Creates a [StaggeredTile] with the given [crossAxisCellCount] that + /// fit its main axis extent to its content. + /// + /// This tile will have a fixed main axis extent. + const StaggeredTile.fit(this.crossAxisCellCount) + : assert(crossAxisCellCount >= 0), + mainAxisExtent = null, + mainAxisCellCount = null; + + /// The number of cells occupied in the cross axis. + final int crossAxisCellCount; + + /// The number of cells occupied in the main axis. + final double? mainAxisCellCount; + + /// The number of pixels occupied in the main axis. + final double? mainAxisExtent; + + bool get fitContent => mainAxisCellCount == null && mainAxisExtent == null; +} + +/// A delegate used by [RenderSliverVariableSizeBoxAdaptor] to manage its children. +/// +/// [RenderSliverVariableSizeBoxAdaptor] objects reify their children lazily to avoid +/// spending resources on children that are not visible in the viewport. This +/// delegate lets these objects create and remove children as well as estimate +/// the total scroll offset extent occupied by the full child list. +abstract class RenderSliverVariableSizeBoxChildManager { + /// Called during layout when a new child is needed. The child should be + /// inserted into the child list in the appropriate position. Its index and + /// scroll offsets will automatically be set appropriately. + /// + /// The `index` argument gives the index of the child to show. It is possible + /// for negative indices to be requested. For example: if the user scrolls + /// from child 0 to child 10, and then those children get much smaller, and + /// then the user scrolls back up again, this method will eventually be asked + /// to produce a child for index -1. + /// + /// If no child corresponds to `index`, then do nothing. + /// + /// Which child is indicated by index zero depends on the [GrowthDirection] + /// specified in the [RenderSliverVariableSizeBoxAdaptor.constraints]. For example + /// if the children are the alphabet, then if + /// [SliverConstraints.growthDirection] is [GrowthDirection.forward] then + /// index zero is A, and index 25 is Z. On the other hand if + /// [SliverConstraints.growthDirection] is [GrowthDirection.reverse] + /// then index zero is Z, and index 25 is A. + /// + /// During a call to [createChild] it is valid to remove other children from + /// the [RenderSliverVariableSizeBoxAdaptor] object if they were not created during + /// this frame and have not yet been updated during this frame. It is not + /// valid to add any other children to this render object. + /// + /// If this method does not create a child for a given `index` greater than or + /// equal to zero, then [computeMaxScrollOffset] must be able to return a + /// precise value. + void createChild(int index); + + /// Remove the given child from the child list. + /// + /// Called by [RenderSliverVariableSizeBoxAdaptor.collectGarbage], which itself is + /// called from [RenderSliverVariableSizeBoxAdaptor.performLayout]. + /// + /// The index of the given child can be obtained using the + /// [RenderSliverVariableSizeBoxAdaptor.indexOf] method, which reads it from the + /// [SliverVariableSizeBoxAdaptorParentData.index] field of the child's + /// [RenderObject.parentData]. + void removeChild(RenderBox child); + + /// Called to estimate the total scrollable extents of this object. + /// + /// Must return the total distance from the start of the child with the + /// earliest possible index to the end of the child with the last possible + /// index. + double estimateMaxScrollOffset( + SliverConstraints constraints, { + int? firstIndex, + int? lastIndex, + double? leadingScrollOffset, + double? trailingScrollOffset, + }); + + /// Called to obtain a precise measure of the total number of children. + /// + /// Must return the number that is one greater than the greatest `index` for + /// which `createChild` will actually create a child. + /// + /// This is used when [createChild] cannot add a child for a positive `index`, + /// to determine the precise dimensions of the sliver. It must return an + /// accurate and precise non-null value. It will not be called if + /// [createChild] is always able to create a child (e.g. for an infinite + /// list). + int get childCount; + + /// Called during [RenderSliverVariableSizeBoxAdaptor.adoptChild]. + /// + /// Subclasses must ensure that the [SliverVariableSizeBoxAdaptorParentData.index] + /// field of the child's [RenderObject.parentData] accurately reflects the + /// child's index in the child list after this function returns. + void didAdoptChild(RenderBox child); + + /// Called during layout to indicate whether this object provided insufficient + /// children for the [RenderSliverVariableSizeBoxAdaptor] to fill the + /// [SliverConstraints.remainingPaintExtent]. + /// + /// Typically called unconditionally at the start of layout with false and + /// then later called with true when the [RenderSliverVariableSizeBoxAdaptor] + /// fails to create a child required to fill the + /// [SliverConstraints.remainingPaintExtent]. + /// + /// Useful for subclasses to determine whether newly added children could + /// affect the visible contents of the [RenderSliverVariableSizeBoxAdaptor]. + // ignore: avoid_positional_boolean_parameters + void setDidUnderflow(bool value); + + /// Called at the beginning of layout to indicate that layout is about to + /// occur. + void didStartLayout() {} + + /// Called at the end of layout to indicate that layout is now complete. + void didFinishLayout() {} + + /// In debug mode, asserts that this manager is not expecting any + /// modifications to the [RenderSliverVariableSizeBoxAdaptor]'s child list. + /// + /// This function always returns true. + /// + /// The manager is not required to track whether it is expecting modifications + /// to the [RenderSliverVariableSizeBoxAdaptor]'s child list and can simply return + /// true without making any assertions. + bool debugAssertChildListLocked() => true; +} + +/// Parent data structure used by [RenderSliverVariableSizeBoxAdaptor]. +class SliverVariableSizeBoxAdaptorParentData + extends SliverMultiBoxAdaptorParentData { + /// The offset of the child in the non-scrolling axis. + /// + /// If the scroll axis is vertical, this offset is from the left-most edge of + /// the parent to the left-most edge of the child. If the scroll axis is + /// horizontal, this offset is from the top-most edge of the parent to the + /// top-most edge of the child. + late double crossAxisOffset; + + /// Whether the widget is currently in the + /// [RenderSliverVariableSizeBoxAdaptor._keepAliveBucket]. + bool _keptAlive = false; + + @override + String toString() => 'crossAxisOffset=$crossAxisOffset; ${super.toString()}'; +} + +/// A sliver with multiple variable size box children. +/// +/// [RenderSliverVariableSizeBoxAdaptor] is a base class for slivers that have multiple +/// variable size box children. The children are managed by a [RenderSliverBoxChildManager], +/// which lets subclasses create children lazily during layout. Typically +/// subclasses will create only those children that are actually needed to fill +/// the [SliverConstraints.remainingPaintExtent]. +/// +/// The contract for adding and removing children from this render object is +/// more strict than for normal render objects: +/// +/// * Children can be removed except during a layout pass if they have already +/// been laid out during that layout pass. +/// * Children cannot be added except during a call to [childManager], and +/// then only if there is no child corresponding to that index (or the child +/// child corresponding to that index was first removed). +/// +/// See also: +/// +/// * [RenderSliverToBoxAdapter], which has a single box child. +/// * [RenderSliverList], which places its children in a linear +/// array. +/// * [RenderSliverFixedExtentList], which places its children in a linear +/// array with a fixed extent in the main axis. +/// * [RenderSliverGrid], which places its children in arbitrary positions. +abstract class RenderSliverVariableSizeBoxAdaptor extends RenderSliver + with + TileContainerRenderObjectMixin, + RenderSliverWithKeepAliveMixin, + RenderSliverHelpers { + /// Creates a sliver with multiple box children. + /// + /// The [childManager] argument must not be null. + RenderSliverVariableSizeBoxAdaptor( + {required RenderSliverVariableSizeBoxChildManager childManager}) + : _childManager = childManager; + + @override + void setupParentData(RenderObject child) { + if (child.parentData is! SliverVariableSizeBoxAdaptorParentData) { + child.parentData = SliverVariableSizeBoxAdaptorParentData(); + } + } + + /// The delegate that manages the children of this object. + /// + /// Rather than having a concrete list of children, a + /// [RenderSliverVariableSizeBoxAdaptor] uses a [RenderSliverVariableSizeBoxChildManager] to + /// create children during layout in order to fill the + /// [SliverConstraints.remainingPaintExtent]. + @protected + RenderSliverVariableSizeBoxChildManager get childManager => _childManager; + final RenderSliverVariableSizeBoxChildManager _childManager; + + /// The nodes being kept alive despite not being visible. + final Map _keepAliveBucket = {}; + + @override + void adoptChild(RenderObject child) { + super.adoptChild(child); + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + if (!childParentData._keptAlive) { + childManager.didAdoptChild(child as RenderBox); + } + } + + bool _debugAssertChildListLocked() => + childManager.debugAssertChildListLocked(); + + @override + void remove(int index) { + final RenderBox? child = this[index]; + + // if child is null, it means this element was cached - drop the cached element + if (child == null) { + final RenderBox? cachedChild = _keepAliveBucket[index]; + if (cachedChild != null) { + dropChild(cachedChild); + _keepAliveBucket.remove(index); + } + return; + } + + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + if (!childParentData._keptAlive) { + super.remove(index); + return; + } + assert(_keepAliveBucket[childParentData.index!] == child); + _keepAliveBucket.remove(childParentData.index); + dropChild(child); + } + + @override + void removeAll() { + super.removeAll(); + _keepAliveBucket.values.forEach(dropChild); + _keepAliveBucket.clear(); + } + + void _createOrObtainChild(int index) { + invokeLayoutCallback((SliverConstraints constraints) { + assert(constraints == this.constraints); + if (_keepAliveBucket.containsKey(index)) { + final RenderBox child = _keepAliveBucket.remove(index)!; + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + assert(childParentData._keptAlive); + dropChild(child); + child.parentData = childParentData; + this[index] = child; + childParentData._keptAlive = false; + } else { + _childManager.createChild(index); + } + }); + } + + void _destroyOrCacheChild(int index) { + final RenderBox child = this[index]!; + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + if (childParentData.keepAlive) { + assert(!childParentData._keptAlive); + remove(index); + _keepAliveBucket[childParentData.index!] = child; + child.parentData = childParentData; + super.adoptChild(child); + childParentData._keptAlive = true; + } else { + assert(child.parent == this); + _childManager.removeChild(child); + assert(child.parent == null); + } + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _keepAliveBucket.values.forEach((child) => child.attach(owner)); + } + + @override + void detach() { + super.detach(); + _keepAliveBucket.values.forEach((child) => child.detach()); + } + + @override + void redepthChildren() { + super.redepthChildren(); + _keepAliveBucket.values.forEach(redepthChild); + } + + @override + void visitChildren(RenderObjectVisitor visitor) { + super.visitChildren(visitor); + _keepAliveBucket.values.forEach(visitor); + } + + bool addChild(int index) { + assert(_debugAssertChildListLocked()); + _createOrObtainChild(index); + final child = this[index]; + if (child != null) { + assert(indexOf(child) == index); + return true; + } + childManager.setDidUnderflow(true); + return false; + } + + RenderBox? addAndLayoutChild( + int index, + BoxConstraints childConstraints, { + bool parentUsesSize = false, + }) { + assert(_debugAssertChildListLocked()); + _createOrObtainChild(index); + final child = this[index]; + if (child != null) { + assert(indexOf(child) == index); + child.layout(childConstraints, parentUsesSize: parentUsesSize); + return child; + } + childManager.setDidUnderflow(true); + return null; + } + + /// Called after layout with the number of children that can be garbage + /// collected at the head and tail of the child list. + /// + /// Children whose [SliverVariableSizeBoxAdaptorParentData.keepAlive] property is + /// set to true will be removed to a cache instead of being dropped. + /// + /// This method also collects any children that were previously kept alive but + /// are now no longer necessary. As such, it should be called every time + /// [performLayout] is run, even if the arguments are both zero. + @protected + void collectGarbage(Set visibleIndices) { + assert(_debugAssertChildListLocked()); + assert(childCount >= visibleIndices.length); + invokeLayoutCallback((SliverConstraints constraints) { + // We destroy only those which are not visible. + indices.toSet().difference(visibleIndices).forEach(_destroyOrCacheChild); + + // Ask the child manager to remove the children that are no longer being + // kept alive. (This should cause _keepAliveBucket to change, so we have + // to prepare our list ahead of time.) + _keepAliveBucket.values + .where((RenderBox child) { + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + return !childParentData.keepAlive; + }) + .toList() + .forEach(_childManager.removeChild); + assert(_keepAliveBucket.values.where((RenderBox child) { + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + return !childParentData.keepAlive; + }).isEmpty); + }); + } + + /// Returns the index of the given child, as given by the + /// [SliverVariableSizeBoxAdaptorParentData.index] field of the child's [parentData]. + int indexOf(RenderBox child) { + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + assert(childParentData.index != null); + return childParentData.index!; + } + + /// Returns the dimension of the given child in the main axis, as given by the + /// child's [RenderBox.size] property. This is only valid after layout. + @protected + double paintExtentOf(RenderBox child) { + assert(child.hasSize); + switch (constraints.axis) { + case Axis.horizontal: + return child.size.width; + case Axis.vertical: + return child.size.height; + } + } + + @override + bool hitTestChildren(HitTestResult result, + {required double mainAxisPosition, required double crossAxisPosition}) { + for (final child in children) { + if (hitTestBoxChild(BoxHitTestResult.wrap(result), child, + mainAxisPosition: mainAxisPosition, + crossAxisPosition: crossAxisPosition)) { + return true; + } + } + return false; + } + + @override + double childMainAxisPosition(RenderBox child) { + return childScrollOffset(child)! - constraints.scrollOffset; + } + + @override + double childCrossAxisPosition(RenderBox child) { + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + return childParentData.crossAxisOffset; + } + + @override + double? childScrollOffset(RenderObject child) { + assert(child.parent == this); + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + assert(childParentData.layoutOffset != null); + return childParentData.layoutOffset; + } + + @override + void applyPaintTransform(RenderObject child, Matrix4 transform) { + applyPaintTransformForBoxChild(child as RenderBox, transform); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (childCount == 0) { + return; + } + // offset is to the top-left corner, regardless of our axis direction. + // originOffset gives us the delta from the real origin to the origin in the axis direction. + Offset? mainAxisUnit, crossAxisUnit, originOffset; + bool? addExtent; + switch (applyGrowthDirectionToAxisDirection( + constraints.axisDirection, constraints.growthDirection)) { + case AxisDirection.up: + mainAxisUnit = const Offset(0, -1); + crossAxisUnit = const Offset(1, 0); + originOffset = offset + Offset(0, geometry!.paintExtent); + addExtent = true; + break; + case AxisDirection.right: + mainAxisUnit = const Offset(1, 0); + crossAxisUnit = const Offset(0, 1); + originOffset = offset; + addExtent = false; + break; + case AxisDirection.down: + mainAxisUnit = const Offset(0, 1); + crossAxisUnit = const Offset(1, 0); + originOffset = offset; + addExtent = false; + break; + case AxisDirection.left: + mainAxisUnit = const Offset(-1, 0); + crossAxisUnit = const Offset(0, 1); + originOffset = offset + Offset(geometry!.paintExtent, 0); + addExtent = true; + break; + } + + for (final child in children) { + final double mainAxisDelta = childMainAxisPosition(child); + final double crossAxisDelta = childCrossAxisPosition(child); + Offset childOffset = Offset( + originOffset.dx + + mainAxisUnit.dx * mainAxisDelta + + crossAxisUnit.dx * crossAxisDelta, + originOffset.dy + + mainAxisUnit.dy * mainAxisDelta + + crossAxisUnit.dy * crossAxisDelta, + ); + if (addExtent) { + childOffset += mainAxisUnit * paintExtentOf(child); + } + context.paintChild(child, childOffset); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsNode.message(childCount > 0 + ? 'currently live children: ${indices.join(',')}' + : 'no children current live')); + } + + @override + List debugDescribeChildren() { + final List childList = []; + if (childCount > 0) { + for (final child in children) { + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + childList.add(child.toDiagnosticsNode( + name: 'child with index ${childParentData.index}')); + } + } + if (_keepAliveBucket.isNotEmpty) { + final List indices = _keepAliveBucket.keys.toList()..sort(); + for (final index in indices) { + childList.add(_keepAliveBucket[index]!.toDiagnosticsNode( + name: 'child with index $index (kept alive offstage)', + style: DiagnosticsTreeStyle.offstage, + )); + } + } + return childList; + } +} + +/// Generic mixin for render objects with a list of children. +/// +/// Provides a child model for a render object subclass that stores children +/// in a HashMap. +mixin TileContainerRenderObjectMixin on RenderObject { + final SplayTreeMap _childRenderObjects = + SplayTreeMap(); + + /// The number of children. + int get childCount => _childRenderObjects.length; + + Iterable get children => _childRenderObjects.values; + + Iterable get indices => _childRenderObjects.keys; + + /// Checks whether the given render object has the correct [runtimeType] to be + /// a child of this render object. + /// + /// Does nothing if assertions are disabled. + /// + /// Always returns true. + bool debugValidateChild(RenderObject child) { + assert(() { + if (child is! ChildType) { + throw FlutterError( + 'A $runtimeType expected a child of type $ChildType but received a ' + 'child of type ${child.runtimeType}.\n' + 'RenderObjects expect specific types of children because they ' + 'coordinate with their children during layout and paint. For ' + 'example, a RenderSliver cannot be the child of a RenderBox because ' + 'a RenderSliver does not understand the RenderBox layout protocol.\n' + '\n' + 'The $runtimeType that expected a $ChildType child was created by:\n' + ' $debugCreator\n' + '\n' + 'The ${child.runtimeType} that did not match the expected child type ' + 'was created by:\n' + ' ${child.debugCreator}\n'); + } + return true; + }()); + return true; + } + + ChildType? operator [](int index) => _childRenderObjects[index]; + + void operator []=(int index, ChildType child) { + if (index < 0) { + throw ArgumentError(index); + } + _removeChild(_childRenderObjects[index]); + adoptChild(child); + _childRenderObjects[index] = child; + } + + void forEachChild(void Function(ChildType child) f) { + _childRenderObjects.values.forEach(f); + } + + /// Remove the child at the specified index from the child list. + void remove(int index) { + final child = _childRenderObjects.remove(index); + _removeChild(child); + } + + void _removeChild(ChildType? child) { + if (child != null) { + // Remove the old child. + dropChild(child); + } + } + + /// Remove all their children from this render object's child list. + /// + /// More efficient than removing them individually. + void removeAll() { + _childRenderObjects.values.forEach(dropChild); + _childRenderObjects.clear(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _childRenderObjects.values.forEach((child) => child.attach(owner)); + } + + @override + void detach() { + super.detach(); + _childRenderObjects.values.forEach((child) => child.detach()); + } + + @override + void redepthChildren() { + _childRenderObjects.values.forEach(redepthChild); + } + + @override + void visitChildren(RenderObjectVisitor visitor) { + _childRenderObjects.values.forEach(visitor); + } + + @override + List debugDescribeChildren() { + final List children = []; + _childRenderObjects.forEach((index, child) => + children.add(child.toDiagnosticsNode(name: 'child $index'))); + return children; + } +} + +/// A base class for sliver that have multiple variable size box children. +/// +/// Helps subclasses build their children lazily using a [SliverVariableSizeChildDelegate]. +abstract class SliverVariableSizeBoxAdaptorWidget + extends SliverWithKeepAliveWidget { + /// Initializes fields for subclasses. + const SliverVariableSizeBoxAdaptorWidget({ + Key? key, + required this.delegate, + this.addAutomaticKeepAlives = true, + }) : super(key: key); + + /// Whether to add keepAlives to children + final bool addAutomaticKeepAlives; + + /// The delegate that provides the children for this widget. + /// + /// The children are constructed lazily using this widget to avoid creating + /// more children than are visible through the [Viewport]. + /// + /// See also: + /// + /// * [SliverChildBuilderDelegate] and [SliverChildListDelegate], which are + /// commonly used subclasses of [SliverChildDelegate] that use a builder + /// callback and an explicit child list, respectively. + final SliverChildDelegate delegate; + + @override + SliverVariableSizeBoxAdaptorElement createElement() => + SliverVariableSizeBoxAdaptorElement( + this, + addAutomaticKeepAlives: addAutomaticKeepAlives, + ); + + @override + RenderSliverVariableSizeBoxAdaptor createRenderObject(BuildContext context); + + /// Returns an estimate of the max scroll extent for all the children. + /// + /// Subclasses should override this function if they have additional + /// information about their max scroll extent. + /// + /// This is used by [SliverMultiBoxAdaptorElement] to implement part of the + /// [RenderSliverBoxChildManager] API. + /// + /// The default implementation defers to [delegate] via its + /// [SliverChildDelegate.estimateMaxScrollOffset] method. + double? estimateMaxScrollOffset( + SliverConstraints constraints, + int firstIndex, + int lastIndex, + double leadingScrollOffset, + double trailingScrollOffset, + ) { + assert(lastIndex >= firstIndex); + return delegate.estimateMaxScrollOffset( + firstIndex, + lastIndex, + leadingScrollOffset, + trailingScrollOffset, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty('delegate', delegate), + ); + } +} + +/// An element that lazily builds children for a [SliverVariableSizeBoxAdaptorWidget]. +/// +/// Implements [RenderSliverVariableSizeBoxChildManager], which lets this element manage +/// the children of subclasses of [RenderSliverVariableSizeBoxAdaptor]. +class SliverVariableSizeBoxAdaptorElement extends RenderObjectElement + implements RenderSliverVariableSizeBoxChildManager { + /// Creates an element that lazily builds children for the given widget. + SliverVariableSizeBoxAdaptorElement(SliverVariableSizeBoxAdaptorWidget widget, + {this.addAutomaticKeepAlives = true}) + : super(widget); + + /// Whether to add keepAlives to children + final bool addAutomaticKeepAlives; + + @override + SliverVariableSizeBoxAdaptorWidget get widget => + super.widget as SliverVariableSizeBoxAdaptorWidget; + + @override + RenderSliverVariableSizeBoxAdaptor get renderObject => + super.renderObject as RenderSliverVariableSizeBoxAdaptor; + + @override + void update(covariant SliverVariableSizeBoxAdaptorWidget newWidget) { + final SliverVariableSizeBoxAdaptorWidget oldWidget = widget; + super.update(newWidget); + final SliverChildDelegate newDelegate = newWidget.delegate; + final SliverChildDelegate oldDelegate = oldWidget.delegate; + if (newDelegate != oldDelegate && + (newDelegate.runtimeType != oldDelegate.runtimeType || + newDelegate.shouldRebuild(oldDelegate))) { + performRebuild(); + } + } + + // We inflate widgets at two different times: + // 1. When we ourselves are told to rebuild (see performRebuild). + // 2. When our render object needs a child (see createChild). + // In both cases, we cache the results of calling into our delegate to get the widget, + // so that if we do case 2 later, we don't call the builder again. + // Any time we do case 1, though, we reset the cache. + + final Map _childWidgets = HashMap(); + final SplayTreeMap _childElements = + SplayTreeMap(); + + @override + void performRebuild() { + _childWidgets.clear(); // Reset the cache, as described above. + super.performRebuild(); + assert(_currentlyUpdatingChildIndex == null); + try { + late final int firstIndex; + late final int lastIndex; + if (_childElements.isEmpty) { + firstIndex = 0; + lastIndex = 0; + } else if (_didUnderflow) { + firstIndex = _childElements.firstKey()!; + lastIndex = _childElements.lastKey()! + 1; + } else { + firstIndex = _childElements.firstKey()!; + lastIndex = _childElements.lastKey()!; + } + + for (int index = firstIndex; index <= lastIndex; ++index) { + _currentlyUpdatingChildIndex = index; + final Element? newChild = + updateChild(_childElements[index], _build(index), index); + if (newChild != null) { + _childElements[index] = newChild; + } else { + _childElements.remove(index); + } + } + } finally { + _currentlyUpdatingChildIndex = null; + } + } + + Widget? _build(int index) { + return _childWidgets.putIfAbsent( + index, () => widget.delegate.build(this, index)); + } + + @override + void createChild(int index) { + assert(_currentlyUpdatingChildIndex == null); + owner!.buildScope(this, () { + Element? newChild; + try { + _currentlyUpdatingChildIndex = index; + newChild = updateChild(_childElements[index], _build(index), index); + } finally { + _currentlyUpdatingChildIndex = null; + } + if (newChild != null) { + _childElements[index] = newChild; + } else { + _childElements.remove(index); + } + }); + } + + @override + Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) { + final oldParentData = child?.renderObject?.parentData + as SliverVariableSizeBoxAdaptorParentData?; + final Element? newChild = super.updateChild(child, newWidget, newSlot); + final newParentData = newChild?.renderObject?.parentData + as SliverVariableSizeBoxAdaptorParentData?; + + // set keepAlive to true in order to populate the cache + if (addAutomaticKeepAlives && newParentData != null) { + newParentData.keepAlive = true; + } + + // Preserve the old layoutOffset if the renderObject was swapped out. + if (oldParentData != newParentData && + oldParentData != null && + newParentData != null) { + newParentData.layoutOffset = oldParentData.layoutOffset; + } + + return newChild; + } + + @override + void forgetChild(Element child) { + assert(child.slot != null); + assert(_childElements.containsKey(child.slot)); + _childElements.remove(child.slot); + super.forgetChild(child); + } + + @override + void removeChild(RenderBox child) { + final int index = renderObject.indexOf(child); + assert(_currentlyUpdatingChildIndex == null); + assert(index >= 0); + owner!.buildScope(this, () { + assert(_childElements.containsKey(index)); + try { + _currentlyUpdatingChildIndex = index; + final Element? result = updateChild(_childElements[index], null, index); + assert(result == null); + } finally { + _currentlyUpdatingChildIndex = null; + } + _childElements.remove(index); + assert(!_childElements.containsKey(index)); + }); + } + + double? _extrapolateMaxScrollOffset( + int? firstIndex, + int? lastIndex, + double? leadingScrollOffset, + double? trailingScrollOffset, + ) { + final int? childCount = widget.delegate.estimatedChildCount; + if (childCount == null) { + return double.infinity; + } + if (lastIndex == childCount - 1) { + return trailingScrollOffset; + } + final int reifiedCount = lastIndex! - firstIndex! + 1; + final double averageExtent = + (trailingScrollOffset! - leadingScrollOffset!) / reifiedCount; + final int remainingCount = childCount - lastIndex - 1; + return trailingScrollOffset + averageExtent * remainingCount; + } + + @override + double estimateMaxScrollOffset( + SliverConstraints constraints, { + int? firstIndex, + int? lastIndex, + double? leadingScrollOffset, + double? trailingScrollOffset, + }) { + return widget.estimateMaxScrollOffset( + constraints, + firstIndex!, + lastIndex!, + leadingScrollOffset!, + trailingScrollOffset!, + ) ?? + _extrapolateMaxScrollOffset( + firstIndex, + lastIndex, + leadingScrollOffset, + trailingScrollOffset, + )!; + } + + @override + int get childCount => widget.delegate.estimatedChildCount ?? 0; + + @override + void didStartLayout() { + assert(debugAssertChildListLocked()); + } + + @override + void didFinishLayout() { + assert(debugAssertChildListLocked()); + final int firstIndex = _childElements.firstKey() ?? 0; + final int lastIndex = _childElements.lastKey() ?? 0; + widget.delegate.didFinishLayout(firstIndex, lastIndex); + } + + int? _currentlyUpdatingChildIndex; + + @override + bool debugAssertChildListLocked() { + assert(_currentlyUpdatingChildIndex == null); + return true; + } + + @override + void didAdoptChild(RenderBox child) { + assert(_currentlyUpdatingChildIndex != null); + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + childParentData.index = _currentlyUpdatingChildIndex; + } + + bool _didUnderflow = false; + + @override + void setDidUnderflow(bool value) { + _didUnderflow = value; + } + + @override + void insertRenderObjectChild(covariant RenderBox child, int slot) { + assert(_currentlyUpdatingChildIndex == slot); + assert(renderObject.debugValidateChild(child)); + renderObject[_currentlyUpdatingChildIndex!] = child; + assert(() { + final childParentData = + child.parentData! as SliverVariableSizeBoxAdaptorParentData; + assert(slot == childParentData.index); + return true; + }()); + } + + @override + void moveRenderObjectChild( + covariant RenderObject child, + covariant Object? oldSlot, + covariant Object? newSlot, + ) { + assert(false); + } + + @override + void removeRenderObjectChild( + covariant RenderObject child, + covariant Object? slot, + ) { + assert(_currentlyUpdatingChildIndex != null); + renderObject.remove(_currentlyUpdatingChildIndex!); + } + + @override + void visitChildren(ElementVisitor visitor) { + // The toList() is to make a copy so that the underlying list can be modified by + // the visitor: + _childElements.values.toList().forEach(visitor); + } + + @override + void debugVisitOnstageChildren(ElementVisitor visitor) { + _childElements.values.where((Element child) { + final parentData = + child.renderObject!.parentData as SliverMultiBoxAdaptorParentData?; + late double itemExtent; + switch (renderObject.constraints.axis) { + case Axis.horizontal: + itemExtent = child.renderObject!.paintBounds.width; + break; + case Axis.vertical: + itemExtent = child.renderObject!.paintBounds.height; + break; + } + + return parentData!.layoutOffset! < + renderObject.constraints.scrollOffset + + renderObject.constraints.remainingPaintExtent && + parentData.layoutOffset! + itemExtent > + renderObject.constraints.scrollOffset; + }).forEach(visitor); + } +} + +/// A sliver that places multiple box children in a two dimensional arrangement. +/// +/// [SliverStaggeredGrid] places its children in arbitrary positions determined by +/// [gridDelegate]. Each child is forced to have the size specified by the +/// [gridDelegate]. +/// +/// The main axis direction of a grid is the direction in which it scrolls; the +/// cross axis direction is the orthogonal direction. +/// +/// ## Sample code +/// +/// This example, which would be inserted into a [CustomScrollView.slivers] +/// list, shows 8 boxes: +/// +/// ```dart +///SliverStaggeredGrid.count( +/// crossAxisCount: 4, +/// mainAxisSpacing: 4.0, +/// crossAxisSpacing: 4.0, +/// children: const [ +/// const Text('1'), +/// const Text('2'), +/// const Text('3'), +/// const Text('4'), +/// const Text('5'), +/// const Text('6'), +/// const Text('7'), +/// const Text('8'), +/// ], +/// staggeredTiles: const [ +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// const StaggeredTile.count(2, 2), +/// const StaggeredTile.count(2, 1), +/// ], +///) +/// ``` +/// +/// See also: +/// +/// * [SliverList], which places its children in a linear array. +/// * [SliverFixedExtentList], which places its children in a linear +/// array with a fixed extent in the main axis. +/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList] +/// except that it uses a prototype list item instead of a pixel value to define +/// the main axis extent of each item. +class SliverStaggeredGrid extends SliverVariableSizeBoxAdaptorWidget { + /// Creates a sliver that places multiple box children in a two dimensional + /// arrangement. + const SliverStaggeredGrid({ + Key? key, + required SliverChildDelegate delegate, + required this.gridDelegate, + bool addAutomaticKeepAlives = true, + }) : super( + key: key, + delegate: delegate, + addAutomaticKeepAlives: addAutomaticKeepAlives, + ); + + /// Creates a sliver that places multiple box children in a two dimensional + /// arrangement with a fixed number of tiles in the cross axis. + /// + /// Uses a [SliverStaggeredGridDelegateWithFixedCrossAxisCount] as the [gridDelegate], + /// and a [SliverVariableSizeChildListDelegate] as the [delegate]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + // [SliverVariableSizeChildListDelegate.addAutomaticKeepAlives] property. The + /// + /// See also: + /// + /// * [StaggeredGridView.count], the equivalent constructor for [StaggeredGridView] widgets. + SliverStaggeredGrid.count({ + Key? key, + required int crossAxisCount, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + List children = const [], + List staggeredTiles = const [], + bool addAutomaticKeepAlives = true, + }) : gridDelegate = SliverStaggeredGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: (i) => staggeredTiles[i], + staggeredTileCount: staggeredTiles.length, + ), + super( + key: key, + delegate: SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + ), + ); + + /// Creates a sliver that builds multiple box children in a two dimensional + /// arrangement with a fixed number of tiles in the cross axis. + /// + /// This constructor is appropriate for grid views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Uses a [SliverStaggeredGridDelegateWithFixedCrossAxisCount] as the + /// [gridDelegate], and a [SliverVariableSizeChildBuilderDelegate] as the [delegate]. + /// + /// See also: + /// + /// * [StaggeredGridView.countBuilder], the equivalent constructor for + /// [StaggeredGridView] widgets. + SliverStaggeredGrid.countBuilder({ + Key? key, + required int crossAxisCount, + required IndexedStaggeredTileBuilder staggeredTileBuilder, + required IndexedWidgetBuilder itemBuilder, + required int itemCount, + double mainAxisSpacing = 0, + double crossAxisSpacing = 0, + bool addAutomaticKeepAlives = true, + }) : gridDelegate = SliverStaggeredGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: staggeredTileBuilder, + staggeredTileCount: itemCount, + ), + super( + key: key, + delegate: SliverChildBuilderDelegate( + itemBuilder, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + ), + ); + + /// Creates a sliver that places multiple box children in a two dimensional + /// arrangement with tiles that each have a maximum cross-axis extent. + /// + /// Uses a [SliverStaggeredGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate], + /// and a [SliverVariableSizeChildListDelegate] as the [delegate]. + /// + /// See also: + /// + /// * [StaggeredGridView.extent], the equivalent constructor for [StaggeredGridView] widgets. + SliverStaggeredGrid.extent({ + Key? key, + required double maxCrossAxisExtent, + double mainAxisSpacing = 0, + double crossAxisSpacing = 0, + List children = const [], + List staggeredTiles = const [], + bool addAutomaticKeepAlives = true, + }) : gridDelegate = SliverStaggeredGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: (i) => staggeredTiles[i], + staggeredTileCount: staggeredTiles.length, + ), + super( + key: key, + delegate: SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + ), + ); + + /// Creates a sliver that builds multiple box children in a two dimensional + /// arrangement with tiles that each have a maximum cross-axis extent. + /// + /// This constructor is appropriate for grid views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Uses a [SliverStaggeredGridDelegateWithMaxCrossAxisExtent] as the + /// [gridDelegate], and a [SliverVariableSizeChildBuilderDelegate] as the [delegate]. + /// + /// See also: + /// + /// * [StaggeredGridView.extentBuilder], the equivalent constructor for + /// [StaggeredGridView] widgets. + SliverStaggeredGrid.extentBuilder({ + Key? key, + required double maxCrossAxisExtent, + required IndexedStaggeredTileBuilder staggeredTileBuilder, + required IndexedWidgetBuilder itemBuilder, + required int itemCount, + double mainAxisSpacing = 0, + double crossAxisSpacing = 0, + bool addAutomaticKeepAlives = true, + }) : gridDelegate = SliverStaggeredGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + staggeredTileBuilder: staggeredTileBuilder, + staggeredTileCount: itemCount, + ), + super( + key: key, + delegate: SliverChildBuilderDelegate( + itemBuilder, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + ), + ); + + /// The delegate that controls the size and position of the children. + final SliverStaggeredGridDelegate gridDelegate; + + @override + RenderSliverStaggeredGrid createRenderObject(BuildContext context) { + final element = context as SliverVariableSizeBoxAdaptorElement; + return RenderSliverStaggeredGrid( + childManager: element, gridDelegate: gridDelegate); + } + + @override + void updateRenderObject( + BuildContext context, RenderSliverStaggeredGrid renderObject) { + renderObject.gridDelegate = gridDelegate; + } +} diff --git a/chameleonultragui/lib/l10n/README.md b/chameleonultragui/lib/l10n/README.md new file mode 100644 index 00000000..344eb829 --- /dev/null +++ b/chameleonultragui/lib/l10n/README.md @@ -0,0 +1,3 @@ +# Translations + +If you want to collaborate by adding your language to the application, you can do it through [our Crowdin project](https://crowdin.com/project/chameleon-ultra-gui). Do not contribute files into `chameleonultragui/lib/l100n/app_*.arb`. All translations should be added only to Crowdin. If your language is missing, you can create issue and ask to enable it. "Chameleon Ultra GUI", "Chameleon" and other trademarks should not be translated. diff --git a/chameleonultragui/lib/l10n/app_de.arb b/chameleonultragui/lib/l10n/app_de.arb index 5627a3af..f210c0e6 100644 --- a/chameleonultragui/lib/l10n/app_de.arb +++ b/chameleonultragui/lib/l10n/app_de.arb @@ -1,180 +1,180 @@ { - "@@locale": "de", - "ok": "OK", - "cancel": "Abbrechen", - "close": "Schließen", - "save": "Speichern", - "no": "Nein", - "yes": "Ja", - "enabled": "Aktiviert", - "disabled": "Deaktiviert", - "available": "Verfügbar", - "unavialable": "Nicht verfügbar", - "connect": "Verbinden", - "home": "Startseite", - "card": "Karte", - "cards": "Karten", - "dictionary": "Wörterbuch", - "dictionaries": "Wörterbücher", - "slot": "Slot", - "slots": "Slots", - "slot_manager": "Slot Manager", - "saved_cards": "Gespeicherte Karten", - "read_card": "Karte lesen", - "write_card": "Karte schreiben", - "settings": "Einstellungen", - "theme": "Erscheinungsbild", - "system": "System", - "light": "Hell", - "dark": "Dunkel", - "color_scheme": "Farbschema", - "def": "Standard", - "purple": "Lila", - "blue": "Blau", - "green": "Grün", - "indigo": "Indigo", - "lime": "Limette", - "red": "Rot", - "yellow": "Gelb", - "about": "Über", - "activate": "Aktivieren", - "deactivate": "Deaktivieren", - "debug_mode": "Debug-Modus", - "debug_mode_confirmation": "Sind sie sicher, dass die den Debugmodus {mode} möchten? Er ist für Entwickler um spezifische Funktionen auf nicht unterstützten Plattformen zu testen.", - "debug": "Debug", - "debug_page_warning": "Wenn Sie dieses Menü benutzen, können Sie ihr Chameleon PERMANENT zerstören.", - "warned": "Sie wurden gewarnt.", - "platform": "Plattform", - "android": "Android", - "serial_protocol": "Serielles Protokoll", - "chameleon_connected": "Chameleon verbunden", - "chameleon_device_type": "Chameleon Gerätetyp", - "nested_attack": "Nested Angriff auf Karte ausführen", - "darkside_attack": "Darkside Angriff auf Karte ausführen", - "copy_uid": "KartenUID in Emulator kopieren", - "test_naming": "Test Namensgebung", - "test_nested_lib": "Nested Bibliothek testen", - "test_darkside_lib": "Darkside Bibliothek testen", - "dfu_flash_ultra": "DFU Flash Ultra FW", - "dfu_flash_lite": "DFU Flash Lite FW", - "safe_option": "Sichere Option", - "restart_chameleon": "Chameleon neu starten", - "error": "Fehler", - "chameleon_is_dfu": "Chameleon ist im DFU-Modus.", - "firmware_is_corrupted": "Dies bedeutet wahrscheinlich, dass Ihre Firmware beschädigt ist. Möchten Sie die neueste FW flashen?", - "flash": "Flash", - "dfu": " (DFU)", - "keys": "Schlüssel", - "found_keys": "Gefundene Schlüssel", - "please_wait": "Bitte warten", - "used_slots": "Benutzte Slots", - "firmware_version": "Firmware Version", - "update_error": "Aktualisierung fehlerhaft", - "up_to_date": "Ihre Chameleon-{model} Firmware ist aktuell", - "downloading_fw": "Herunterladen und vorbereiten neuer Chameleon-{model} Firmware...", - "check_updates": "Nach Updates suchen", - "emulator_mode": "Gehe zum Emulator-Modus", - "reader_mode": "Gehe zum Lesermodus", - "recover_keys_via": "Schlüssel wiederherstellen über {mode}", - "recover_keys": "Schlüssel wiederherstellen", - "recover_keys_nonce": "Schlüssel von {number} Nonce(s) wiederherstellen", - "restart_required": "Neustart erforderlich", - "take_effects": "Änderungen werden nach dem Neustart übernommen", - "language": "Sprache", - "sidebar_expansion": "Seitenleiste Erweiterung", - "expand": "Erweitern", - "retract": "Zurückziehen", - "auto": "Auto", - "restart_now": "Jetzt neu starten", - "about_text": "Ein Tool zum grafischen Verwalten und Konfigurieren Ihres Chameleon Ultras, geschrieben in Flutter und läuft auf Desktop so wie Mobile.", - "version": "Version", - "developed_by": "Entwickelt von", - "license": "Lizenz", - "thanks_for_support": "Vielen Dank an alle, die uns auf Open Collective unterstützen!", - "code_contributors": "Mitwirkende Programmierer", - "not_implemented": "Nicht Implementiert", - "edit_data": "Daten bearbeiten", - "enter_data": "Daten eingeben", - "sector": "Sektor", - "edit_card": "Karte bearbeiten", - "please_enter_name": "Bitte gebe einen Namen an", - "name": "Namen", - "enter_name": "Geben Sie den Namen der Karte ein", - "pick_color": "Wählen Sie eine Farbe", - "reset_default": "Auf Standard zurücksetzen", - "please_enter_something": "Bitte {name} eingeben", - "uid": "UID", - "sak": "SAK", - "atqa": "ATQA", - "enter_something": "{name} eingeben", - "must_or": "{name} muss {a} oder {b} Bytes lang sein.", - "must_be": "{name} muss {a} Bytes lang sein.", - "device_settings": "Geräteeinstellungen", - "firmware_management": "Firmware Management", - "enter_dfu": "In den DFU-Modus wechseln", - "flash_via_dfu": "Aktuelle FW via DFU flashen", - "flash_zip_dfu": ".zip FW via DFU flashen", - "animations": "Animationen", - "button_config": "Tastenkonfiguration", - "button_x": "{x} Taste", - "long_press": "Langes drücken", - "disable": "Deaktivieren", - "forward": "Vorwärts", - "backward": "Rückwärts", - "clone_uid": "UID klonen", - "other": "Sonstiges", - "reset_settings": "Einstellungen zurücksetzen", - "factory_reset": "Werkseinstellungen", - "factory_sure": "Sind Sie sicher, dass Sie Ihren Chameleon auf Werkseinstellungen zurücksetzen möchten?", - "full": "Volle", - "mini": "Minimiert", - "none": "Keine", - "edit_dictionary": "Wörterbuch bearbeiten", - "enter_dict_name": "Name des Wörterbuchs eingeben", - "enter_dict_keys": "Schlüssel für das Wörterbuch eingeben", - "empty": "Leer", - "slot_settings": "Slot-Einstellungen", - "slot_status": "Slot-Status", - "hf": "HF", - "lf": "LF", - "mifare_clasic_e_s": "Mifare Classic Emulator Einstellungen", - "mode_gen1a": "Gen1A Magic Modus", - "mode_gen2": "Gen2 Magic Modus", - "use_from_block": "Benutze UID/SAK/ATQA aus 0 Block", - "collect_nonces": "Sammle Nonces ({type})", - "present_cham_reader_keys": "Chameleon Lesegerät vorlegen, um Schlüssel wiederherzustellen", - "ena_coll_recover_keys": "Sammlung zum Wiederherstellen der Schlüssel aktivieren", - "write_mode": "Schreib-Modus", - "normal": "Normal", - "decline": "Ablehnen", - "deceive": "Täuschen", - "shadow": "Schatten", - "outdated_fw": "Veraltete FW", - "unknown": "Unbekannt", - "recovery_error_no_supported": "Schlüsselwiederherstellung von dieser Karte wird noch nicht unterstützt", - "recovery_error_no_keys_darkside": "Keine Schlüssel und nicht anfällig für Darkside Angriff", - "recovery_error_dict": "Bei der Wörterbuchprüfung ist etwas schief gelaufen", - "recovery_error_dump_data": "Beim Kopieren von Daten ist etwas schiefgelaufen", - "output_file": "Bitte wählen Sie eine Ausgabe-Datei aus", - "hf_tag_info": "HF Tag Info", - "lf_tag_info": "LF Tag Info", - "no_card_found": "Keine Karte gefunden. Versuchen Sie, Chameleon auf eine Karte zu verschieben", - "no_supported": "Nicht unterstützte Aktion", - "lite_no_read": "Chameleon Lite unterstützt keine Lesen von Karten", - "read": "Lesen", - "write": "Schreiben", - "save_only_uid": "Nur UID speichern", - "letter_space": "{letter} ", - "dump_partial_data": "Teildaten kopieren", - "additional_key_dict": "Zusätzliches Schlüsselwörterbuch", - "check_keys_dict": "Schlüssel aus dem Wörterbuch überprüfen", - "dump_card": "Karte kopieren", - "save_as": "Als {name} speichern", - "correct_tag_deta": "Tag-Details korrigieren", - "uid_len": "UID {len} Byte Länge", - "tag_type": "Tag-Typ", - "select_save_format": "Speicherformat auswählen", - "key_count": "Schlüssel Anzahl", - "all": "Alle", - "no_name": "Kein Name" -} + "@@locale": "de", + "ok": "OK", + "cancel": "Abbrechen", + "close": "Schließen", + "save": "Speichern", + "no": "Nein", + "yes": "Ja", + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "available": "Verfügbar", + "unavailable": "Nicht verfügbar", + "connect": "Verbinden", + "home": "Startseite", + "card": "Karte", + "cards": "Karten", + "dictionary": "Wörterbuch", + "dictionaries": "Wörterbücher", + "slot": "Slot", + "slots": "Slots", + "slot_manager": "Slot Manager", + "saved_cards": "Gespeicherte Karten", + "read_card": "Karte lesen", + "write_card": "Karte schreiben", + "settings": "Einstellungen", + "theme": "Erscheinungsbild", + "system": "System", + "light": "Hell", + "dark": "Dunkel", + "color_scheme": "Farbschema", + "def": "Standard", + "purple": "Lila", + "blue": "Blau", + "green": "Grün", + "indigo": "Indigo", + "lime": "Limette", + "red": "Rot", + "yellow": "Gelb", + "about": "Über", + "activate": "Aktivieren", + "deactivate": "Deaktivieren", + "debug_mode": "Debug-Modus", + "debug_mode_confirmation": "Sind sie sicher, dass die den Debugmodus {mode} möchten? Er ist für Entwickler um spezifische Funktionen auf nicht unterstützten Plattformen zu testen.", + "debug": "Debug", + "debug_page_warning": "Wenn Sie dieses Menü benutzen, können Sie ihr Chameleon PERMANENT zerstören.", + "warned": "Sie wurden gewarnt.", + "platform": "Plattform", + "android": "Android", + "serial_protocol": "Serielles Protokoll", + "chameleon_connected": "Chameleon verbunden", + "chameleon_device_type": "Chameleon Gerätetyp", + "nested_attack": "Nested Angriff auf Karte ausführen", + "darkside_attack": "Darkside Angriff auf Karte ausführen", + "copy_uid": "KartenUID in Emulator kopieren", + "test_naming": "Test Namensgebung", + "test_nested_lib": "Nested Bibliothek testen", + "test_darkside_lib": "Darkside Bibliothek testen", + "dfu_flash_ultra": "DFU Flash Ultra FW", + "dfu_flash_lite": "DFU Flash Lite FW", + "safe_option": "Sichere Option", + "restart_chameleon": "Chameleon neu starten", + "error": "Fehler", + "chameleon_is_dfu": "Chameleon ist im DFU-Modus.", + "firmware_is_corrupted": "Dies bedeutet wahrscheinlich, dass Ihre Firmware beschädigt ist. Möchten Sie die neueste FW flashen?", + "flash": "Flash", + "dfu": " (DFU)", + "keys": "Schlüssel", + "found_keys": "Gefundene Schlüssel", + "please_wait": "Bitte warten", + "used_slots": "Benutzte Slots", + "firmware_version": "Firmware Version", + "update_error": "Aktualisierung fehlerhaft", + "up_to_date": "Ihre Chameleon-{model} Firmware ist aktuell", + "downloading_fw": "Herunterladen und vorbereiten neuer Chameleon-{model} Firmware...", + "check_updates": "Nach Updates suchen", + "emulator_mode": "Gehe zum Emulator-Modus", + "reader_mode": "Gehe zum Lesermodus", + "recover_keys_via": "Schlüssel wiederherstellen über {mode}", + "recover_keys": "Schlüssel wiederherstellen", + "recover_keys_nonce": "Schlüssel von {number} Nonce(s) wiederherstellen", + "restart_required": "Neustart erforderlich", + "take_effects": "Änderungen werden nach dem Neustart übernommen", + "language": "Sprache", + "sidebar_expansion": "Seitenleiste Erweiterung", + "expand": "Erweitern", + "retract": "Zurückziehen", + "auto": "Auto", + "restart_now": "Jetzt neu starten", + "about_text": "Ein Tool zum grafischen Verwalten und Konfigurieren Ihres Chameleon Ultras, geschrieben in Flutter und läuft auf Desktop so wie Mobile.", + "version": "Version", + "developed_by": "Entwickelt von", + "license": "Lizenz", + "thanks_for_support": "Vielen Dank an alle, die uns auf Open Collective unterstützen!", + "code_contributors": "Mitwirkende Programmierer", + "not_implemented": "Nicht Implementiert", + "edit_data": "Daten bearbeiten", + "enter_data": "Daten eingeben", + "sector": "Sektor", + "edit_card": "Karte bearbeiten", + "please_enter_name": "Bitte gebe einen Namen an", + "name": "Namen", + "enter_name": "Geben Sie den Namen der Karte ein", + "pick_color": "Wählen Sie eine Farbe", + "reset_default": "Auf Standard zurücksetzen", + "please_enter_something": "Bitte {name} eingeben", + "uid": "UID", + "sak": "SAK", + "atqa": "ATQA", + "enter_something": "{name} eingeben", + "must_or": "{name} muss {a} oder {b} Bytes lang sein.", + "must_be": "{name} muss {a} Bytes lang sein.", + "device_settings": "Geräteeinstellungen", + "firmware_management": "Firmware Management", + "enter_dfu": "In den DFU-Modus wechseln", + "flash_via_dfu": "Aktuelle FW via DFU flashen", + "flash_zip_dfu": ".zip FW via DFU flashen", + "animations": "Animationen", + "button_config": "Tastenkonfiguration", + "button_x": "{x} Taste", + "long_press": "Langes drücken", + "disable": "Deaktivieren", + "forward": "Vorwärts", + "backward": "Rückwärts", + "clone_uid": "UID klonen", + "other": "Sonstiges", + "reset_settings": "Einstellungen zurücksetzen", + "factory_reset": "Werkseinstellungen", + "factory_sure": "Sind Sie sicher, dass Sie Ihren Chameleon auf Werkseinstellungen zurücksetzen möchten?", + "full": "Volle", + "mini": "Minimiert", + "none": "Keine", + "edit_dictionary": "Wörterbuch bearbeiten", + "enter_dict_name": "Name des Wörterbuchs eingeben", + "enter_dict_keys": "Schlüssel für das Wörterbuch eingeben", + "empty": "Leer", + "slot_settings": "Slot-Einstellungen", + "slot_status": "Slot-Status", + "hf": "HF", + "lf": "LF", + "mifare_clasic_e_s": "Mifare Classic Emulator Einstellungen", + "mode_gen1a": "Gen1A Magic Modus", + "mode_gen2": "Gen2 Magic Modus", + "use_from_block": "Benutze UID/SAK/ATQA aus 0 Block", + "collect_nonces": "Sammle Nonces ({type})", + "present_cham_reader_keys": "Chameleon Lesegerät vorlegen, um Schlüssel wiederherzustellen", + "ena_coll_recover_keys": "Sammlung zum Wiederherstellen der Schlüssel aktivieren", + "write_mode": "Schreib-Modus", + "normal": "Normal", + "decline": "Ablehnen", + "deceive": "Täuschen", + "shadow": "Schatten", + "outdated_fw": "Veraltete FW", + "unknown": "Unbekannt", + "recovery_error_no_supported": "Schlüsselwiederherstellung von dieser Karte wird noch nicht unterstützt", + "recovery_error_no_keys_darkside": "Keine Schlüssel und nicht anfällig für Darkside Angriff", + "recovery_error_dict": "Bei der Wörterbuchprüfung ist etwas schief gelaufen", + "recovery_error_dump_data": "Beim Kopieren von Daten ist etwas schiefgelaufen", + "output_file": "Bitte wählen Sie eine Ausgabe-Datei aus", + "hf_tag_info": "HF Tag Info", + "lf_tag_info": "LF Tag Info", + "no_card_found": "Keine Karte gefunden. Versuchen Sie, Chameleon auf eine Karte zu verschieben", + "no_supported": "Nicht unterstützte Aktion", + "lite_no_read": "Chameleon Lite unterstützt keine Lesen von Karten", + "read": "Lesen", + "write": "Schreiben", + "save_only_uid": "Nur UID speichern", + "letter_space": "{letter} ", + "dump_partial_data": "Teildaten kopieren", + "additional_key_dict": "Zusätzliches Schlüsselwörterbuch", + "check_keys_dict": "Schlüssel aus dem Wörterbuch überprüfen", + "dump_card": "Karte kopieren", + "save_as": "Als {name} speichern", + "correct_tag_data": "Tag-Details korrigieren", + "uid_len": "UID {len} Byte Länge", + "tag_type": "Tag-Typ", + "select_save_format": "Speicherformat auswählen", + "key_count": "Schlüssel Anzahl", + "all": "Alle", + "no_name": "Kein Name" +} \ No newline at end of file diff --git a/chameleonultragui/lib/l10n/app_de_AT.arb b/chameleonultragui/lib/l10n/app_de_AT.arb index 48befdcc..cc5f78d7 100644 --- a/chameleonultragui/lib/l10n/app_de_AT.arb +++ b/chameleonultragui/lib/l10n/app_de_AT.arb @@ -1,180 +1,180 @@ { - "@@locale": "de_AT", - "ok": "Passt", - "cancel": "Abbruch", - "close": "Zu mochn", - "save": "Speichan", - "no": "Nah", - "yes": "Joh", - "enabled": "Aktviert", - "disabled": "Deaktviert", - "available": "Ferfiagbar", - "unavialable": "Ned Ferfiagbar", - "connect": "Verbindn", - "home": "Zhaus", - "card": "Koardn", - "cards": "Koardn", - "dictionary": "Wärdabuach", - "dictionaries": "Wärdabiacher", - "slot": "Slot", - "slots": "Slots", - "slot_manager": "Slot verwoiter", - "saved_cards": "Gepeicherte Koarten", - "read_card": "Koartn lesn", - "write_card": "Koartn schreibn", - "settings": "Einstellung", - "theme": "Erschoanungsbüd", - "system": "Süstam", - "light": "Hö", - "dark": "Dunkl", - "color_scheme": "Foab thema", - "def": "Stondoard", - "purple": "Lila", - "blue": "Blau", - "green": "Grean", - "indigo": "Indigo", - "lime": "Limettn", - "red": "Rot", - "yellow": "Göb", - "about": "Über", - "activate": "Actviern", - "deactivate": "Deactviern", - "debug_mode": "käfa Modus", - "debug_mode_confirmation": "Siacha, dassd indn Debugmodus {mode} wüsst? Der is fiad Entwickla um spezi Funktiona auf ned unterstützte Plattforma zu testn.", - "debug": "Entkäfa", - "debug_page_warning": "Des Menü zu verwendn kinad dei Chameleon permanent runiern.", - "warned": "Du wurdst gwarnt.", - "platform": "Platform", - "android": "Android", - "serial_protocol": "Seriels Protokoll", - "chameleon_connected": "Chameleon verbundn", - "chameleon_device_type": "Chameleon gerät typ", - "nested_attack": "Nested Angriff auf Koartn aussfiarn", - "darkside_attack": "Darkside Angriff auf Koartn ausfiarn", - "copy_uid": "KoartnUID in Emulator kopiern", - "test_naming": "Namengebung testn", - "test_nested_lib": "Nested Biblothek testn", - "test_darkside_lib": "Darkside Bibliothek testn", - "dfu_flash_ultra": "DFU Flash Ultra FW", - "dfu_flash_lite": "DFU Flash Lite FW", - "safe_option": "Sichere option", - "restart_chameleon": "Chameleon neich startn", - "error": "Fäla", - "chameleon_is_dfu": "Chameleon is im DFU modus.", - "firmware_is_corrupted": "Des hast, dass dei Firmware wahrscheinlich beschädigt is, wüst die neuste FW flashen?", - "flash": "Flash", - "dfu": " (DFU)", - "keys": "Schliasl", - "found_keys": "Gfundene Schliasl", - "please_wait": "Bitte woardn", - "used_slots": "Verwendte Slots", - "firmware_version": "Firmware version", - "update_error": "Update fäla", - "up_to_date": "Dei Chameleon {model} firmware is akutell", - "downloading_fw": "Runterladn und vorbreiten neuer Chameleon {model} firmware...", - "check_updates": "Auf updates priafn", - "emulator_mode": "Zum emulator mode wächsln", - "reader_mode": "Zum lesa mode wächsln", - "recover_keys_via": "Schliasl via {mode} wiaderherstön", - "recover_keys": "Schliasl wiaderherstön", - "recover_keys_nonce": "Schliasl von {number} Nonce(s) wiaderherstön", - "restart_required": "Neichstart erfordalich", - "take_effects": "Ändrung nachm Neichstoart sichtboar", - "language": "Sproch", - "sidebar_expansion": "Seitnleistn erweitrung", - "expand": "Erweitrt", - "retract": "Zam zong", - "auto": "Auto", - "restart_now": "Jetzt neich stoartn", - "about_text": "A werkzeich zum graphischn verwalten von deim Chameleon Ultra, geschriebn in Flutter und rennt am Pc und am Taschntelefon.", - "version": "Version", - "developed_by": "Entwückid von", - "license": "Lizenz", - "thanks_for_support": "Donke on jeden, der uns auf Open Collective unterstiazt!", - "code_contributors": "Mitwiarkende Programmiera", - "not_implemented": "Ned Implementiert", - "edit_data": "Doatn beorbaten", - "enter_data": "Doatn eingebn", - "sector": "Sektor", - "edit_card": "Koartn beorbatn", - "please_enter_name": "Bitte giab an nom ein", - "name": "Nom", - "enter_name": "Giab an nom von der Koartn ei", - "pick_color": "Wöh a foab", - "reset_default": "Auf standoard zuricksetzn", - "please_enter_something": "Bitte giab {name} ei", - "uid": "UID", - "sak": "SAK", - "atqa": "ATQA", - "enter_something": "Giab {name} ei", - "must_or": "{name} mias {a} oder {b} Bytes long sei.", - "must_be": "{name} mias {a} Bytes long sei.", - "device_settings": "Geräteistellung", - "firmware_management": "Firmware verwoitung", - "enter_dfu": "Indn DFU-Modus wöchsi", - "flash_via_dfu": "Aktuelle FW via DFU flashen", - "flash_zip_dfu": ".zip FW via DFU flashen", - "animations": "Animationen", - "button_config": "Knopf config", - "button_x": "{x} Knopf", - "long_press": "Long drickn", - "disable": "Deaktviern", - "forward": "Forwärts", - "backward": "Riackwärts", - "clone_uid": "UID Kopiern", - "other": "Sonstiegs", - "reset_settings": "Einstellung zuriagsetzn", - "factory_reset": "Werkseinstellungen", - "factory_sure": "Bist da sicher, dassd dei Chameleon auf Werkseinstellungen zurücksetzn wüst?", - "full": "Voi", - "mini": "Weng", - "none": "Nix", - "edit_dictionary": "Wörtabiach beorbeitn", - "enter_dict_name": "Name from wörtabiachl eingebn", - "enter_dict_keys": "Schliasl vom wörterbiachl eingebn", - "empty": "La", - "slot_settings": "Slot-Einstellungen", - "slot_status": "Slot-Status", - "hf": "HF", - "lf": "LF", - "mifare_clasic_e_s": "Mifare Classic Emulator Einstellungn", - "mode_gen1a": "Gen1A Magic Modus", - "mode_gen2": "Gen2 Magic Modus", - "use_from_block": "Nimm UID/SAK/ATQA aus Block Nui", - "collect_nonces": "Somml Nonces ({type})", - "present_cham_reader_keys": "Chameleon Lesegerät vorlegn, um Schliasl wiederherzustön", - "ena_coll_recover_keys": "Sommln zum Weiderherstön der Schliassl aktivieren", - "write_mode": "Schreib-Modus", - "normal": "Normal", - "decline": "Ablehnen", - "deceive": "Täuschen", - "shadow": "Schotten", - "outdated_fw": "Veroutete FW", - "unknown": "Unbekannt", - "recovery_error_no_supported": "Schliasslwiederherstöllung von dieser Koartn wird noch nicht unterstützt", - "recovery_error_no_keys_darkside": "Keine Schliassl und ned anfällig für Darkside Angriff", - "recovery_error_dict": "Bei der Wöarterbuachpriafung ist wos schief glaufn", - "recovery_error_dump_data": "Beim Kopieren von Dotn ist wos schiefgelaufn", - "output_file": "Bitte wählen a Ausgabe-Datei aus", - "hf_tag_info": "HF Tag Info", - "lf_tag_info": "LF Tag Info", - "no_card_found": "Ka Koartn gfundn. Probier du des Chameleon auf der Koartn zu verschieben", - "no_supported": "Ned unterstitzte Aktion", - "lite_no_read": "Chameleon Lite unterstitzt ka Lesen vo Koartn", - "read": "Lesen", - "write": "Schreibn", - "save_only_uid": "Nur UID speichorn", - "letter_space": "{letter} ", - "dump_partial_data": "Teildatn kopiern", - "additional_key_dict": "Zusätzliches Schliasslwörterbuach", - "check_keys_dict": "Schliassl ausm Wörterbiach überprüfen", - "dump_card": "Kortn kopieren", - "save_as": "Als {name} speichan", - "correct_tag_deta": "Tag-Details korrigieren", - "uid_len": "UID {len} Byte Läng", - "tag_type": "Tag-Typ", - "select_save_format": "Speicherformat wön", - "key_count": "Schliassl Anzahl", - "all": "Alle", - "no_name": "Kei Nam" -} + "@@locale": "de_AT", + "ok": "Passt", + "cancel": "Abbruch", + "close": "Zu mochn", + "save": "Speichan", + "no": "Nah", + "yes": "Joh", + "enabled": "Aktviert", + "disabled": "Deaktviert", + "available": "Ferfiagbar", + "unavailable": "Ned Ferfiagbar", + "connect": "Verbindn", + "home": "Zhaus", + "card": "Koardn", + "cards": "Koardn", + "dictionary": "Wärdabuach", + "dictionaries": "Wärdabiacher", + "slot": "Slot", + "slots": "Slots", + "slot_manager": "Slot verwoiter", + "saved_cards": "Gepeicherte Koarten", + "read_card": "Koartn lesn", + "write_card": "Koartn schreibn", + "settings": "Einstellung", + "theme": "Erschoanungsbüd", + "system": "Süstam", + "light": "Hö", + "dark": "Dunkl", + "color_scheme": "Foab thema", + "def": "Stondoard", + "purple": "Lila", + "blue": "Blau", + "green": "Grean", + "indigo": "Indigo", + "lime": "Limettn", + "red": "Rot", + "yellow": "Göb", + "about": "Über", + "activate": "Actviern", + "deactivate": "Deactviern", + "debug_mode": "käfa Modus", + "debug_mode_confirmation": "Siacha, dassd indn Debugmodus {mode} wüsst? Der is fiad Entwickla um spezi Funktiona auf ned unterstützte Plattforma zu testn.", + "debug": "Entkäfa", + "debug_page_warning": "Des Menü zu verwendn kinad dei Chameleon permanent runiern.", + "warned": "Du wurdst gwarnt.", + "platform": "Platform", + "android": "Android", + "serial_protocol": "Seriels Protokoll", + "chameleon_connected": "Chameleon verbundn", + "chameleon_device_type": "Chameleon gerät typ", + "nested_attack": "Nested Angriff auf Koartn aussfiarn", + "darkside_attack": "Darkside Angriff auf Koartn ausfiarn", + "copy_uid": "KoartnUID in Emulator kopiern", + "test_naming": "Namengebung testn", + "test_nested_lib": "Nested Biblothek testn", + "test_darkside_lib": "Darkside Bibliothek testn", + "dfu_flash_ultra": "DFU Flash Ultra FW", + "dfu_flash_lite": "DFU Flash Lite FW", + "safe_option": "Sichere option", + "restart_chameleon": "Chameleon neich startn", + "error": "Fäla", + "chameleon_is_dfu": "Chameleon is im DFU modus.", + "firmware_is_corrupted": "Des hast, dass dei Firmware wahrscheinlich beschädigt is, wüst die neuste FW flashen?", + "flash": "Flash", + "dfu": " (DFU)", + "keys": "Schliasl", + "found_keys": "Gfundene Schliasl", + "please_wait": "Bitte woardn", + "used_slots": "Verwendte Slots", + "firmware_version": "Firmware version", + "update_error": "Update fäla", + "up_to_date": "Dei Chameleon {model} firmware is akutell", + "downloading_fw": "Runterladn und vorbreiten neuer Chameleon {model} firmware...", + "check_updates": "Auf updates priafn", + "emulator_mode": "Zum emulator mode wächsln", + "reader_mode": "Zum lesa mode wächsln", + "recover_keys_via": "Schliasl via {mode} wiaderherstön", + "recover_keys": "Schliasl wiaderherstön", + "recover_keys_nonce": "Schliasl von {number} Nonce(s) wiaderherstön", + "restart_required": "Neichstart erfordalich", + "take_effects": "Ändrung nachm Neichstoart sichtboar", + "language": "Sproch", + "sidebar_expansion": "Seitnleistn erweitrung", + "expand": "Erweitrt", + "retract": "Zam zong", + "auto": "Auto", + "restart_now": "Jetzt neich stoartn", + "about_text": "A werkzeich zum graphischn verwalten von deim Chameleon Ultra, geschriebn in Flutter und rennt am Pc und am Taschntelefon.", + "version": "Version", + "developed_by": "Entwückid von", + "license": "Lizenz", + "thanks_for_support": "Donke on jeden, der uns auf Open Collective unterstiazt!", + "code_contributors": "Mitwiarkende Programmiera", + "not_implemented": "Ned Implementiert", + "edit_data": "Doatn beorbaten", + "enter_data": "Doatn eingebn", + "sector": "Sektor", + "edit_card": "Koartn beorbatn", + "please_enter_name": "Bitte giab an nom ein", + "name": "Nom", + "enter_name": "Giab an nom von der Koartn ei", + "pick_color": "Wöh a foab", + "reset_default": "Auf standoard zuricksetzn", + "please_enter_something": "Bitte giab {name} ei", + "uid": "UID", + "sak": "SAK", + "atqa": "ATQA", + "enter_something": "Giab {name} ei", + "must_or": "{name} mias {a} oder {b} Bytes long sei.", + "must_be": "{name} mias {a} Bytes long sei.", + "device_settings": "Geräteistellung", + "firmware_management": "Firmware verwoitung", + "enter_dfu": "Indn DFU-Modus wöchsi", + "flash_via_dfu": "Aktuelle FW via DFU flashen", + "flash_zip_dfu": ".zip FW via DFU flashen", + "animations": "Animationen", + "button_config": "Knopf config", + "button_x": "{x} Knopf", + "long_press": "Long drickn", + "disable": "Deaktviern", + "forward": "Forwärts", + "backward": "Riackwärts", + "clone_uid": "UID Kopiern", + "other": "Sonstiegs", + "reset_settings": "Einstellung zuriagsetzn", + "factory_reset": "Werkseinstellungen", + "factory_sure": "Bist da sicher, dassd dei Chameleon auf Werkseinstellungen zurücksetzn wüst?", + "full": "Voi", + "mini": "Weng", + "none": "Nix", + "edit_dictionary": "Wörtabiach beorbeitn", + "enter_dict_name": "Name from wörtabiachl eingebn", + "enter_dict_keys": "Schliasl vom wörterbiachl eingebn", + "empty": "La", + "slot_settings": "Slot-Einstellungen", + "slot_status": "Slot-Status", + "hf": "HF", + "lf": "LF", + "mifare_clasic_e_s": "Mifare Classic Emulator Einstellungn", + "mode_gen1a": "Gen1A Magic Modus", + "mode_gen2": "Gen2 Magic Modus", + "use_from_block": "Nimm UID/SAK/ATQA aus Block Nui", + "collect_nonces": "Somml Nonces ({type})", + "present_cham_reader_keys": "Chameleon Lesegerät vorlegn, um Schliasl wiederherzustön", + "ena_coll_recover_keys": "Sommln zum Weiderherstön der Schliassl aktivieren", + "write_mode": "Schreib-Modus", + "normal": "Normal", + "decline": "Ablehnen", + "deceive": "Täuschen", + "shadow": "Schotten", + "outdated_fw": "Veroutete FW", + "unknown": "Unbekannt", + "recovery_error_no_supported": "Schliasslwiederherstöllung von dieser Koartn wird noch nicht unterstützt", + "recovery_error_no_keys_darkside": "Keine Schliassl und ned anfällig für Darkside Angriff", + "recovery_error_dict": "Bei der Wöarterbuachpriafung ist wos schief glaufn", + "recovery_error_dump_data": "Beim Kopieren von Dotn ist wos schiefgelaufn", + "output_file": "Bitte wählen a Ausgabe-Datei aus", + "hf_tag_info": "HF Tag Info", + "lf_tag_info": "LF Tag Info", + "no_card_found": "Ka Koartn gfundn. Probier du des Chameleon auf der Koartn zu verschieben", + "no_supported": "Ned unterstitzte Aktion", + "lite_no_read": "Chameleon Lite unterstitzt ka Lesen vo Koartn", + "read": "Lesen", + "write": "Schreibn", + "save_only_uid": "Nur UID speichorn", + "letter_space": "{letter} ", + "dump_partial_data": "Teildatn kopiern", + "additional_key_dict": "Zusätzliches Schliasslwörterbuach", + "check_keys_dict": "Schliassl ausm Wörterbiach überprüfen", + "dump_card": "Kortn kopieren", + "save_as": "Als {name} speichan", + "correct_tag_data": "Tag-Details korrigieren", + "uid_len": "UID {len} Byte Läng", + "tag_type": "Tag-Typ", + "select_save_format": "Speicherformat wön", + "key_count": "Schliassl Anzahl", + "all": "Alle", + "no_name": "Kei Nam" +} \ No newline at end of file diff --git a/chameleonultragui/lib/l10n/app_en.arb b/chameleonultragui/lib/l10n/app_en.arb index 42f21653..de18508e 100644 --- a/chameleonultragui/lib/l10n/app_en.arb +++ b/chameleonultragui/lib/l10n/app_en.arb @@ -1,180 +1,183 @@ { - "@@locale": "en", - "ok": "OK", - "cancel": "Cancel", - "close": "Close", - "save": "Save", - "no": "No", - "yes": "Yes", - "enabled": "Enabled", - "disabled": "Disabled", - "available": "Available", - "unavialable": "Unavailable", - "connect": "Connect", - "home": "Home", - "card": "Card", - "cards": "Cards", - "dictionary": "Dictionary", - "dictionaries": "Dictionaries", - "slot": "Slot", - "slots": "Slots", - "slot_manager": "Slot Manager", - "saved_cards": "Saved Cards", - "read_card": "Read Card", - "write_card": "Write Card", - "settings": "Settings", - "theme": "Theme", - "system": "System", - "light": "Light", - "dark": "Dark", - "color_scheme": "Color Scheme", - "def": "Default", - "purple": "Purple", - "blue": "Blue", - "green": "Green", - "indigo": "Indigo", - "lime": "Lime", - "red": "Red", - "yellow": "Yellow", - "about": "About", - "activate": "Activate", - "deactivate": "Deactivate", - "debug_mode": "Debug Mode", - "debug_mode_confirmation": "Are you sure you want to {mode} debug mode? It is created specifically for developers to test specific app functions on UNSUPPORTED platforms.", - "debug": "Debug", - "debug_page_warning": "Using this menu may brick your Chameleon PERMANENTLY.", - "warned": "You have been warned.", - "platform": "Platform", - "android": "Android", - "serial_protocol": "Serial Protocol", - "chameleon_connected": "Chameleon Connected", - "chameleon_device_type": "Chameleon Device Type", - "nested_attack": "Run nested attack on card", - "darkside_attack": "Run Darkside attack on card", - "copy_uid": "Copy card UID to emulator", - "test_naming": "Test Naming", - "test_nested_lib": "Test Nested Library", - "test_darkside_lib": "Test Darkside Library", - "dfu_flash_ultra": "DFU Flash Ultra FW", - "dfu_flash_lite": "DFU Flash Lite FW", - "safe_option": "Safe Option", - "restart_chameleon": "Restart Chameleon", - "error": "Error", - "chameleon_is_dfu": "Chameleon is in DFU mode.", - "firmware_is_corrupted": "This probably means your firmware is corrupted. Do you want to flash latest FW?", - "flash": "Flash", - "dfu": " (DFU)", - "keys": "Keys", - "found_keys": "Found keys", - "please_wait": "Please wait", - "used_slots": "Used Slots", - "firmware_version": "Firmware version", - "update_error": "Update error", - "up_to_date": "Your Chameleon {model} firmware is up to date", - "downloading_fw": "Downloading and preparing new Chameleon {model} firmware...", - "check_updates": "Check for updates", - "emulator_mode": "Go to emulator mode", - "reader_mode": "Go to reader mode", - "recover_keys_via": "Recover keys via {mode}", - "recover_keys": "Recover keys", - "recover_keys_nonce": "Recover keys from {number} nonce(s)", - "restart_required": "Restart required", - "take_effects": "Changes will take effect after a restart", - "language": "Language", - "sidebar_expansion": "Sidebar Expansion", - "expand": "Expand", - "retract": "Retract", - "auto": "Auto", - "restart_now": "Restart now", - "about_text": "A Tool to graphically manage and configure your Chameleon Ultra, written in Flutter and running on Desktop and Mobile.", - "version": "Version", - "developed_by": "Developed by", - "license": "License", - "thanks_for_support": "Thanks to everyone who supports us on Open Collective!", - "code_contributors": "Code contributors", - "not_implemented": "Not Implemented", - "edit_data": "Edit Data", - "enter_data": "Enter Data", - "sector": "Sector", - "edit_card": "Edit Card", - "please_enter_name": "Please enter a name", - "name": "Name", - "enter_name": "Enter name of card", - "pick_color": "Pick a color", - "reset_default": "Reset to default", - "please_enter_something": "Please enter {name}", - "uid": "UID", - "sak": "SAK", - "atqa": "ATQA", - "enter_something": "Enter {name}", - "must_or": "{name} must be {a} or {b} bytes long.", - "must_be": "{name} must be {a} bytes long.", - "device_settings": "Device Settings", - "firmware_management": "Firmware management", - "enter_dfu": "Enter DFU mode", - "flash_via_dfu": "Flash latest FW via DFU", - "flash_zip_dfu": "Flash .zip FW via DFU", - "animations": "Animations", - "button_config": "Button config", - "button_x": "{x} button", - "long_press": "Long press", - "disable": "Disable", - "forward": "Forward", - "backward": "Backward", - "clone_uid": "Clone UID", - "other": "Other", - "reset_settings": "Reset settings", - "factory_reset": "Factory reset", - "factory_sure": "Are you sure you want to factory reset your Chameleon?", - "full": "Full", - "mini": "Mini", - "none": "None", - "edit_dictionary": "Edit Dictionary", - "enter_dict_name": "Enter name of dictionary", - "enter_dict_keys": "Enter keys for dictionary", - "empty": "Empty", - "slot_settings": "Slot Settings", - "slot_status": "Slot Status", - "hf": "HF", - "lf": "LF", - "mifare_clasic_e_s": "Mifare Classic emulator settings", - "mode_gen1a": "Gen1A Magic Mode", - "mode_gen2": "Gen2 Magic Mode", - "use_from_block": "Use UID/SAK/ATQA from 0 block", - "collect_nonces": "Collect nonces ({type})", - "present_cham_reader_keys": "Present Chameleon to reader to recover keys", - "ena_coll_recover_keys": "Enable collection to recover keys", - "write_mode": "Write mode", - "normal": "Normal", - "decline": "Decline", - "deceive": "Deceive", - "shadow": "Shadow", - "outdated_fw": "Outdated FW", - "unknown": "Unknown", - "recovery_error_no_supported": "Key recovery from this card doesn't yet supported", - "recovery_error_no_keys_darkside": "No keys and not vulnerable to Darkside attack", - "recovery_error_dict": "Something went wrong in dictionary check", - "recovery_error_dump_data": "Something went wrong while dumping data", - "output_file": "Please select an output file", - "hf_tag_info": "HF Tag Info", - "lf_tag_info": "LF Tag Info", - "no_card_found": "No card found. Try to move Chameleon on card", - "no_supported": "Unsupported Action", - "lite_no_read": "Chameleon Lite does not support reading cards", - "read": "Read", - "write": "Write", - "save_only_uid": "Save only UID", - "letter_space": "{letter} ", - "dump_partial_data": "Dump partial data", - "additional_key_dict": "Additional key dictionary", - "check_keys_dict": "Check keys from dictionary", - "dump_card": "Dump card", - "save_as": "Save as {name}", - "correct_tag_deta": "Correct tag details", - "uid_len": "UID {len} byte length", - "tag_type": "Tag type", - "select_save_format": "Select save format", - "key_count": "Key count", - "all": "All", - "no_name": "No name" -} + "@@locale": "en", + "ok": "OK", + "cancel": "Cancel", + "close": "Close", + "save": "Save", + "no": "No", + "yes": "Yes", + "enabled": "Enabled", + "disabled": "Disabled", + "available": "Available", + "unavailable": "Unavailable", + "connect": "Connect", + "home": "Home", + "card": "Card", + "cards": "Cards", + "dictionary": "Dictionary", + "dictionaries": "Dictionaries", + "slot": "Slot", + "slots": "Slots", + "slot_manager": "Slot Manager", + "saved_cards": "Saved Cards", + "read_card": "Read Card", + "write_card": "Write Card", + "settings": "Settings", + "theme": "Theme", + "system": "System", + "light": "Light", + "dark": "Dark", + "color_scheme": "Color Scheme", + "def": "Default", + "purple": "Purple", + "blue": "Blue", + "green": "Green", + "indigo": "Indigo", + "lime": "Lime", + "red": "Red", + "yellow": "Yellow", + "about": "About", + "activate": "Activate", + "deactivate": "Deactivate", + "debug_mode": "Debug Mode", + "debug_mode_confirmation": "Are you sure you want to {mode} debug mode? It is created specifically for developers to test specific app functions on UNSUPPORTED platforms.", + "debug": "Debug", + "debug_page_warning": "Using this menu may brick your Chameleon PERMANENTLY.", + "warned": "You have been warned.", + "platform": "Platform", + "android": "Android", + "serial_protocol": "Serial Protocol", + "chameleon_connected": "Chameleon Connected", + "chameleon_device_type": "Chameleon Device Type", + "nested_attack": "Run nested attack on card", + "darkside_attack": "Run Darkside attack on card", + "copy_uid": "Copy card UID to emulator", + "test_naming": "Test Naming", + "test_nested_lib": "Test Nested Library", + "test_darkside_lib": "Test Darkside Library", + "dfu_flash_ultra": "DFU Flash Ultra FW", + "dfu_flash_lite": "DFU Flash Lite FW", + "safe_option": "Safe Option", + "restart_chameleon": "Restart Chameleon", + "error": "Error", + "chameleon_is_dfu": "Chameleon is in DFU mode.", + "firmware_is_corrupted": "This probably means your firmware is corrupted. Do you want to flash latest FW?", + "flash": "Flash", + "dfu": " (DFU)", + "keys": "Keys", + "found_keys": "Found keys", + "please_wait": "Please wait", + "used_slots": "Used Slots", + "firmware_version": "Firmware version", + "update_error": "Update error", + "up_to_date": "Your Chameleon {model} firmware is up-to-date", + "downloading_fw": "Downloading and preparing new Chameleon {model} firmware...", + "check_updates": "Check for updates", + "emulator_mode": "Go to emulator mode", + "reader_mode": "Go to reader mode", + "recover_keys_via": "Recover keys via {mode}", + "recover_keys": "Recover keys", + "recover_keys_nonce": "Recover keys from {number} nonce(s)", + "restart_required": "Restart required", + "take_effects": "Changes will take effect after a restart", + "language": "Language", + "sidebar_expansion": "Sidebar Expansion", + "expand": "Expand", + "retract": "Retract", + "auto": "Auto", + "restart_now": "Restart now", + "about_text": "A Tool to graphically manage and configure your Chameleon Ultra, written in Flutter and running on Desktop and Mobile.", + "version": "Version", + "developed_by": "Developed by", + "license": "License", + "thanks_for_support": "Thanks to everyone who supports us on Open Collective!", + "code_contributors": "Code contributors", + "not_implemented": "Not Implemented", + "edit_data": "Edit Data", + "enter_data": "Enter Data", + "sector": "Sector", + "edit_card": "Edit Card", + "please_enter_name": "Please enter a name", + "name": "Name", + "enter_name": "Enter name of card", + "pick_color": "Pick a color", + "reset_default": "Reset to default", + "please_enter_something": "Please enter {name}", + "uid": "UID", + "sak": "SAK", + "atqa": "ATQA", + "enter_something": "Enter {name}", + "must_or": "{name} must be {a} or {b} bytes long.", + "must_be": "{name} must be {a} bytes long.", + "device_settings": "Device Settings", + "firmware_management": "Firmware management", + "enter_dfu": "Enter DFU mode", + "flash_via_dfu": "Flash latest FW via DFU", + "flash_zip_dfu": "Flash .zip FW via DFU", + "animations": "Animations", + "button_config": "Button config", + "button_x": "{x} button", + "long_press": "Long press", + "disable": "Disable", + "forward": "Forward", + "backward": "Backward", + "clone_uid": "Clone UID", + "other": "Other", + "reset_settings": "Reset settings", + "factory_reset": "Factory reset", + "factory_sure": "Are you sure you want to factory reset your Chameleon?", + "full": "Full", + "mini": "Mini", + "none": "None", + "edit_dictionary": "Edit Dictionary", + "enter_dict_name": "Enter name of dictionary", + "enter_dict_keys": "Enter keys for dictionary", + "empty": "Empty", + "slot_settings": "Slot Settings", + "slot_status": "Slot Status", + "hf": "HF", + "lf": "LF", + "mifare_clasic_e_s": "Mifare Classic emulator settings", + "mode_gen1a": "Gen1A Magic Mode", + "mode_gen2": "Gen2 Magic Mode", + "use_from_block": "Use UID/SAK/ATQA from 0 block", + "collect_nonces": "Collect nonces ({type})", + "present_cham_reader_keys": "Present Chameleon to reader to recover keys", + "ena_coll_recover_keys": "Enable collection to recover keys", + "write_mode": "Write mode", + "normal": "Normal", + "decline": "Decline", + "deceive": "Deceive", + "shadow": "Shadow", + "outdated_fw": "Outdated FW", + "unknown": "Unknown", + "recovery_error_no_supported": "Key recovery from this card doesn't yet support", + "recovery_error_no_keys_darkside": "No keys and not vulnerable to Darkside attack", + "recovery_error_dict": "Something went wrong in dictionary check", + "recovery_error_dump_data": "Something went wrong while dumping data", + "output_file": "Please select an output file", + "hf_tag_info": "HF Tag Info", + "lf_tag_info": "LF Tag Info", + "no_card_found": "No card found. Try to move Chameleon on card", + "no_supported": "Unsupported Action", + "lite_no_read": "Chameleon Lite does not support reading cards", + "read": "Read", + "write": "Write", + "save_only_uid": "Save only UID", + "letter_space": "{letter} ", + "dump_partial_data": "Dump partial data", + "additional_key_dict": "Additional key dictionary", + "check_keys_dict": "Check keys from dictionary", + "dump_card": "Dump card", + "save_as": "Save as {name}", + "correct_tag_data": "Correct tag details", + "uid_len": "UID {len} byte length", + "tag_type": "Tag type", + "select_save_format": "Select save format", + "key_count": "Key count", + "all": "All", + "no_name": "No name", + "connecting_to_ble": "Connecting to BLE device...", + "default_ble_password": "Default BLE connection password is 123456", + "connection_might_take_some_time": "First connection might take some time" +} \ No newline at end of file diff --git a/chameleonultragui/lib/l10n/app_es.arb b/chameleonultragui/lib/l10n/app_es.arb index 08da4baa..c3261c46 100644 --- a/chameleonultragui/lib/l10n/app_es.arb +++ b/chameleonultragui/lib/l10n/app_es.arb @@ -1,180 +1,180 @@ { - "@@locale": "es", - "ok": "OK", - "cancel": "Cancelar", - "close": "Cerrar", - "save": "Guardar", - "no": "No", - "yes": "Sí", - "enabled": "Habilitado", - "disabled": "Deshabilitado", - "available": "Disponible", - "unavialable": "No disponible", - "connect": "Conectar", - "home": "Inicio", - "card": "Tarjeta", - "cards": "Tarjetas", - "dictionary": "Diccionario", - "dictionaries": "Diccionarios", - "slot": "Ranura", - "slots": "Ranuras", - "slot_manager": "Gestor de ranuras", - "saved_cards": "Tarjetas guardadas", - "read_card": "Leer tarjeta", - "write_card": "Escribir tarjeta", - "settings": "Configuración", - "theme": "Tema", - "system": "Sistema", - "light": "Claro", - "dark": "Oscuro", - "color_scheme": "Esquema de colores", - "def": "Por defecto", - "purple": "Púrpura", - "blue": "Azul", - "green": "Verde", - "indigo": "Añil", - "lime": "Lima", - "red": "Rojo", - "yellow": "Amarillo", - "about": "Acerca de", - "activate": "Activar", - "deactivate": "Desactivar", - "debug_mode": "Modo de depuración", - "debug_mode_confirmation": "¿Está seguro que desea {mode} el modo de depuración? Se ha creado específicamente para que los desarrolladores prueben funciones específicas de la aplicación en plataformas NO SOPORTADAS.", - "debug": "Depurar", - "debug_page_warning": "Usar este menú puede bloquear su Chameleon PERMANENTEMENTE.", - "warned": "Has sido advertido.", - "platform": "Plataforma", - "android": "Android", - "serial_protocol": "Protocolo Serial", - "chameleon_connected": "Chameleon conectado", - "chameleon_device_type": "Tipo de dispositivo de Chameleon", - "nested_attack": "Ejecutar ataque Nested a tarjeta", - "darkside_attack": "Ejecutar ataque Darkside a tarjeta", - "copy_uid": "Copiar tarjeta UID al emulador", - "test_naming": "Test Naming", - "test_nested_lib": "Test librerías Nested", - "test_darkside_lib": "Test librerías Darkside", - "dfu_flash_ultra": "Flash DFU Ultra FW", - "dfu_flash_lite": "Flash DFU Lite FW", - "safe_option": "Opción segura", - "restart_chameleon": "Reiniciar Chameleon", - "error": "Error", - "chameleon_is_dfu": "Chameleon está en modo DFU.", - "firmware_is_corrupted": "Esto probablemente significa que su firmware está dañado. ¿Quieres flashear el último FW?", - "flash": "Flash", - "dfu": " (DFU)", - "keys": "Claves", - "found_keys": "Claves encontradas", - "please_wait": "Por favor, espere", - "used_slots": "Ranuras usadas", - "firmware_version": "Versión de firmware", - "update_error": "Error de actualización", - "up_to_date": "El firmware de tu Chameleon {model} está actualizado.", - "downloading_fw": "Descargando y preparando el nuevo firmware para tu Chameleon {model}...", - "check_updates": "Comprobar actualizaciones", - "emulator_mode": "Ir a modo emulador", - "reader_mode": "Ir a modo lector", - "recover_keys_via": "Recuperar claves via {mode}", - "recover_keys": "Recuperar claves", - "recover_keys_nonce": "Recuperar claves a partir de {number} nonce(s)", - "restart_required": "Reinicio requerido", - "take_effects": "Los cambios surtirán efecto tras el reinicio", - "language": "Idioma", - "sidebar_expansion": "Barra lateral", - "expand": "Expandida", - "retract": "Oculta", - "auto": "Auto", - "restart_now": "Reiniciar ahora", - "about_text": "Una herramienta para gestionar y configurar gráficamente su Chameleon Ultra, escrita en Flutter, funcional en escritorio y móvil.", - "version": "Versión", - "developed_by": "Desarrollador por", - "license": "Licencia", - "thanks_for_support": "¡Gracias a todos los que nos apoyan en Open Collective!", - "code_contributors": "Colaboradores de código", - "not_implemented": "No implementado", - "edit_data": "Editar datos", - "enter_data": "Introducir datos", - "sector": "Sector", - "edit_card": "Editar tarjeta", - "please_enter_name": "Por favor, introduzca un nombre", - "name": "Nombre", - "enter_name": "Introduzca el nombre de la tarjeta", - "pick_color": "Elija un color", - "reset_default": "Restablecer a valores predeterminados", - "please_enter_something": "Por favor, introduzca un {name}", - "uid": "UID", - "sak": "SAK", - "atqa": "ATQA", - "enter_something": "Introduzca el {name}", - "must_or": "El {name} debe tener {a} o {b} bytes de longitud.", - "must_be": "El {name} debe ser {a} bytes de longitud.", - "device_settings": "Configuración del dispositivo", - "firmware_management": "Gestión del firmware", - "enter_dfu": "Entrar en modo DFU", - "flash_via_dfu": "Flashear el firmware más reciente vía DFU", - "flash_zip_dfu": "Flashear un archivo .zip vía DFU", - "animations": "Animaciones", - "button_config": "Configuración de botones", - "button_x": "Botón {x}", - "long_press": "Pulsación larga", - "disable": "Desactivar", - "forward": "Adelante", - "backward": "Atrás", - "clone_uid": "Clonar UID", - "other": "Otro", - "reset_settings": "Restablecer configuración", - "factory_reset": "Restablecer a valores de fábrica", - "factory_sure": "¿Está seguro de que desea restablecer la configuración a los valores de fábrica?", - "full": "Completo", - "mini": "Mínimo", - "none": "Ninguno", - "edit_dictionary": "Editar diccionario", - "enter_dict_name": "Introduzca el nombre del diccionario", - "enter_dict_keys": "Introduzca las claves para el diccionario", - "empty": "Vacío", - "slot_settings": "Configuración de ranuras", - "slot_status": "Estado de la ranura", - "hf": "HF", - "lf": "LF", - "mifare_clasic_e_s": "Ajustes de emulacion Mifare Classic", - "mode_gen1a": "Modo Gen1A Magic", - "mode_gen2": "Modo Gen2 Magic", - "use_from_block": "Usar UID/SAK/ATQA desde el bloque 0", - "collect_nonces": "Recoger nonces ({type})", - "present_cham_reader_keys": "Presente tu Chameleon al lector para recuperar las claves", - "ena_coll_recover_keys": "Habilitar la recuperación de claves", - "write_mode": "Modo de escritura", - "normal": "Normal", - "decline": "Rechazar", - "deceive": "Deceive", - "shadow": "Shadow", - "outdated_fw": "Firmware desactualizado", - "unknown": "Desconocido", - "recovery_error_no_supported": "La recuperación de claves de esta tarjeta aún no está soportada", - "recovery_error_no_keys_darkside": "No hay claves y no es vulverable a Darkside", - "recovery_error_dict": "Algo ha fallado en la comprobación del diccionario", - "recovery_error_dump_data": "Algo ha ido mal al volcar los datos", - "output_file": "Seleccione un archivo de salida", - "hf_tag_info": "HF Tag Info", - "lf_tag_info": "LF Tag Info", - "no_card_found": "No se ha encontrado ninguna tarjeta. Prueba a moverla sobre el Chameleon", - "no_supported": "Accion no soportada", - "lite_no_read": "Chameleon Lite no admite la lectura de tarjetas", - "read": "Leer", - "write": "Escribir", - "save_only_uid": "Guardar solo UID", - "letter_space": "{letter} ", - "dump_partial_data": "Volcar datos parciales", - "additional_key_dict": "Diccionario de claves adicional", - "check_keys_dict": "Comprobar claves del diccionario", - "dump_card": "Volcar tarjeta", - "save_as": "Guardar como {name}", - "correct_tag_deta": "Datos correctos de la etiqueta", - "uid_len": "UID longitud {len} bytes", - "tag_type": "Tipo de etiqueta", - "select_save_format": "Seleccione el formato de guardado", - "key_count": "Contador de claves", - "all": "Todo", - "no_name": "Sin nombre" + "@@locale": "es", + "ok": "OK", + "cancel": "Cancelar", + "close": "Cerrar", + "save": "Guardar", + "no": "No", + "yes": "Sí", + "enabled": "Habilitado", + "disabled": "Deshabilitado", + "available": "Disponible", + "unavailable": "No disponible", + "connect": "Conectar", + "home": "Inicio", + "card": "Tarjeta", + "cards": "Tarjetas", + "dictionary": "Diccionario", + "dictionaries": "Diccionarios", + "slot": "Ranura", + "slots": "Ranuras", + "slot_manager": "Gestor de ranuras", + "saved_cards": "Tarjetas guardadas", + "read_card": "Leer tarjeta", + "write_card": "Escribir tarjeta", + "settings": "Configuración", + "theme": "Tema", + "system": "Sistema", + "light": "Claro", + "dark": "Oscuro", + "color_scheme": "Esquema de colores", + "def": "Por defecto", + "purple": "Púrpura", + "blue": "Azul", + "green": "Verde", + "indigo": "Añil", + "lime": "Lima", + "red": "Rojo", + "yellow": "Amarillo", + "about": "Acerca de", + "activate": "Activar", + "deactivate": "Desactivar", + "debug_mode": "Modo de depuración", + "debug_mode_confirmation": "¿Está seguro que desea {mode} el modo de depuración? Se ha creado específicamente para que los desarrolladores prueben funciones específicas de la aplicación en plataformas NO SOPORTADAS.", + "debug": "Depurar", + "debug_page_warning": "Usar este menú puede bloquear su Chameleon PERMANENTEMENTE.", + "warned": "Has sido advertido.", + "platform": "Plataforma", + "android": "Android", + "serial_protocol": "Protocolo Serial", + "chameleon_connected": "Chameleon conectado", + "chameleon_device_type": "Tipo de dispositivo de Chameleon", + "nested_attack": "Ejecutar ataque Nested a tarjeta", + "darkside_attack": "Ejecutar ataque Darkside a tarjeta", + "copy_uid": "Copiar tarjeta UID al emulador", + "test_naming": "Test Naming", + "test_nested_lib": "Test librerías Nested", + "test_darkside_lib": "Test librerías Darkside", + "dfu_flash_ultra": "Flash DFU Ultra FW", + "dfu_flash_lite": "Flash DFU Lite FW", + "safe_option": "Opción segura", + "restart_chameleon": "Reiniciar Chameleon", + "error": "Error", + "chameleon_is_dfu": "Chameleon está en modo DFU.", + "firmware_is_corrupted": "Esto probablemente significa que su firmware está dañado. ¿Quieres flashear el último FW?", + "flash": "Flash", + "dfu": " (DFU)", + "keys": "Claves", + "found_keys": "Claves encontradas", + "please_wait": "Por favor, espere", + "used_slots" : "Ranuras usadas", + "firmware_version": "Versión de firmware", + "update_error": "Error de actualización", + "up_to_date": "El firmware de tu Chameleon {model} está actualizado.", + "downloading_fw": "Descargando y preparando el nuevo firmware para tu Chameleon {model}...", + "check_updates": "Comprobar actualizaciones", + "emulator_mode": "Ir a modo emulador", + "reader_mode": "Ir a modo lector", + "recover_keys_via": "Recuperar claves via {mode}", + "recover_keys": "Recuperar claves", + "recover_keys_nonce": "Recuperar claves a partir de {number} nonce(s)", + "restart_required": "Reinicio requerido", + "take_effects": "Los cambios surtirán efecto tras el reinicio", + "language": "Idioma", + "sidebar_expansion": "Barra lateral", + "expand": "Expandida", + "retract": "Oculta", + "auto": "Auto", + "restart_now": "Reiniciar ahora", + "about_text": "Una herramienta para gestionar y configurar gráficamente su Chameleon Ultra, escrita en Flutter, funcional en escritorio y móvil.", + "version": "Versión", + "developed_by": "Desarrollador por", + "license": "Licencia", + "thanks_for_support": "¡Gracias a todos los que nos apoyan en Open Collective!", + "code_contributors": "Colaboradores de código", + "not_implemented": "No implementado", + "edit_data": "Editar datos", + "enter_data": "Introducir datos", + "sector": "Sector", + "edit_card": "Editar tarjeta", + "please_enter_name": "Por favor, introduzca un nombre", + "name": "Nombre", + "enter_name": "Introduzca el nombre de la tarjeta", + "pick_color": "Elija un color", + "reset_default": "Restablecer a valores predeterminados", + "please_enter_something": "Por favor, introduzca un {name}", + "uid": "UID", + "sak": "SAK", + "atqa": "ATQA", + "enter_something": "Introduzca el {name}", + "must_or": "El {name} debe tener {a} o {b} bytes de longitud.", + "must_be": "El {name} debe ser {a} bytes de longitud.", + "device_settings": "Configuración del dispositivo", + "firmware_management": "Gestión del firmware", + "enter_dfu": "Entrar en modo DFU", + "flash_via_dfu": "Flashear el firmware más reciente vía DFU", + "flash_zip_dfu": "Flashear un archivo .zip vía DFU", + "animations": "Animaciones", + "button_config": "Configuración de botones", + "button_x": "Botón {x}", + "long_press": "Pulsación larga", + "disable": "Desactivar", + "forward": "Adelante", + "backward": "Atrás", + "clone_uid": "Clonar UID", + "other": "Otro", + "reset_settings": "Restablecer configuración", + "factory_reset": "Restablecer a valores de fábrica", + "factory_sure": "¿Está seguro de que desea restablecer la configuración a los valores de fábrica?", + "full": "Completo", + "mini": "Mínimo", + "none": "Ninguno", + "edit_dictionary": "Editar diccionario", + "enter_dict_name": "Introduzca el nombre del diccionario", + "enter_dict_keys": "Introduzca las claves para el diccionario", + "empty": "Vacío", + "slot_settings": "Configuración de ranuras", + "slot_status": "Estado de la ranura", + "hf": "HF", + "lf": "LF", + "mifare_clasic_e_s": "Ajustes de emulacion Mifare Classic", + "mode_gen1a": "Modo Gen1A Magic", + "mode_gen2": "Modo Gen2 Magic", + "use_from_block": "Usar UID/SAK/ATQA desde el bloque 0", + "collect_nonces": "Recoger nonces ({type})", + "present_cham_reader_keys": "Presente tu Chameleon al lector para recuperar las claves", + "ena_coll_recover_keys": "Habilitar la recuperación de claves", + "write_mode": "Modo de escritura", + "normal": "Normal", + "decline": "Rechazar", + "deceive": "Deceive", + "shadow": "Shadow", + "outdated_fw": "Firmware desactualizado", + "unknown": "Desconocido", + "recovery_error_no_supported": "La recuperación de claves de esta tarjeta aún no está soportada", + "recovery_error_no_keys_darkside": "No hay claves y no es vulverable a Darkside", + "recovery_error_dict": "Algo ha fallado en la comprobación del diccionario", + "recovery_error_dump_data": "Algo ha ido mal al volcar los datos", + "output_file": "Seleccione un archivo de salida", + "hf_tag_info": "HF Tag Info", + "lf_tag_info": "LF Tag Info", + "no_card_found": "No se ha encontrado ninguna tarjeta. Prueba a moverla sobre el Chamaleon", + "no_supported": "Accion no soportada", + "lite_no_read": "Chameleon Lite no admite la lectura de tarjetas", + "read": "Leer", + "write": "Escribir", + "save_only_uid": "Guardar solo UID", + "letter_space": "{letter} ", + "dump_partial_data": "Volcar datos parciales", + "additional_key_dict": "Diccionario de claves adicional", + "check_keys_dict": "Comprobar claves del diccionario", + "dump_card": "Volcar tarjeta", + "save_as": "Guardar como {name}", + "correct_tag_data": "Datos correctos de la etiqueta", + "uid_len": "UID longitud {len} bytes", + "tag_type": "Tipo de etiqueta", + "select_save_format": "Seleccione el formato de guardado", + "key_count": "Contador de claves", + "all": "Todo", + "no_name": "Sin nombre" } diff --git a/chameleonultragui/lib/l10n/app_ko.arb b/chameleonultragui/lib/l10n/app_ko.arb new file mode 100644 index 00000000..6e4f4072 --- /dev/null +++ b/chameleonultragui/lib/l10n/app_ko.arb @@ -0,0 +1,183 @@ +{ + "@@locale": "ko", + "ok": "확인", + "cancel": "취소", + "close": "닫기", + "save": "저장", + "no": "아니오", + "yes": "예", + "enabled": "활성화됨", + "disabled": "비활성화됨", + "available": "사용 가능", + "unavailable": "Unavailable", + "connect": "연결", + "home": "홈", + "card": "카드", + "cards": "카드들", + "dictionary": "사전", + "dictionaries": "사전", + "slot": "슬롯", + "slots": "슬롯들", + "slot_manager": "슬롯 관리자", + "saved_cards": "저장된 카드", + "read_card": "카드 읽기", + "write_card": "카드 쓰기", + "settings": "설정", + "theme": "테마", + "system": "시스템", + "light": "라이트 모드", + "dark": "다크 모드", + "color_scheme": "색 구성표", + "def": "기본값", + "purple": "보라색", + "blue": "파란색", + "green": "녹색", + "indigo": "남색", + "lime": "연두색", + "red": "빨간색", + "yellow": "노란색", + "about": "앱 정보", + "activate": "활성화", + "deactivate": "비활성화", + "debug_mode": "디버그 모드", + "debug_mode_confirmation": "정말로 {mode} 디버그 모드를 사용하시겠습니까? 개발자가 지원되지 않는 플랫폼에서 특정 앱 기능을 테스트할 수 있도록 특별히 제작되었습니다.", + "debug": "디버그", + "debug_page_warning": "이 메뉴를 사용하면 카멜레온이 영구적으로 벽돌이 될 수 있습니다.", + "warned": "주의하세요.", + "platform": "플랫폼", + "android": "안드로이드", + "serial_protocol": "시리얼 프로토콜", + "chameleon_connected": "Chameleon 연결됨", + "chameleon_device_type": "Chameleon 장치 유형", + "nested_attack": "카드에 중첩 공격 실행", + "darkside_attack": "카드에 Darkside 공격 실행", + "copy_uid": "카드 UID를 에뮬레이터에 복사", + "test_naming": "테스트 이름 지정", + "test_nested_lib": "중첩 라이브러리 테스트", + "test_darkside_lib": "Darkside 라이브러리 테스트", + "dfu_flash_ultra": "DFU를 통해 Ultra 펌웨어 설치", + "dfu_flash_lite": "DFU를 통해 Lite 펌웨어 설치", + "safe_option": "안전 옵션", + "restart_chameleon": "Chameleon 재시작", + "error": "오류", + "chameleon_is_dfu": "Chameleon은 DFU 모드입니다.", + "firmware_is_corrupted": "아마도 펌웨어가 손상된것같습니다. 최신 펌웨어를 플래시하시겠습니까?", + "flash": "플래시", + "dfu": " (DFU)", + "keys": "키", + "found_keys": "찾은 키", + "please_wait": "잠시만 기다려 주세요.", + "used_slots": "사용된 슬롯", + "firmware_version": "펌웨어 버전", + "update_error": "업데이트 오류", + "up_to_date": "Chameleon {model} 펌웨어가 최신 버전입니다.", + "downloading_fw": "새로운 Chameleon {model} 펌웨어 다운로드 및 준비 중...", + "check_updates": "업데이트 확인", + "emulator_mode": "에뮬레이터 모드 변경", + "reader_mode": "리더 모드 변경", + "recover_keys_via": "{mode} 를 통해 키 복구", + "recover_keys": "키 복구", + "recover_keys_nonce": "{number} nonce에서 키 복구", + "restart_required": "재시작 필요", + "take_effects": "재시작하면 변경사항이 적용됩니다.", + "language": "언어", + "sidebar_expansion": "메뉴 확장", + "expand": "펼치기", + "retract": "접기", + "auto": "자동", + "restart_now": "지금 재시작", + "about_text": "플러터로 작성되고 데스크톱 및 모바일에서 실행되는 Chameleon Ultra를 그래픽으로 관리하고 구성하는 도구입니다.", + "version": "버전", + "developed_by": "개발자:", + "license": "라이센스", + "thanks_for_support": "Open Collective를 지지해 주시는 모든 분들께 감사드립니다!", + "code_contributors": "코드 기여자", + "not_implemented": "구현되지 않음", + "edit_data": "데이터 편집", + "enter_data": "데이터 입력", + "sector": "섹터", + "edit_card": "카드 편집", + "please_enter_name": "이름을 입력해 주세요.", + "name": "이름", + "enter_name": "카드 이름을 입력하세요.", + "pick_color": "색상 선택", + "reset_default": "기본값으로 재설정", + "please_enter_something": "{name} 을(를) 입력하세요", + "uid": "UID", + "sak": "SAK", + "atqa": "ATQA", + "enter_something": "{name} 입력", + "must_or": "{name} 길이는 {a} 또는 {b} 바이트여야 합니다.", + "must_be": "{name} 은 길이가 {a} 바이트여야 합니다.", + "device_settings": "장치 설정", + "firmware_management": "펌웨어 관리", + "enter_dfu": "DFU 모드로 진입", + "flash_via_dfu": "DFU를 통해 최신 펌웨어 설치", + "flash_zip_dfu": "DFU를 통해 .zip 펌웨어 설치", + "animations": "애니메이션", + "button_config": "버튼 설정", + "button_x": "{x} 버튼", + "long_press": "길게 누르기", + "disable": "비활성화", + "forward": "앞으로", + "backward": "뒤로", + "clone_uid": "UID 복제", + "other": "기타", + "reset_settings": "설정 초기화", + "factory_reset": "공장 초기화", + "factory_sure": "Chameleon을 초기화하시겠습니까?", + "full": "전체", + "mini": "최소", + "none": "없음", + "edit_dictionary": "사전 편집", + "enter_dict_name": "사전 이름을 입력하세요.", + "enter_dict_keys": "사전에 대한 키를 입력하세요.", + "empty": "비어 있음", + "slot_settings": "슬롯 설정", + "slot_status": "슬롯 상태", + "hf": "HF", + "lf": "LF", + "mifare_clasic_e_s": "Mifare Classic 에뮬레이터 설정", + "mode_gen1a": "Gen1A 매직 모드", + "mode_gen2": "Gen2 매직 모드", + "use_from_block": "0 블록의 UID/SAK/ATQA 사용", + "collect_nonces": "Nonce 수집({type})", + "present_cham_reader_keys": "키를 복구하려면 리더에게 Chameleon을 보여주세요.", + "ena_coll_recover_keys": "키를 복구하려면 수집을 활성화하세요.", + "write_mode": "쓰기 모드", + "normal": "일반", + "decline": "거부", + "deceive": "속이기", + "shadow": "그림자", + "outdated_fw": "오래된 펌웨어", + "unknown": "알 수 없음", + "recovery_error_no_supported": "이 카드의 키 복구는 아직 지원되지 않습니다.", + "recovery_error_no_keys_darkside": "키가 없으며 Darkside 공격에 취약하지 않습니다.", + "recovery_error_dict": "사전 확인에 문제가 발생했습니다.", + "recovery_error_dump_data": "데이터를 덤프하는 중에 문제가 발생했습니다.", + "output_file": "출력 파일을 선택하세요.", + "hf_tag_info": "HF 태그 정보", + "lf_tag_info": "LF 태그 정보", + "no_card_found": "카드를 찾을 수 없습니다. 카드에서 Chameleon을 움직여 보세요", + "no_supported": "지원되지 않는 작업", + "lite_no_read": "Chameleon Lite는 카드 읽기를 지원하지 않습니다.", + "read": "읽기", + "write": "쓰기", + "save_only_uid": "UID만 저장", + "letter_space": "{letter} ", + "dump_partial_data": "일부 데이터 덤프", + "additional_key_dict": "키 사전 추가", + "check_keys_dict": "사전에서 키 확인", + "dump_card": "카드 덤프", + "save_as": "{name} 으로 저장", + "correct_tag_data": "Correct tag details", + "uid_len": "UID {len} 바이트 길이", + "tag_type": "태그 유형", + "select_save_format": "저장 형식 선택", + "key_count": "키 개수", + "all": "전체", + "no_name": "이름 없음", + "connecting_to_ble": "Connecting to BLE device...", + "default_ble_password": "Default BLE connection password is 123456", + "connection_might_take_some_time": "First connection might take some time" +} \ No newline at end of file diff --git a/chameleonultragui/lib/l10n/app_ru.arb b/chameleonultragui/lib/l10n/app_ru.arb index 0af5c26a..bd7b0c14 100644 --- a/chameleonultragui/lib/l10n/app_ru.arb +++ b/chameleonultragui/lib/l10n/app_ru.arb @@ -1,180 +1,183 @@ { - "@@locale": "ru", - "ok": "ОК", - "cancel": "Отмена", - "close": "Закрыть", - "save": "Сохранить", - "no": "Нет", - "yes": "Да", - "enabled": "Включено", - "disabled": "Отключено", - "available": "Доступно", - "unavialable": "Недоступно", - "connect": "Подключить", - "home": "Главная", - "card": "Карта", - "cards": "Карты", - "dictionary": "Словарь", - "dictionaries": "Словари", - "slot": "Слот", - "slots": "Слоты", - "slot_manager": "Менеджер слотов", - "saved_cards": "Сохраненные карты", - "read_card": "Чтение карт", - "write_card": "Запись карт", - "settings": "Настройки", - "theme": "Тема", - "system": "Системная", - "light": "Светлая", - "dark": "Темная", - "color_scheme": "Цветовая схема", - "def": "По умолчанию", - "purple": "Фиолетовая", - "blue": "Синяя", - "green": "Зелёная", - "indigo": "Индиго", - "lime": "Салатовая", - "red": "Красная", - "yellow": "Жёлтая", - "about": "О программе", - "activate": "включить", - "deactivate": "выключить", - "debug_mode": "Режим отладки", - "debug_mode_confirmation": "Вы уверены, что хотите {mode} режим отладки? Он создан специально для разработчиков для тестирования специфических функций приложения на не поддерживаемых платформах.", - "debug": "Отладка", - "debug_page_warning": "С помощью этого меню вы можете убить Ваш Chameleon НАВСЕГДА.", - "warned": "Вы были предупреждены.", - "platform": "Платформа", - "android": "Android", - "serial_protocol": "Последовательный протокол", - "chameleon_connected": "Chameleon подключен", - "chameleon_device_type": "Тип устройства", - "nested_attack": "Запустить Nested атаку на карте", - "darkside_attack": "Запустить Darkside атаку на карте", - "copy_uid": "Копировать карту UID в эмулятор", - "test_naming": "Тест именования", - "test_nested_lib": "Тестирование Nested библиотеки", - "test_darkside_lib": "Тестирование Darkside библиотеки", - "dfu_flash_ultra": "Установить Ultra ПО через DFU", - "dfu_flash_lite": "Установить Lite ПО через DFU", - "safe_option": "Безопасный вариант", - "restart_chameleon": "Перезапустить Chameleon", - "error": "Ошибка", - "chameleon_is_dfu": "Chameleon в режиме DFU.", - "firmware_is_corrupted": "Скорее всего, это означает, что ваша прошивка повреждена. Хотите установить последнюю версию ПО?", - "flash": "Прошить", - "dfu": " (DFU)", - "keys": "Ключи", - "found_keys": "Найденные ключи", - "please_wait": "Пожалуйста, подождите", - "used_slots": "Используемые слоты", - "firmware_version": "Версия ПО", - "update_error": "Ошибка обновления", - "up_to_date": "Ваша версия ПО Chameleon {model} является актуальной", - "downloading_fw": "Загрузка и подготовка новой прошивки Chameleon {model}...", - "check_updates": "Проверить обновления", - "emulator_mode": "Перейти в режим эмулятора", - "reader_mode": "Перейти в режим чтения", - "recover_keys_via": "Восстановить ключи через {mode}", - "recover_keys": "Восстановить ключи", - "recover_keys_nonce": "Восстановить ключи из {number} nonce(s)", - "restart_required": "Требуется перезапуск", - "take_effects": "Изменения вступят в силу после перезапуска приложения", - "language": "Язык", - "sidebar_expansion": "Расширение боковой панели", - "expand": "Раскрыть", - "retract": "Сжать", - "auto": "Автоматически", - "restart_now": "Перезапустить сейчас", - "about_text": "Инструмент для графического управления и настройки вашего Chameleon Ultra/Lite, написанный на Flutter и работающий на ПК и мобильных устройствах.", - "version": "Версия", - "developed_by": "Разработан", - "license": "Лицензия", - "thanks_for_support": "Спасибо всем, кто поддерживает нас на Open Collective!", - "code_contributors": "Люди, которые внесли вклад в код", - "not_implemented": "Не реализовано", - "edit_data": "Изменить данные", - "enter_data": "Введите данные", - "sector": "Сектор", - "edit_card": "Редактировать карту", - "please_enter_name": "Пожалуйста, введите имя", - "name": "Имя", - "enter_name": "Введите имя карты", - "pick_color": "Выберите цвет", - "reset_default": "Сброс по умолчанию", - "please_enter_something": "Пожалуйста, введите {name}", - "uid": "UID", - "sak": "SAK", - "atqa": "ATQA", - "enter_something": "Введите {name}", - "must_or": "{name} должен быть длиной {a} или {b} байт.", - "must_be": "{name} должен быть длиной {a} байт.", - "device_settings": "Настройки устройства", - "firmware_management": "Управление прошивкой", - "enter_dfu": "Войти в режим DFU", - "flash_via_dfu": "Прошить последнюю версию ПО через DFU", - "flash_zip_dfu": "Прошить .zip ПО через DFU", - "animations": "Анимация", - "button_config": "Настройка кнопок", - "button_x": "Кнопка {x}", - "long_press": "Длительное нажатие", - "disable": "Выкл", - "forward": "Вперед", - "backward": "Назад", - "clone_uid": "Скопировать UID", - "other": "Другое", - "reset_settings": "Сбросить настройки", - "factory_reset": "Возврат к заводским настройкам", - "factory_sure": "Вы уверены, что хотите сбросить ваш Chameleon?", - "full": "Полная", - "mini": "Мини", - "none": "Выкл", - "edit_dictionary": "Редактирование словаря", - "enter_dict_name": "Введите название словаря", - "enter_dict_keys": "Введите ключи", - "empty": "Пусто", - "slot_settings": "Настройки слотов", - "slot_status": "Статус слота", - "hf": "ВЧ", - "lf": "НЧ", - "mifare_clasic_e_s": "Настройка эмулятора Mifare Classic", - "mode_gen1a": "Режим Gen1A", - "mode_gen2": "Режим Gen2", - "use_from_block": "Использовать UID/SAK/ATQA из 0 блока", - "collect_nonces": "Сбор nonces ({type})", - "present_cham_reader_keys": "Представьте Chameleon считывателю для восстановления ключей", - "ena_coll_recover_keys": "Включите сбор для восстановления ключей", - "write_mode": "Режим записи", - "normal": "Обычный", - "decline": "Запрет", - "deceive": "Сброс", - "shadow": "Теневой", - "outdated_fw": "Устаревшее ПО", - "unknown": "Неизвестно", - "recovery_error_no_supported": "Восстановление ключей с этой карты пока не поддерживается", - "recovery_error_no_keys_darkside": "Нет ключей и карта не уязвима к атаке Darkside", - "recovery_error_dict": "Что-то пошло не так при проверке словаря", - "recovery_error_dump_data": "Что-то пошло не так во время сохранения данных", - "output_file": "Выберите файл для сохранения", - "hf_tag_info": "Информация ВЧ теге", - "lf_tag_info": "Информация НЧ теге", - "no_card_found": "Карта не найдена. Попробуйте переместить Chameleon на карте", - "no_supported": "Действие не поддерживается", - "lite_no_read": "Chameleon Lite не поддерживает чтение карт", - "read": "Прочитать", - "write": "Записать", - "save_only_uid": "Сохранить только UID", - "letter_space": "{letter} ", - "dump_partial_data": "Считать частичные данные", - "additional_key_dict": "Дополнительный словарь ключей", - "check_keys_dict": "Проверить ключи из словаря", - "dump_card": "Считать данные с карты", - "save_as": "Сохранить как {name}", - "correct_tag_deta": "Исправьте сведения о теге", - "uid_len": "Длина UID {len} байт", - "tag_type": "Тип тега", - "select_save_format": "Выберите формат файла", - "key_count": "Количество ключей", - "all": "Все", - "no_name": "Без имени" -} + "@@locale": "ru", + "ok": "ОК", + "cancel": "Отмена", + "close": "Закрыть", + "save": "Сохранить", + "no": "Нет", + "yes": "Да", + "enabled": "Включено", + "disabled": "Отключено", + "available": "Доступно", + "unavailable": "Недоступно", + "connect": "Подключить", + "home": "Главная", + "card": "Карта", + "cards": "Карты", + "dictionary": "Словарь", + "dictionaries": "Словари", + "slot": "Слот", + "slots": "Слоты", + "slot_manager": "Менеджер слотов", + "saved_cards": "Сохраненные карты", + "read_card": "Чтение карт", + "write_card": "Запись карт", + "settings": "Настройки", + "theme": "Тема", + "system": "Системная", + "light": "Светлая", + "dark": "Темная", + "color_scheme": "Цветовая схема", + "def": "По умолчанию", + "purple": "Фиолетовая", + "blue": "Синяя", + "green": "Зелёная", + "indigo": "Индиго", + "lime": "Салатовая", + "red": "Красная", + "yellow": "Жёлтая", + "about": "О программе", + "activate": "включить", + "deactivate": "выключить", + "debug_mode": "Режим отладки", + "debug_mode_confirmation": "Вы уверены, что хотите {mode} режим отладки? Он создан специально для разработчиков для тестирования специфических функций приложения на не поддерживаемых платформах.", + "debug": "Отладка", + "debug_page_warning": "С помощью этого меню вы можете убить Ваш Chameleon НАВСЕГДА.", + "warned": "Вы были предупреждены.", + "platform": "Платформа", + "android": "Android", + "serial_protocol": "Последовательный протокол", + "chameleon_connected": "Chameleon подключен", + "chameleon_device_type": "Тип устройства", + "nested_attack": "Запустить Nested атаку на карте", + "darkside_attack": "Запустить Darkside атаку на карте", + "copy_uid": "Копировать карту UID в эмулятор", + "test_naming": "Тест именования", + "test_nested_lib": "Тестирование Nested библиотеки", + "test_darkside_lib": "Тестирование Darkside библиотеки", + "dfu_flash_ultra": "Установить Ultra ПО через DFU", + "dfu_flash_lite": "Установить Lite ПО через DFU", + "safe_option": "Безопасный вариант", + "restart_chameleon": "Перезапустить Chameleon", + "error": "Ошибка", + "chameleon_is_dfu": "Chameleon в режиме DFU.", + "firmware_is_corrupted": "Скорее всего, это означает, что ваша прошивка повреждена. Хотите установить последнюю версию ПО?", + "flash": "Прошить", + "dfu": " (DFU)", + "keys": "Ключи", + "found_keys": "Найденные ключи", + "please_wait": "Пожалуйста, подождите", + "used_slots": "Используемые слоты", + "firmware_version": "Версия ПО", + "update_error": "Ошибка обновления", + "up_to_date": "Ваша версия ПО Chameleon {model} является актуальной", + "downloading_fw": "Загрузка и подготовка новой прошивки Chameleon {model}...", + "check_updates": "Проверить обновления", + "emulator_mode": "Перейти в режим эмулятора", + "reader_mode": "Перейти в режим чтения", + "recover_keys_via": "Восстановить ключи через {mode}", + "recover_keys": "Восстановить ключи", + "recover_keys_nonce": "Восстановить ключи из {number} nonce(s)", + "restart_required": "Требуется перезапуск", + "take_effects": "Изменения вступят в силу после перезапуска приложения", + "language": "Язык", + "sidebar_expansion": "Расширение боковой панели", + "expand": "Раскрыть", + "retract": "Сжать", + "auto": "Автоматически", + "restart_now": "Перезапустить сейчас", + "about_text": "Инструмент для графического управления и настройки вашего Chameleon Ultra/Lite, написанный на Flutter и работающий на ПК и мобильных устройствах.", + "version": "Версия", + "developed_by": "Разработан", + "license": "Лицензия", + "thanks_for_support": "Спасибо всем, кто поддерживает нас на Open Collective!", + "code_contributors": "Люди, которые внесли вклад в код", + "not_implemented": "Не реализовано", + "edit_data": "Изменить данные", + "enter_data": "Введите данные", + "sector": "Сектор", + "edit_card": "Редактировать карту", + "please_enter_name": "Пожалуйста, введите имя", + "name": "Имя", + "enter_name": "Введите имя карты", + "pick_color": "Выберите цвет", + "reset_default": "Сброс по умолчанию", + "please_enter_something": "Пожалуйста, введите {name}", + "uid": "UID", + "sak": "SAK", + "atqa": "ATQA", + "enter_something": "Введите {name}", + "must_or": "{name} должен быть длиной {a} или {b} байт.", + "must_be": "{name} должен быть длиной {a} байт.", + "device_settings": "Настройки устройства", + "firmware_management": "Управление прошивкой", + "enter_dfu": "Войти в режим DFU", + "flash_via_dfu": "Прошить последнюю версию ПО через DFU", + "flash_zip_dfu": "Прошить .zip ПО через DFU", + "animations": "Анимация", + "button_config": "Настройка кнопок", + "button_x": "Кнопка {x}", + "long_press": "Длительное нажатие", + "disable": "Выкл", + "forward": "Вперед", + "backward": "Назад", + "clone_uid": "Скопировать UID", + "other": "Другое", + "reset_settings": "Сбросить настройки", + "factory_reset": "Возврат к заводским настройкам", + "factory_sure": "Вы уверены, что хотите сбросить ваш Chameleon?", + "full": "Полная", + "mini": "Мини", + "none": "Выкл", + "edit_dictionary": "Редактирование словаря", + "enter_dict_name": "Введите название словаря", + "enter_dict_keys": "Введите ключи", + "empty": "Пусто", + "slot_settings": "Настройки слотов", + "slot_status": "Статус слота", + "hf": "ВЧ", + "lf": "НЧ", + "mifare_clasic_e_s": "Настройка эмулятора Mifare Classic", + "mode_gen1a": "Режим Gen1A", + "mode_gen2": "Режим Gen2", + "use_from_block": "Использовать UID/SAK/ATQA из 0 блока", + "collect_nonces": "Сбор nonces ({type})", + "present_cham_reader_keys": "Представьте Chameleon считывателю для восстановления ключей", + "ena_coll_recover_keys": "Включите сбор для восстановления ключей", + "write_mode": "Режим записи", + "normal": "Обычный", + "decline": "Запрет", + "deceive": "Сброс", + "shadow": "Теневой", + "outdated_fw": "Устаревшее ПО", + "unknown": "Неизвестно", + "recovery_error_no_supported": "Восстановление ключей с этой карты пока не поддерживается", + "recovery_error_no_keys_darkside": "Нет ключей и карта не уязвима к атаке Darkside", + "recovery_error_dict": "Что-то пошло не так при проверке словаря", + "recovery_error_dump_data": "Что-то пошло не так во время сохранения данных", + "output_file": "Выберите файл для сохранения", + "hf_tag_info": "Информация ВЧ теге", + "lf_tag_info": "Информация НЧ теге", + "no_card_found": "Карта не найдена. Попробуйте переместить Chameleon на карте", + "no_supported": "Действие не поддерживается", + "lite_no_read": "Chameleon Lite не поддерживает чтение карт", + "read": "Прочитать", + "write": "Записать", + "save_only_uid": "Сохранить только UID", + "letter_space": "{letter} ", + "dump_partial_data": "Считать частичные данные", + "additional_key_dict": "Дополнительный словарь ключей", + "check_keys_dict": "Проверить ключи из словаря", + "dump_card": "Считать данные с карты", + "save_as": "Сохранить как {name}", + "correct_tag_data": "Исправьте сведения о теге", + "uid_len": "Длина UID {len} байт", + "tag_type": "Тип тега", + "select_save_format": "Выберите формат файла", + "key_count": "Количество ключей", + "all": "Все", + "no_name": "Без имени", + "connecting_to_ble": "Подключение к BLE устройству...", + "default_ble_password": "123456 - пароль по умолчанию для подключения через BLE", + "connection_might_take_some_time": "Первое подключение может занять некоторое время" +} \ No newline at end of file diff --git a/chameleonultragui/lib/main.dart b/chameleonultragui/lib/main.dart index e7428187..04fb03e8 100644 --- a/chameleonultragui/lib/main.dart +++ b/chameleonultragui/lib/main.dart @@ -20,6 +20,7 @@ import 'package:chameleonultragui/gui/page/flashing.dart'; import 'package:chameleonultragui/gui/page/mfkey32.dart'; import 'package:chameleonultragui/gui/page/read_card.dart'; import 'package:chameleonultragui/gui/page/write_card.dart'; +import 'package:chameleonultragui/gui/page/pending_connection.dart'; // Localizations import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -154,14 +155,18 @@ class _MyHomePageState extends State { switch (selectedIndex) { // Sidebar Navigation case 0: - if (appState.connector.connected) { - if (appState.connector.isDFU) { - page = const FlashingPage(); + if (appState.connector.pendingConnection) { + page = const PendingConnectionPage(); + } else { + if (appState.connector.connected) { + if (appState.connector.isDFU) { + page = const FlashingPage(); + } else { + page = const HomePage(); + } } else { - page = const HomePage(); + page = const ConnectPage(); } - } else { - page = const ConnectPage(); } break; case 1: @@ -214,15 +219,10 @@ class _MyHomePageState extends State { AppLocalizations.of(context)!.home), // Home ), NavigationRailDestination( - icon: Icon(Icons.widgets, - color: appState.connector.connected == false - ? Colors.grey - : null), + disabled: !appState.connector.connected, + icon: const Icon(Icons.widgets), label: Text( - AppLocalizations.of(context)!.slot_manager, - style: appState.connector.connected == false - ? const TextStyle(color: Colors.grey) - : null), + AppLocalizations.of(context)!.slot_manager), ), NavigationRailDestination( icon: @@ -231,25 +231,16 @@ class _MyHomePageState extends State { Text(AppLocalizations.of(context)!.saved_cards), ), NavigationRailDestination( - icon: Icon(Icons.wifi, - color: appState.connector.connected == false - ? Colors.grey - : null), - label: Text(AppLocalizations.of(context)!.read_card, - style: appState.connector.connected == false - ? const TextStyle(color: Colors.grey) - : null), + disabled: !appState.connector.connected, + icon: const Icon(Icons.wifi), + label: + Text(AppLocalizations.of(context)!.read_card), ), NavigationRailDestination( - icon: Icon(Icons.credit_card, - color: appState.connector.connected == false - ? Colors.grey - : null), - label: Text( - AppLocalizations.of(context)!.write_card, - style: appState.connector.connected == false - ? const TextStyle(color: Colors.grey) - : null), + disabled: !appState.connector.connected, + icon: const Icon(Icons.credit_card), + label: + Text(AppLocalizations.of(context)!.write_card), ), NavigationRailDestination( icon: const Icon(Icons.settings), @@ -290,7 +281,7 @@ class BottomProgressBar extends StatelessWidget { @override Widget build(BuildContext context) { var appState = context.watch(); - return (appState.connector.connected == true && appState.connector.isDFU) + return (appState.connector.connected && appState.connector.isDFU) ? LinearProgressIndicator( value: appState.progress, backgroundColor: Colors.grey[300], diff --git a/chameleonultragui/lib/sharedprefsprovider.dart b/chameleonultragui/lib/sharedprefsprovider.dart index bc6603a7..dbd319a4 100644 --- a/chameleonultragui/lib/sharedprefsprovider.dart +++ b/chameleonultragui/lib/sharedprefsprovider.dart @@ -284,16 +284,13 @@ class SharedPreferencesProvider extends ChangeNotifier { var ccode = loc.toString().split("-").last; if (!AppLocalizations.supportedLocales.contains(Locale(lcode, ccode))) { return const Locale('en'); - } - else { + } else { return Locale(lcode, ccode); } - } - else if(loc != null) { + } else if (loc != null) { if (!AppLocalizations.supportedLocales.contains(Locale(loc.toString()))) { return const Locale('en'); - } - else { + } else { return Locale(loc.toString()); } } @@ -317,6 +314,8 @@ class SharedPreferencesProvider extends ChangeNotifier { return 'Русский'; case 'fr': return 'Français'; + case 'ko': + return '한국어'; default: return 'English'; } diff --git a/chameleonultragui/pubspec.lock b/chameleonultragui/pubspec.lock index c6c4f609..e8d6c3a3 100644 --- a/chameleonultragui/pubspec.lock +++ b/chameleonultragui/pubspec.lock @@ -89,14 +89,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.dev" - source: hosted - version: "1.0.5" dylib: dependency: "direct main" description: @@ -141,10 +133,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 url: "https://pub.dev" source: hosted - version: "5.3.3" + version: "5.5.0" file_saver: dependency: "direct main" description: @@ -194,10 +186,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -207,26 +199,26 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" flutter_reactive_ble: dependency: "direct main" description: name: flutter_reactive_ble - sha256: dfcd3cca271a054121ef96fde0fdad43274a5a38b423fc3f519fd9edc43e42c9 + sha256: "7a0d245412dc8e1b72ce2adc423808583b42ce824b1be74001ff22c8bb5ada48" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" flutter_staggered_grid_view: dependency: "direct main" description: name: flutter_staggered_grid_view - sha256: f0b6d8c0fa7b4b444985cdde68492c0138a4fb6fc57a641b24cb234b7ee0f5c4 + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.7.0" flutter_test: dependency: "direct dev" description: flutter @@ -401,50 +393,50 @@ packages: dependency: transitive description: name: path_provider - sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" permission_handler: dependency: "direct main" description: @@ -457,10 +449,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" + sha256: d74e77a5ecd38649905db0a7d05ef16bed42ff263b9efb73ed794317c5764ec3 url: "https://pub.dev" source: hosted - version: "10.3.3" + version: "10.3.4" permission_handler_apple: dependency: transitive description: @@ -497,18 +489,18 @@ packages: dependency: transitive description: name: platform - sha256: "57c07bf82207aee366dfaa3867b3164e4f03a238a461a11b0e8a3a510d51203d" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" pointycastle: dependency: transitive description: @@ -545,82 +537,74 @@ packages: dependency: transitive description: name: reactive_ble_mobile - sha256: "2fdd8bcd00533f3e1e544030edeba723ce2dfdd24c3b22a04103b410b1272290" + sha256: e4623446d5fd6e641c984892ee1fa7c67499a2bb0971d85a500815e1d05db6fb url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" reactive_ble_platform_interface: dependency: transitive description: name: reactive_ble_platform_interface - sha256: d0c0041de584df5c697d276ae8c3ddc767b2c364ed29b4ae25126c57e4467cba + sha256: "8988d16497886dccc69dca1c3eebce28ae387371f3f948a4f1b03dec9954fb05" url: "https://pub.dev" source: hosted - version: "5.1.1" - serial_communication: - dependency: "direct main" - description: - name: serial_communication - sha256: d00689a1c47500299feafc0aed1cf180683ecc120f8d3c2b38afc7be2423142a - url: "https://pub.dev" - source: hosted - version: "0.0.2" + version: "5.2.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" sky_engine: dependency: transitive description: flutter @@ -710,66 +694,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + sha256: "38d8e783681bc342e92dc9799cbe4421d08c4d210a67ee9d61d0f7310491a465" url: "https://pub.dev" source: hosted - version: "6.1.12" + version: "6.1.13" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025" + sha256: ef7e34951ffa963fb7a65928deeb38d40fb3c5975baf93c1d631341ff7f2650a url: "https://pub.dev" source: hosted - version: "6.0.38" + version: "6.0.39" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "4d0dae953f80dc06fa24c58d0f8381302139c22c2dad301417787ad96f5f73bd" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.0.20" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.8" usb_serial: dependency: "direct main" description: @@ -807,18 +791,18 @@ packages: dependency: transitive description: name: win32 - sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.0.7" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" xml: dependency: transitive description: @@ -844,5 +828,5 @@ packages: source: hosted version: "2.1.1" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/chameleonultragui/pubspec.yaml b/chameleonultragui/pubspec.yaml index eb97c366..6e809733 100644 --- a/chameleonultragui/pubspec.yaml +++ b/chameleonultragui/pubspec.yaml @@ -36,34 +36,32 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 flutter_libserialport: ^0.3.0 - provider: ^6.0.0 - serial_communication: ^0.0.2 + provider: ^6.0.5 logger: ^2.0.1 convert: ^3.1.1 - shared_preferences: ^2.1.2 + shared_preferences: ^2.2.1 ffigen: ^8.0.2 dylib: ^0.3.3 - ffi: ^2.0.2 + ffi: ^2.1.0 usb_serial: git: url: https://github.com/Foxushka/usbserial.git ref: fixes - file_saver: ^0.2.4 - file_picker: ^5.3.2 - flutter_staggered_grid_view: 0.4.1 # TODO: upgrade to latest + file_saver: ^0.2.8 + file_picker: ^5.5.0 + flutter_staggered_grid_view: ^0.7.0 uuid: ^3.0.7 http: ^1.1.0 archive: ^3.3.7 - flutter_reactive_ble: ^5.1.1 + flutter_reactive_ble: ^5.2.0 permission_handler: ^10.4.3 - protobuf: ^2.0.0 + protobuf: ^2.1.0 collection: ^1.17.1 crypto: ^3.0.3 flutter_colorpicker: ^1.0.3 package_info_plus: ^4.1.0 - url_launcher: ^6.1.12 + url_launcher: ^6.1.13 flutter_localizations: sdk: flutter intl: any diff --git a/chameleonultragui/untranslated_messages.json b/chameleonultragui/untranslated_messages.json index 9e26dfee..212d8d5a 100644 --- a/chameleonultragui/untranslated_messages.json +++ b/chameleonultragui/untranslated_messages.json @@ -1 +1,27 @@ -{} \ No newline at end of file +{ + "de": [ + "connecting_to_ble", + "default_ble_password", + "connection_might_take_some_time" + ], + + "de_AT": [ + "connecting_to_ble", + "default_ble_password", + "connection_might_take_some_time" + ], + + "es": [ + "connecting_to_ble", + "default_ble_password", + "connection_might_take_some_time" + ], + + "fr": [ + "unavailable", + "correct_tag_data", + "connecting_to_ble", + "default_ble_password", + "connection_might_take_some_time" + ] +}