Compare commits

..

48 Commits

Author SHA1 Message Date
zjs81 1c9c089a53 Remove 'jni' from Flutter plugin and FFI plugin lists in generated CMake files 2026-04-14 21:58:39 -07:00
zjs81 cb3b5a84eb Merge pull request #387 from zjs81/dev
translations
2026-04-14 21:39:50 -07:00
zjs81 a4bbeffddc Merge pull request #386 from zjs81/dev_translations
Dev translations
2026-04-14 21:38:52 -07:00
zjs81 37ec8f2f05 Add localization for chat and repeater features in multiple languages
- Added translations for "Send message", "Guest information", and "Guest tools" in Bulgarian, German, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated the "Clock synchronization after login" feature subtitle in all affected languages.
- Removed untranslated keys from the untranslated.json file as they have now been localized.
2026-04-14 21:38:12 -07:00
zjs81 39cd6d5514 Merge pull request #385 from zjs81/dev
merge dev to main
2026-04-14 21:04:04 -07:00
zjs81 44eb4fad58 Merge pull request #361 from zjs81/unused-plugin
remove unused macos path_provider_foundation
2026-04-14 21:02:30 -07:00
zjs81 1a209cbcfc Merge pull request #372 from zjs81/group-elem
fix: settings dialog lists
2026-04-14 21:02:08 -07:00
zjs81 33a8f34463 Merge pull request #365 from zjs81/rpt-guest
enh: make repeater admin guest aware
2026-04-14 20:44:14 -07:00
zjs81 ce8e8f0d5b Merge pull request #384 from zjs81/clear_toast
clear toast on tap
2026-04-14 20:42:23 -07:00
Enot (ded) Skelly aa2d0f1927 clear toast on tap
this adds a generator showDismissibleSnackBar which by default allows
tapping to clear snack bar toasts. all SnackBar properties are still
available and the

all callers should now use showDismissibleSnackBar() instead of calling
ScaffoldMessenger.of(context).showSnackBar(SnackBar())
2026-04-14 12:01:42 -07:00
Ded 0757c8e53a Merge pull request #369 from just-stuff-tm/auto-time-sync-349
add auto clock synchronization setting after repeater login
2026-04-13 08:20:01 -07:00
Enot (ded) Skelly add4731d05 fix: settings dialog lists
switched to using RadioListTile instead of ListTile to be more accessible
2026-04-10 15:11:44 -07:00
Enot (ded) Skelly 7dc162d968 temp
translations fix
2026-04-10 14:15:14 -07:00
just-stuff-tm 8ba4bbfbc5 add auto clock synchronization setting after repeater login
Introduced a new setting for automatic clock synchronization after a successful repeater login.
Added localization support for the new feature in multiple languages (Bulgarian, German, English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, Chinese).
Implemented storage service methods to manage the new setting.
Updated the repeater settings screen to include a toggle for the new feature.
Enhanced the repeater login dialog to trigger clock synchronization automatically if the setting is enabled.
2026-04-10 14:25:53 -04:00
Ded cac6abfef1 Fix dev
rebase dev over main and resolve merge conflicts
2026-04-09 10:12:47 -07:00
Enot (ded) Skelly 5354acb1d3 clean up after merge conflicts 2026-04-09 09:57:46 -07:00
Enot (ded) Skelly fae416fb34 Merge branch 'dev' of github.com:zjs81/meshcore-open into dev 2026-04-09 09:50:36 -07:00
Enot (ded) Skelly 69433b6d89 small clean up from PR #275
just removes extraneous assignment to _lastNonRepeatSnapshot and moves
the Navigator pop to after all uses of the context in _RadioSettingsDialog
2026-04-09 09:41:02 -07:00
just-stuff-tm ea3b9609fc fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging
- Replace floating-point epsilon frequency comparison with integer Hz
- Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot
  conversion methods on _RadioSettingsSnapshot
- Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions
- Gate _logRadioSettingsState behind kDebugMode
- Use integer Hz in == and hashCode for _RadioSettingsSnapshot

Addresses code review findings on preset/off-grid repeat toggle PR.
2026-04-09 09:41:02 -07:00
just-stuff-tm 20a9939314 fix(settings): scope repeat preset memory to saved state 2026-04-09 09:41:02 -07:00
just-stuff-tm c7b7deb0f6 fix(settings): preserve preset across off-grid repeat 2026-04-09 09:41:02 -07:00
just-stuff-tm 82e04e8090 Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183"
This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527.
2026-04-09 09:41:02 -07:00
Enot (ded) Skelly f299608296 use l10n strings for discovered menu item 2026-04-09 09:41:02 -07:00
ericz 7dcec5b4ee moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. 2026-04-09 09:41:02 -07:00
ericszimmermann e4684b585a codex suggested fix: explicit check if contact location is not null
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-09 09:41:02 -07:00
ericz 8386f262e1 reimplement location aware snr-indikator after alpha7 2026-04-09 09:41:02 -07:00
Enot (ded) Skelly 45cd8a56a3 add jni to generated plugins
linux and windows were missing jni which was being added on fresh builds from dev
2026-04-09 09:41:02 -07:00
Enot (ded) Skelly 754f8a6c62 add fvm directory and rc file to gitignore 2026-04-09 09:41:02 -07:00
Enot (ded) Skelly c4f54efd77 add tooltip to send message buttons 2026-04-09 09:41:02 -07:00
Winston Lowe 637e08d22c Update ML timeout handling and adjust distance threshold for path hops 2026-04-09 09:40:24 -07:00
Winston Lowe 32dc0fca22 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-09 09:40:06 -07:00
n-kam b5aa294fc1 make unread badge max out at 9999+ not 99+ 2026-04-09 09:30:25 -07:00
Winston Lowe 26516baf67 Update ML timeout handling and adjust distance threshold for path hops 2026-04-09 09:30:25 -07:00
Winston Lowe 4879b136f8 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-09 09:30:25 -07:00
Ded 89a14c2719 Merge pull request #347 from zjs81/add-contribution
init contributing.md
2026-04-07 14:37:35 -07:00
Enot (ded) Skelly 4ad01ed43c init contributing.md 2026-04-07 13:01:46 -07:00
zjs81 ffaa4033ae Merge pull request #321 from just-stuff-tm/main
Add additional device name prefixes to MeshCoreUuids
2026-04-06 23:04:29 -07:00
zjs81 1a4fd1b477 Merge pull request #339 from ericszimmermann/ez_fix_coordinates
Preserve Coordinates with contact.copyWith() function
2026-04-06 22:58:21 -07:00
zjs81 e1555ce380 Merge pull request #337 from interfect/lowmesh
Add LowMesh prefix and explain how to add more
2026-04-06 22:51:44 -07:00
zjs81 c7933d363b Merge pull request #342 from interfect/graceful-gif-render
Support receiving more formats of GIF message
2026-04-06 14:28:19 -07:00
Zach 08ffb978cf fix: gif trnslat 2026-04-06 14:26:42 -07:00
Adam Novak c5ec60638c Put reaction and GIF helpers in charge of encoding 2026-04-06 02:09:40 -04:00
Adam Novak 75ec3b6116 Centralize GIF parsing in a helper like for reactions 2026-04-06 01:57:51 -04:00
Adam Novak 45c9823c6f Escape forward slashes in regexes 2026-04-05 22:51:48 -04:00
Adam Novak 45658a7612 Understand more kinds of Giphy reference as GIF
This adds Giphy page URLs and `media.giphy.com` URLs (with and without
protocols) as *accepted* encodings for GIF messages, alongside the `g:`
syntax.

When someone posts such a URL by itself as a message, it will be rendered inline just like `g:` messages are now.

This does not change the encoding that GIF messages are *sent* in; that
is still the `g:` syntax.
2026-04-05 22:39:20 -04:00
ericz 7633327f45 Previously, the merge only preserved path override fields and could overwrite existing GPS with null when the incoming frame had 0,0 coordinates.
Now it also preserves prior coordinates when the incoming update omits location.
2026-04-05 14:06:23 +02:00
Adam Novak 6b4b2d7ce6 Add LowMesh prefix and explain how to add more 2026-04-04 19:40:39 -04:00
just-stuff-tm e4e8bfa4ef Add additional device name prefixes to MeshCoreUuids 2026-03-28 12:20:27 -04:00
80 changed files with 1538 additions and 786 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
## BLE Frames & Protocol Notes ## BLE Frames & Protocol Notes
- Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`. - Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`.
- Discovery: scans for device name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`. - Discovery: scans for device names matching known prefixes and filters by `platformName`/`advertisementData.advName`.
- Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`. - Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`.
- Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57. - Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57.
- Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25. - Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25.
+1 -1
View File
@@ -61,7 +61,7 @@ lib/
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
### Device Discovery ### Device Discovery
- Scans for devices with name prefix `MeshCore-` - Scans for devices with known name prefixes
- Filters by `platformName` or `advertisementData.advName` - Filters by `platformName` or `advertisementData.advName`
### Connection States ### Connection States
+71
View File
@@ -0,0 +1,71 @@
# How to contribute to Meshcore Open
Before submitting any pull requests (PR), please review the following information.
Unsolicited PRs without previous discussion or open issues may be
rejected. As may changes that are too broad (i.e. 100 files changed) or that
cover too many separate changes. If the changes are clearly AI generated they
may also be rejected. [See more](#ai-use)
## First Step Checklist
### **Did you find a bug?**
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/zjs81/meshcore-open/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/zjs81/meshcore-open/issues/new).
Be sure to include a **title and clear description**, as much relevant
information as possible, and a **code sample** or an **executable test case**
demonstrating the expected behavior that is not occurring. You can also include
screenshots or video.
* DO NOT start work and submit a PR at this time, please discuss the issue and
your implementation plan first.
### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
Changes that are cosmetic in nature and do not add anything substantial to the
stability, functionality, or testability of the application will generally not
be accepted.
### **Do you intend to add a new feature or change an existing one?**
* Suggest your change in a new issue as a feature request.
* DO NOT start work and submit a PR at this time, please discuss the change and
your implementation plan first.
* After it is generally decided that the feature or change fits the goals of the
project you can start work or open a PR if you have already started.
## Submitting your patch
* All changes should be based on the `dev` branch. When creating your PR please
be sure to change the target to merge into dev, and when starting work on a new
branch be sure to start on latest `dev`.
* Ensure the PR description clearly describes the problem and solution. Include
the relevant issue number if applicable.
* The PR should contain **one commit** only, the commit message should have a
clear title followed by a new line and then brief description if needed. PR with
multiple commits will be squashed into one before merging if required. See
[Git Mastery](https://git-mastery.org/lessons/commitMessage/) for more
information on good commit messages.
* **Before committing changes** on your branch, be sure to run both
`dart format .` and `flutter analyze`. The continuous development checks will
fail if issues here are not addressed before hand.
## AI-use
Everyone loves some help, AI agents are a tool in many of our belts. The project
is not anti-AI.
There are some limits to acceptable use however. Generally:
* All code generated by AI should be thoroughly reviewed by the contributor.
* The changes should be tightly controlled to not change anything out of scope
for the patch, bug fix, etc.
* The contributor should have a good understanding of what the code does and how
the application works in order to effectively be able to manage the agent.
+11 -2
View File
@@ -150,7 +150,8 @@ lib/
├── main.dart # App entry point ├── main.dart # App entry point
├── connector/ ├── connector/
│ ├── meshcore_connector.dart # BLE communication & state management │ ├── meshcore_connector.dart # BLE communication & state management
── meshcore_protocol.dart # Protocol definitions & frame parsing ── meshcore_protocol.dart # Protocol definitions & frame parsing
│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!)
├── screens/ ├── screens/
│ ├── scanner_screen.dart # Device scanning (home screen) │ ├── scanner_screen.dart # Device scanning (home screen)
│ ├── contacts_screen.dart # Contact list │ ├── contacts_screen.dart # Contact list
@@ -184,7 +185,15 @@ lib/
### Device Discovery ### Device Discovery
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-` Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently:
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
### Message Format ### Message Format
+6 -1
View File
@@ -21,7 +21,12 @@ The MeshCore BLE protocol implements a binary frame-based communication system u
### Connection Flow ### Connection Flow
1. **Scan** for devices with name prefix `MeshCore-` 1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
2. **Connect** with 15-second timeout 2. **Connect** with 15-second timeout
3. **Request MTU** of 185 bytes (falls back to default if unsupported) 3. **Request MTU** of 185 bytes (falls back to default if unsupported)
4. **Discover services** and locate NUS characteristics 4. **Discover services** and locate NUS characteristics
+6 -1
View File
@@ -49,7 +49,12 @@ enum MeshCoreConnectionState {
## BLE Connection Lifecycle ## BLE Connection Lifecycle
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]` 1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
2. **Connect** with 15-second timeout 2. **Connect** with 15-second timeout
3. **Request MTU** 185 bytes (non-web only) 3. **Request MTU** 185 bytes (non-web only)
4. **Discover services** and locate NUS 4. **Discover services** and locate NUS
+4 -1
View File
@@ -3976,11 +3976,14 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'Connector', tag: 'Connector',
); );
// CRITICAL: Preserve user's path override when contact is refreshed from device // Preserve user-selected path settings and previously known GPS when
// refreshed frames omit coordinates (lat/lon encoded as 0,0).
_contacts[existingIndex] = contact.copyWith( _contacts[existingIndex] = contact.copyWith(
lastMessageAt: mergedLastMessageAt, lastMessageAt: mergedLastMessageAt,
pathOverride: existing.pathOverride, // Preserve user's path choice pathOverride: existing.pathOverride, // Preserve user's path choice
pathOverrideBytes: existing.pathOverrideBytes, pathOverrideBytes: existing.pathOverrideBytes,
latitude: contact.latitude ?? existing.latitude,
longitude: contact.longitude ?? existing.longitude,
); );
appLogger.info( appLogger.info(
+3
View File
@@ -7,6 +7,9 @@ class MeshCoreUuids {
"MeshCore-", "MeshCore-",
"Whisper-", "Whisper-",
"WisCore-", "WisCore-",
"Seeed",
"Lilygo",
"HT-", "HT-",
"LowMesh_MC_",
]; ];
} }
+38
View File
@@ -0,0 +1,38 @@
class GifHelper {
/// Parse a known GIF format, which can be any of:
/// g:GIFID
/// https://media.giphy.com/media/GIFID/giphy.gif
/// https://giphy.com/gifs/Optional-title-with-dashes-GIFID
///
/// GIFID is a Giphy GIF ID. The https:// is optional (and
/// can also be http://). The giphy.com/gifs form can also
/// include a trailing slash.
///
/// Returns null if text is not a valid GIF format
static String? parseGif(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
if (match != null) {
return match.group(1);
}
final directUrlMatch = RegExp(
r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$',
).firstMatch(trimmed);
if (directUrlMatch != null) {
return directUrlMatch.group(1);
}
// Giphy understands page URLs with just the ID, or any string and a
// dash before the ID, and redirects to a page with a dash-separated
// title, a dash, and the ID. IDs in this form *probably* can't
// contain dashes.
final pageMatch = RegExp(
r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$',
).firstMatch(trimmed);
return pageMatch?.group(1);
}
/// Encode a GIF in a format that parseGif() can parse.
static String encodeGif(String gifId) {
return 'g:$gifId';
}
}
+9 -10
View File
@@ -3,6 +3,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../helpers/snack_bar_builder.dart';
class LinkHandler { class LinkHandler {
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) { static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
@@ -93,21 +94,19 @@ class LinkHandler {
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_couldNotOpenLink(url)), content: Text(context.l10n.chat_couldNotOpenLink(url)),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_invalidLink), content: Text(context.l10n.chat_invalidLink),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+5
View File
@@ -109,4 +109,9 @@ class ReactionHelper {
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji); return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
} }
/// Encode a reaction message that parseReaction() can parse.
static String encodeReaction(String hash, String emojiIndex) {
return 'r:$hash:$emojiIndex';
}
} }
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss
// all other properties are default and optional
void showDismissibleSnackBar(
BuildContext context, {
Key? key,
required Widget content,
Color? backgroundColor,
double? elevation,
EdgeInsetsGeometry? margin,
EdgeInsetsGeometry? padding,
double? width,
ShapeBorder? shape,
HitTestBehavior? hitTestBehavior,
SnackBarBehavior? behavior,
SnackBarAction? action,
double? actionOverflowThreshold,
bool? showCloseIcon,
Color? closeIconColor,
Duration? duration,
bool? persist,
Animation<double>? animation,
void Function()? onVisible,
DismissDirection? dismissDirection,
Clip? clipBehavior,
}) {
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
SnackBar(
key: key,
content: GestureDetector(
onTap: () => messenger.hideCurrentSnackBar(),
child: content,
),
backgroundColor: backgroundColor,
elevation: elevation,
margin: margin,
padding: padding,
width: width,
shape: shape,
hitTestBehavior: hitTestBehavior,
behavior: behavior,
action: action,
actionOverflowThreshold: actionOverflowThreshold,
showCloseIcon: showCloseIcon,
closeIconColor: closeIconColor,
duration: duration ?? const Duration(seconds: 4),
persist: persist,
animation: animation,
onVisible: onVisible,
dismissDirection: dismissDirection ?? DismissDirection.down,
clipBehavior: clipBehavior ?? Clip.hardEdge,
),
);
}
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingHidePin": "Скриване на PIN кода", "scanner_linuxPairingHidePin": "Скриване на PIN кода",
"scanner_linuxPairingShowPin": "Покажи PIN", "scanner_linuxPairingShowPin": "Покажи PIN",
"repeater_cliQuickClockSync": "Синхронизация на часовника", "repeater_cliQuickClockSync": "Синхронизация на часовника",
"repeater_cliQuickDiscovery": "Открий Съседи" "repeater_cliQuickDiscovery": "Открий Съседи",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.",
"repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане",
"chat_sendMessage": "Изпратете съобщение",
"room_guest": "Информация за сървъра на стаята",
"repeater_guest": "Информация за ретранслаторите",
"repeater_guestTools": "Инструменти за гости"
} }
+13 -1
View File
@@ -2089,5 +2089,17 @@
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).", "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).",
"repeater_cliQuickClockSync": "Uhr Synchronisieren", "repeater_cliQuickClockSync": "Uhr Synchronisieren",
"repeater_cliQuickDiscovery": "Entdecke Nachbarn" "repeater_cliQuickDiscovery": "Entdecke Nachbarn",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Uhrzeit-Synchronisation nach dem Anmelden",
"repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.",
"repeater_guest": "Informationen zu Repeatern",
"repeater_guestTools": "Gastwerkzeuge",
"chat_sendMessage": "Nachricht senden",
"room_guest": "Informationen zum Room Server"
} }
+13 -2
View File
@@ -1038,8 +1038,8 @@
"login_enterPassword": "Enter password", "login_enterPassword": "Enter password",
"login_savePassword": "Save password", "login_savePassword": "Save password",
"login_savePasswordSubtitle": "Password will be stored securely on this device", "login_savePasswordSubtitle": "Password will be stored securely on this device",
"login_repeaterDescription": "Enter the repeater password to access settings and status.", "login_repeaterDescription": "Enter the repeater password for guest or admin access.",
"login_roomDescription": "Enter the room password to access settings and status.", "login_roomDescription": "Enter the room password for guest or admin access.",
"login_routing": "Routing", "login_routing": "Routing",
"login_routingMode": "Routing mode", "login_routingMode": "Routing mode",
"login_autoUseSavedPath": "Auto (use saved path)", "login_autoUseSavedPath": "Auto (use saved path)",
@@ -1105,7 +1105,10 @@
"path_setPath": "Set Path", "path_setPath": "Set Path",
"repeater_management": "Repeater Management", "repeater_management": "Repeater Management",
"room_management": "Room Server Management", "room_management": "Room Server Management",
"repeater_guest": "Repeater Information",
"room_guest": "Room Server Information",
"repeater_managementTools": "Management Tools", "repeater_managementTools": "Management Tools",
"repeater_guestTools": "Guest Tools",
"repeater_status": "Status", "repeater_status": "Status",
"repeater_statusSubtitle": "View repeater status, stats, and neighbors", "repeater_statusSubtitle": "View repeater status, stats, and neighbors",
"repeater_telemetry": "Telemetry", "repeater_telemetry": "Telemetry",
@@ -1116,6 +1119,14 @@
"repeater_neighborsSubtitle": "View zero hop neighbors.", "repeater_neighborsSubtitle": "View zero hop neighbors.",
"repeater_settings": "Settings", "repeater_settings": "Settings",
"repeater_settingsSubtitle": "Configure repeater parameters", "repeater_settingsSubtitle": "Configure repeater parameters",
"repeater_clockSyncAfterLogin": "Clock sync after login",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatically send \"clock sync\" after a successful login",
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_statusTitle": "Repeater Status", "repeater_statusTitle": "Repeater Status",
"repeater_routingMode": "Routing mode", "repeater_routingMode": "Routing mode",
"repeater_autoUseSavedPath": "Auto (use saved path)", "repeater_autoUseSavedPath": "Auto (use saved path)",
+13 -1
View File
@@ -2089,5 +2089,17 @@
"translation_translationOptions": "Opciones de traducción", "translation_translationOptions": "Opciones de traducción",
"translation_systemLanguage": "Idioma del sistema", "translation_systemLanguage": "Idioma del sistema",
"repeater_cliQuickDiscovery": "Descubrir Vecinos", "repeater_cliQuickDiscovery": "Descubrir Vecinos",
"repeater_cliQuickClockSync": "Sincronización del reloj" "repeater_cliQuickClockSync": "Sincronización del reloj",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.",
"repeater_clockSyncAfterLogin": "Sincronización del reloj después de iniciar sesión",
"repeater_guest": "Información sobre repetidores",
"chat_sendMessage": "Enviar mensaje",
"repeater_guestTools": "Herramientas para invitados",
"room_guest": "Información del servidor"
} }
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).", "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
"scanner_linuxPairingShowPin": "Afficher le code PIN", "scanner_linuxPairingShowPin": "Afficher le code PIN",
"repeater_cliQuickClockSync": "Synchronisation de l'horloge", "repeater_cliQuickClockSync": "Synchronisation de l'horloge",
"repeater_cliQuickDiscovery": "Découvrir les voisins" "repeater_cliQuickDiscovery": "Découvrir les voisins",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Envoyer automatiquement une notification \"synchronisation de l'heure\" après une connexion réussie.",
"repeater_clockSyncAfterLogin": "Synchronisation de l'horloge après la connexion",
"repeater_guestTools": "Outils pour les invités",
"chat_sendMessage": "Envoyer un message",
"room_guest": "Informations sur le serveur",
"repeater_guest": "Informations sur les répéteurs"
} }
+14 -2
View File
@@ -2081,7 +2081,7 @@
} }
}, },
"scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot", "scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot",
"scanner_linuxPairingPinPrompt": "Adja meg a PIN kódot a {deviceName} számára (hagyja üresen, ha nincs).", "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).",
"scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot", "scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot",
"scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN",
"@translation_translateTo": { "@translation_translateTo": {
@@ -2099,5 +2099,17 @@
"translation_translationOptions": "Fordítási lehetőségek", "translation_translationOptions": "Fordítási lehetőségek",
"translation_systemLanguage": "Rendszer nyelvé", "translation_systemLanguage": "Rendszer nyelvé",
"repeater_cliQuickClockSync": "Óra szinkronizálás", "repeater_cliQuickClockSync": "Óra szinkronizálás",
"repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat" "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.",
"repeater_clockSyncAfterLogin": "Óra szinkronizálás bejelentkezés után",
"repeater_guestTools": "Vendégek számára elérhető eszközök",
"room_guest": "Szoba szerver információk",
"chat_sendMessage": "Üzenet küldése",
"repeater_guest": "Adatok a repeaterről"
} }
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth", "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth",
"scanner_linuxPairingHidePin": "Nascondi il PIN", "scanner_linuxPairingHidePin": "Nascondi il PIN",
"repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
"repeater_cliQuickDiscovery": "Scopri i Vicini" "repeater_cliQuickDiscovery": "Scopri i Vicini",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Invia automaticamente il comando \"sincronizzazione dell'orologio\" dopo un login riuscito.",
"repeater_clockSyncAfterLogin": "Sincronizzazione dell'orologio dopo il login",
"repeater_guest": "Informazioni sul ripetitore",
"repeater_guestTools": "Strumenti per gli ospiti",
"chat_sendMessage": "Invia messaggio",
"room_guest": "Informazioni sul server"
} }
+13 -1
View File
@@ -2099,5 +2099,17 @@
"scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN",
"scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。",
"repeater_cliQuickClockSync": "クロック同期", "repeater_cliQuickClockSync": "クロック同期",
"repeater_cliQuickDiscovery": "近隣を発見する" "repeater_cliQuickDiscovery": "近隣を発見する",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "ログイン後、時計の時刻を同期する",
"repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。",
"room_guest": "ルームサーバーに関する情報",
"chat_sendMessage": "メッセージを送信する",
"repeater_guest": "繰り返し送信に関する情報",
"repeater_guestTools": "ゲスト向けツール"
} }
+13 -1
View File
@@ -2099,5 +2099,17 @@
"translation_translationOptions": "번역 옵션", "translation_translationOptions": "번역 옵션",
"translation_systemLanguage": "시스템 언어", "translation_systemLanguage": "시스템 언어",
"repeater_cliQuickClockSync": "시계 동기화", "repeater_cliQuickClockSync": "시계 동기화",
"repeater_cliQuickDiscovery": "이웃 발견하기" "repeater_cliQuickDiscovery": "이웃 발견하기",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "로그인 후 시계 동기화",
"repeater_clockSyncAfterLoginSubtitle": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.",
"repeater_guestTools": "손님용 도구",
"chat_sendMessage": "메시지를 보내기",
"repeater_guest": "반복 장비 정보",
"room_guest": "서버 정보"
} }
+32 -2
View File
@@ -3438,13 +3438,13 @@ abstract class AppLocalizations {
/// No description provided for @login_repeaterDescription. /// No description provided for @login_repeaterDescription.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Enter the repeater password to access settings and status.'** /// **'Enter the repeater password for guest or admin access.'**
String get login_repeaterDescription; String get login_repeaterDescription;
/// No description provided for @login_roomDescription. /// No description provided for @login_roomDescription.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Enter the room password to access settings and status.'** /// **'Enter the room password for guest or admin access.'**
String get login_roomDescription; String get login_roomDescription;
/// No description provided for @login_routing. /// No description provided for @login_routing.
@@ -3609,12 +3609,30 @@ abstract class AppLocalizations {
/// **'Room Server Management'** /// **'Room Server Management'**
String get room_management; String get room_management;
/// No description provided for @repeater_guest.
///
/// In en, this message translates to:
/// **'Repeater Information'**
String get repeater_guest;
/// No description provided for @room_guest.
///
/// In en, this message translates to:
/// **'Room Server Information'**
String get room_guest;
/// No description provided for @repeater_managementTools. /// No description provided for @repeater_managementTools.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Management Tools'** /// **'Management Tools'**
String get repeater_managementTools; String get repeater_managementTools;
/// No description provided for @repeater_guestTools.
///
/// In en, this message translates to:
/// **'Guest Tools'**
String get repeater_guestTools;
/// No description provided for @repeater_status. /// No description provided for @repeater_status.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -3675,6 +3693,18 @@ abstract class AppLocalizations {
/// **'Configure repeater parameters'** /// **'Configure repeater parameters'**
String get repeater_settingsSubtitle; String get repeater_settingsSubtitle;
/// Repeater setting: auto sync device clock after successful login
///
/// In en, this message translates to:
/// **'Clock sync after login'**
String get repeater_clockSyncAfterLogin;
/// Repeater setting subtitle: describes the clock sync after login behavior
///
/// In en, this message translates to:
/// **'Automatically send \"clock sync\" after a successful login'**
String get repeater_clockSyncAfterLoginSubtitle;
/// No description provided for @repeater_statusTitle. /// No description provided for @repeater_statusTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+18 -1
View File
@@ -1240,7 +1240,7 @@ class AppLocalizationsBg extends AppLocalizations {
String get chat_noMessages => 'Няма съобщения.'; String get chat_noMessages => 'Няма съобщения.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Изпратете съобщение';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2019,9 +2019,18 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get room_management => 'Управление на сървъра за стая'; String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_guest => 'Информация за ретранслаторите';
@override
String get room_guest => 'Информация за сървъра на стаята';
@override @override
String get repeater_managementTools => 'Инструменти за управление'; String get repeater_managementTools => 'Инструменти за управление';
@override
String get repeater_guestTools => 'Инструменти за гости';
@override @override
String get repeater_status => 'Статус'; String get repeater_status => 'Статус';
@@ -2056,6 +2065,14 @@ class AppLocalizationsBg extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Конфигурирайте параметрите на репитера'; 'Конфигурирайте параметрите на репитера';
@override
String get repeater_clockSyncAfterLogin =>
'Синхронизиране на часовника след влизане';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.';
@override @override
String get repeater_statusTitle => 'Статус на повтарянето'; String get repeater_statusTitle => 'Статус на повтарянето';
+18 -1
View File
@@ -1239,7 +1239,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get chat_noMessages => 'Noch keine Nachrichten.'; String get chat_noMessages => 'Noch keine Nachrichten.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Nachricht senden';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2017,9 +2017,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get room_management => 'Raum-Server-Verwaltung'; String get room_management => 'Raum-Server-Verwaltung';
@override
String get repeater_guest => 'Informationen zu Repeatern';
@override
String get room_guest => 'Informationen zum Room Server';
@override @override
String get repeater_managementTools => 'Verwaltungs-Tools'; String get repeater_managementTools => 'Verwaltungs-Tools';
@override
String get repeater_guestTools => 'Gastwerkzeuge';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2052,6 +2061,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren'; String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren';
@override
String get repeater_clockSyncAfterLogin =>
'Uhrzeit-Synchronisation nach dem Anmelden';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.';
@override @override
String get repeater_statusTitle => 'Repeaterstatus'; String get repeater_statusTitle => 'Repeaterstatus';
+18 -2
View File
@@ -1871,11 +1871,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get login_repeaterDescription => String get login_repeaterDescription =>
'Enter the repeater password to access settings and status.'; 'Enter the repeater password for guest or admin access.';
@override @override
String get login_roomDescription => String get login_roomDescription =>
'Enter the room password to access settings and status.'; 'Enter the room password for guest or admin access.';
@override @override
String get login_routing => 'Routing'; String get login_routing => 'Routing';
@@ -1979,9 +1979,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get room_management => 'Room Server Management'; String get room_management => 'Room Server Management';
@override
String get repeater_guest => 'Repeater Information';
@override
String get room_guest => 'Room Server Information';
@override @override
String get repeater_managementTools => 'Management Tools'; String get repeater_managementTools => 'Management Tools';
@override
String get repeater_guestTools => 'Guest Tools';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2014,6 +2023,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configure repeater parameters'; String get repeater_settingsSubtitle => 'Configure repeater parameters';
@override
String get repeater_clockSyncAfterLogin => 'Clock sync after login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatically send \"clock sync\" after a successful login';
@override @override
String get repeater_statusTitle => 'Repeater Status'; String get repeater_statusTitle => 'Repeater Status';
+18 -1
View File
@@ -1239,7 +1239,7 @@ class AppLocalizationsEs extends AppLocalizations {
String get chat_noMessages => 'Aún no hay mensajes'; String get chat_noMessages => 'Aún no hay mensajes';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Enviar mensaje';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2015,9 +2015,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get room_management => 'Administración del Servidor de Habitación'; String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_guest => 'Información sobre repetidores';
@override
String get room_guest => 'Información del servidor';
@override @override
String get repeater_managementTools => 'Herramientas de Gestión'; String get repeater_managementTools => 'Herramientas de Gestión';
@override
String get repeater_guestTools => 'Herramientas para invitados';
@override @override
String get repeater_status => 'Estado'; String get repeater_status => 'Estado';
@@ -2050,6 +2059,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor'; String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronización del reloj después de iniciar sesión';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.';
@override @override
String get repeater_statusTitle => 'Estado del Repetidor'; String get repeater_statusTitle => 'Estado del Repetidor';
+18 -1
View File
@@ -1244,7 +1244,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get chat_noMessages => 'Aucun message pour le moment.'; String get chat_noMessages => 'Aucun message pour le moment.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Envoyer un message';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2026,9 +2026,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get room_management => 'Administrattion Room Server'; String get room_management => 'Administrattion Room Server';
@override
String get repeater_guest => 'Informations sur les répéteurs';
@override
String get room_guest => 'Informations sur le serveur';
@override @override
String get repeater_managementTools => 'Outils de Gestion'; String get repeater_managementTools => 'Outils de Gestion';
@override
String get repeater_guestTools => 'Outils pour les invités';
@override @override
String get repeater_status => 'État'; String get repeater_status => 'État';
@@ -2062,6 +2071,14 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Configurer les paramètres du répéteur'; 'Configurer les paramètres du répéteur';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronisation de l\'horloge après la connexion';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Envoyer automatiquement une notification \"synchronisation de l\'heure\" après une connexion réussie.';
@override @override
String get repeater_statusTitle => 'État du répéteur'; String get repeater_statusTitle => 'État du répéteur';
+19 -2
View File
@@ -1247,7 +1247,7 @@ class AppLocalizationsHu extends AppLocalizations {
String get chat_noMessages => 'Még nincs üzenet.'; String get chat_noMessages => 'Még nincs üzenet.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Üzenet küldése';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2030,9 +2030,18 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get room_management => 'Szoba-szerver kezelés'; String get room_management => 'Szoba-szerver kezelés';
@override
String get repeater_guest => 'Adatok a repeaterről';
@override
String get room_guest => 'Szoba szerver információk';
@override @override
String get repeater_managementTools => 'Menedzsmentes eszközök'; String get repeater_managementTools => 'Menedzsmentes eszközök';
@override
String get repeater_guestTools => 'Vendégek számára elérhető eszközök';
@override @override
String get repeater_status => 'Állapot'; String get repeater_status => 'Állapot';
@@ -2066,6 +2075,14 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket'; String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket';
@override
String get repeater_clockSyncAfterLogin =>
'Óra szinkronizálás bejelentkezés után';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.';
@override @override
String get repeater_statusTitle => 'Adatkapcsolódás állapot'; String get repeater_statusTitle => 'Adatkapcsolódás állapot';
@@ -3677,7 +3694,7 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String scanner_linuxPairingPinPrompt(String deviceName) { String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Adja meg a PIN kódot a $deviceName számára (hagyja üresen, ha nincs).'; return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).';
} }
@override @override
+18 -1
View File
@@ -1240,7 +1240,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get chat_noMessages => 'Nessun messaggio ancora'; String get chat_noMessages => 'Nessun messaggio ancora';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Invia messaggio';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2016,9 +2016,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get room_management => 'Gestione del Server di Camera'; String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_guest => 'Informazioni sul ripetitore';
@override
String get room_guest => 'Informazioni sul server';
@override @override
String get repeater_managementTools => 'Strumenti di Gestione'; String get repeater_managementTools => 'Strumenti di Gestione';
@override
String get repeater_guestTools => 'Strumenti per gli ospiti';
@override @override
String get repeater_status => 'Stato'; String get repeater_status => 'Stato';
@@ -2053,6 +2062,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Configura i parametri del ripetitore'; 'Configura i parametri del ripetitore';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronizzazione dell\'orologio dopo il login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.';
@override @override
String get repeater_statusTitle => 'Stato del Ripetitore'; String get repeater_statusTitle => 'Stato del Ripetitore';
+17 -1
View File
@@ -1180,7 +1180,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get chat_noMessages => 'まだメッセージは届いていません'; String get chat_noMessages => 'まだメッセージは届いていません';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'メッセージを送信する';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1932,9 +1932,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get room_management => 'ルームサーバーの管理'; String get room_management => 'ルームサーバーの管理';
@override
String get repeater_guest => '繰り返し送信に関する情報';
@override
String get room_guest => 'ルームサーバーに関する情報';
@override @override
String get repeater_managementTools => '管理ツール'; String get repeater_managementTools => '管理ツール';
@override
String get repeater_guestTools => 'ゲスト向けツール';
@override @override
String get repeater_status => 'ステータス'; String get repeater_status => 'ステータス';
@@ -1965,6 +1974,13 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する'; String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する';
@override
String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'ログインが成功した場合、自動的に「時刻同期」を送信する。';
@override @override
String get repeater_statusTitle => '再送ステータス'; String get repeater_statusTitle => '再送ステータス';
+17 -1
View File
@@ -1175,7 +1175,7 @@ class AppLocalizationsKo extends AppLocalizations {
String get chat_noMessages => '아직 메시지가 없습니다.'; String get chat_noMessages => '아직 메시지가 없습니다.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => '메시지를 보내기';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1929,9 +1929,18 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get room_management => '방 서버 관리'; String get room_management => '방 서버 관리';
@override
String get repeater_guest => '반복 장비 정보';
@override
String get room_guest => '서버 정보';
@override @override
String get repeater_managementTools => '관리 도구'; String get repeater_managementTools => '관리 도구';
@override
String get repeater_guestTools => '손님용 도구';
@override @override
String get repeater_status => '상태'; String get repeater_status => '상태';
@@ -1962,6 +1971,13 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => '리피터 파라미터 설정'; String get repeater_settingsSubtitle => '리피터 파라미터 설정';
@override
String get repeater_clockSyncAfterLogin => '로그인 후 시계 동기화';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.';
@override @override
String get repeater_statusTitle => '반복 장치 상태'; String get repeater_statusTitle => '반복 장치 상태';
+18 -1
View File
@@ -1228,7 +1228,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get chat_noMessages => 'Nog geen berichten.'; String get chat_noMessages => 'Nog geen berichten.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Verzend bericht';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2003,9 +2003,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get room_management => 'Beheer Server Kamer'; String get room_management => 'Beheer Server Kamer';
@override
String get repeater_guest => 'Informatie over herhalingsapparatuur';
@override
String get room_guest => 'Informatie over de server';
@override @override
String get repeater_managementTools => 'Beheerfuncties'; String get repeater_managementTools => 'Beheerfuncties';
@override
String get repeater_guestTools => 'Gastenfuncties';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2038,6 +2047,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configureer repeaterparameters'; String get repeater_settingsSubtitle => 'Configureer repeaterparameters';
@override
String get repeater_clockSyncAfterLogin =>
'Na het inloggen, klok synchroniseren';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.';
@override @override
String get repeater_statusTitle => 'Status repeater'; String get repeater_statusTitle => 'Status repeater';
+18 -1
View File
@@ -1248,7 +1248,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_noMessages => 'Brak jeszcze wiadomości'; String get chat_noMessages => 'Brak jeszcze wiadomości';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Wyślij wiadomość';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2031,9 +2031,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get room_management => 'Zarządzanie Serwerem Pokoju'; String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_guest => 'Informacje dotyczące urządzenia powtarzającego';
@override
String get room_guest => 'Informacje o serwerze';
@override @override
String get repeater_managementTools => 'Narzędzia Zarządzania'; String get repeater_managementTools => 'Narzędzia Zarządzania';
@override
String get repeater_guestTools => 'Narzędzia dla gości';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2066,6 +2075,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika'; String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronizacja zegara po zalogowaniu';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.';
@override @override
String get repeater_statusTitle => 'Status przekaźnika'; String get repeater_statusTitle => 'Status przekaźnika';
+18 -1
View File
@@ -1239,7 +1239,7 @@ class AppLocalizationsPt extends AppLocalizations {
String get chat_noMessages => 'Ainda não existem mensagens.'; String get chat_noMessages => 'Ainda não existem mensagens.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Enviar mensagem';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2015,9 +2015,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get room_management => 'Gerenciamento de Servidor de Sala'; String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_guest => 'Informações sobre repetidores';
@override
String get room_guest => 'Informações do Servidor';
@override @override
String get repeater_managementTools => 'Ferramentas de Gerenciamento'; String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@override
String get repeater_guestTools => 'Ferramentas para hóspedes';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2050,6 +2059,14 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor';
@override
String get repeater_clockSyncAfterLogin =>
'Sincronização do relógio após o login';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.';
@override @override
String get repeater_statusTitle => 'Status do Repetidor'; String get repeater_statusTitle => 'Status do Repetidor';
+18 -1
View File
@@ -1239,7 +1239,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get chat_noMessages => 'Сообщений пока нет'; String get chat_noMessages => 'Сообщений пока нет';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Отправить сообщение';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2019,9 +2019,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get room_management => 'Управление сервером комнат'; String get room_management => 'Управление сервером комнат';
@override
String get repeater_guest => 'Информация о ретрансляторе';
@override
String get room_guest => 'Информация о сервере';
@override @override
String get repeater_managementTools => 'Инструменты управления'; String get repeater_managementTools => 'Инструменты управления';
@override
String get repeater_guestTools => 'Инструменты для гостей';
@override @override
String get repeater_status => 'Статус'; String get repeater_status => 'Статус';
@@ -2054,6 +2063,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Настройка параметров репитера'; String get repeater_settingsSubtitle => 'Настройка параметров репитера';
@override
String get repeater_clockSyncAfterLogin =>
'Синхронизация часов после входа в систему';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.';
@override @override
String get repeater_statusTitle => 'Статус репитера'; String get repeater_statusTitle => 'Статус репитера';
+18 -1
View File
@@ -1227,7 +1227,7 @@ class AppLocalizationsSk extends AppLocalizations {
String get chat_noMessages => 'Zatiaľ žiadne správy.'; String get chat_noMessages => 'Zatiaľ žiadne správy.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Odoslať správu';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2004,9 +2004,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get room_management => 'Správa servera miestnosti'; String get room_management => 'Správa servera miestnosti';
@override
String get repeater_guest => 'Informácie o opakovači';
@override
String get room_guest => 'Informácie o serveri';
@override @override
String get repeater_managementTools => 'Nástroje na správu'; String get repeater_managementTools => 'Nástroje na správu';
@override
String get repeater_guestTools => 'Nástroje pre hostí';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2039,6 +2048,14 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača';
@override
String get repeater_clockSyncAfterLogin =>
'Synchronizácia hodiniek po prihlávení';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.';
@override @override
String get repeater_statusTitle => 'Status opakého zboru'; String get repeater_statusTitle => 'Status opakého zboru';
+17 -1
View File
@@ -1225,7 +1225,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get chat_noMessages => 'Še ni sporočil.'; String get chat_noMessages => 'Še ni sporočil.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Pošlji sporočilo';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2001,9 +2001,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get room_management => 'Upravljanje stremlišča'; String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_guest => 'Informacije o ponovljalniku';
@override
String get room_guest => 'Informacije o strežniku';
@override @override
String get repeater_managementTools => 'Upravne orodje'; String get repeater_managementTools => 'Upravne orodje';
@override
String get repeater_guestTools => 'Naložila za goste';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2038,6 +2047,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_settingsSubtitle => String get repeater_settingsSubtitle =>
'Konfigurirajte parametre ponovitelja'; 'Konfigurirajte parametre ponovitelja';
@override
String get repeater_clockSyncAfterLogin => 'Sinhronizacija ure po prijavi';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.';
@override @override
String get repeater_statusTitle => 'Status ponovitelja'; String get repeater_statusTitle => 'Status ponovitelja';
+18 -1
View File
@@ -1218,7 +1218,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get chat_noMessages => 'Inga meddelanden ännu'; String get chat_noMessages => 'Inga meddelanden ännu';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Skicka meddelande';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1990,9 +1990,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get room_management => 'Rumserverhantering'; String get room_management => 'Rumserverhantering';
@override
String get repeater_guest => 'Information om repetorer';
@override
String get room_guest => 'Information om servern';
@override @override
String get repeater_managementTools => 'Administrationsverktyg'; String get repeater_managementTools => 'Administrationsverktyg';
@override
String get repeater_guestTools => 'Gästverktyg';
@override @override
String get repeater_status => 'Status'; String get repeater_status => 'Status';
@@ -2025,6 +2034,14 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar'; String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar';
@override
String get repeater_clockSyncAfterLogin =>
'Synkronisera klockan efter inloggning';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.';
@override @override
String get repeater_statusTitle => 'Återspelsstatus'; String get repeater_statusTitle => 'Återspelsstatus';
+17 -1
View File
@@ -1231,7 +1231,7 @@ class AppLocalizationsUk extends AppLocalizations {
String get chat_noMessages => 'Поки немає повідомлень.'; String get chat_noMessages => 'Поки немає повідомлень.';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => 'Надіслати повідомлення';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -2014,9 +2014,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get room_management => 'Адміністрування сервера кімнати'; String get room_management => 'Адміністрування сервера кімнати';
@override
String get repeater_guest => 'Інформація про ретранслятор';
@override
String get room_guest => 'Інформація про сервер кімнати';
@override @override
String get repeater_managementTools => 'Інструменти керування'; String get repeater_managementTools => 'Інструменти керування';
@override
String get repeater_guestTools => 'Інструменти для гостей';
@override @override
String get repeater_status => 'Статус'; String get repeater_status => 'Статус';
@@ -2050,6 +2059,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора'; String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора';
@override
String get repeater_clockSyncAfterLogin => 'Синхронізація годин після входу';
@override
String get repeater_clockSyncAfterLoginSubtitle =>
'Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.';
@override @override
String get repeater_statusTitle => 'Статус ретранслятора'; String get repeater_statusTitle => 'Статус ретранслятора';
+16 -1
View File
@@ -1162,7 +1162,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get chat_noMessages => '暂无消息'; String get chat_noMessages => '暂无消息';
@override @override
String get chat_sendMessage => 'Send message'; String get chat_sendMessage => '发送消息';
@override @override
String chat_sendMessageTo(String contactName) { String chat_sendMessageTo(String contactName) {
@@ -1890,9 +1890,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get room_management => '房间服务器管理'; String get room_management => '房间服务器管理';
@override
String get repeater_guest => '重复器信息';
@override
String get room_guest => '服务器信息';
@override @override
String get repeater_managementTools => '管理工具'; String get repeater_managementTools => '管理工具';
@override
String get repeater_guestTools => '访客工具';
@override @override
String get repeater_status => '状态'; String get repeater_status => '状态';
@@ -1923,6 +1932,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get repeater_settingsSubtitle => '配置转发节点参数'; String get repeater_settingsSubtitle => '配置转发节点参数';
@override
String get repeater_clockSyncAfterLogin => '登录后,自动同步时钟';
@override
String get repeater_clockSyncAfterLoginSubtitle => '在成功登录后,自动发送“时钟同步”指令。';
@override @override
String get repeater_statusTitle => '转发节点状态'; String get repeater_statusTitle => '转发节点状态';
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).", "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
"scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN", "scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN",
"repeater_cliQuickDiscovery": "Ontdek Buren", "repeater_cliQuickDiscovery": "Ontdek Buren",
"repeater_cliQuickClockSync": "Kloksynchronisatie" "repeater_cliQuickClockSync": "Kloksynchronisatie",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.",
"repeater_clockSyncAfterLogin": "Na het inloggen, klok synchroniseren",
"repeater_guestTools": "Gastenfuncties",
"room_guest": "Informatie over de server",
"chat_sendMessage": "Verzend bericht",
"repeater_guest": "Informatie over herhalingsapparatuur"
} }
+13 -1
View File
@@ -2099,5 +2099,17 @@
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).", "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).",
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth", "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth",
"repeater_cliQuickClockSync": "Synchronizacja zegara", "repeater_cliQuickClockSync": "Synchronizacja zegara",
"repeater_cliQuickDiscovery": "Odkryj Sąsiadów" "repeater_cliQuickDiscovery": "Odkryj Sąsiadów",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Synchronizacja zegara po zalogowaniu",
"repeater_clockSyncAfterLoginSubtitle": "Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.",
"chat_sendMessage": "Wyślij wiadomość",
"repeater_guestTools": "Narzędzia dla gości",
"repeater_guest": "Informacje dotyczące urządzenia powtarzającego",
"room_guest": "Informacje o serwerze"
} }
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).", "scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth", "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
"repeater_cliQuickClockSync": "Sincronização do Relógio", "repeater_cliQuickClockSync": "Sincronização do Relógio",
"repeater_cliQuickDiscovery": "Descobrir Vizinhos" "repeater_cliQuickDiscovery": "Descobrir Vizinhos",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.",
"repeater_clockSyncAfterLogin": "Sincronização do relógio após o login",
"room_guest": "Informações do Servidor",
"chat_sendMessage": "Enviar mensagem",
"repeater_guest": "Informações sobre repetidores",
"repeater_guestTools": "Ferramentas para hóspedes"
} }
+13 -1
View File
@@ -1301,5 +1301,17 @@
"scanner_linuxPairingHidePin": "Скрыть PIN", "scanner_linuxPairingHidePin": "Скрыть PIN",
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth", "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
"repeater_cliQuickDiscovery": "Обнаружить Соседей", "repeater_cliQuickDiscovery": "Обнаружить Соседей",
"repeater_cliQuickClockSync": "Синхронизация часов" "repeater_cliQuickClockSync": "Синхронизация часов",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему",
"repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.",
"chat_sendMessage": "Отправить сообщение",
"repeater_guest": "Информация о ретрансляторе",
"room_guest": "Информация о сервере",
"repeater_guestTools": "Инструменты для гостей"
} }
+13 -1
View File
@@ -2061,5 +2061,17 @@
"translation_translationOptions": "Možnosti prekladania", "translation_translationOptions": "Možnosti prekladania",
"translation_systemLanguage": "Jazyk systému", "translation_systemLanguage": "Jazyk systému",
"repeater_cliQuickClockSync": "Synchronizácia hodin", "repeater_cliQuickClockSync": "Synchronizácia hodin",
"repeater_cliQuickDiscovery": "Objaviť susedov" "repeater_cliQuickDiscovery": "Objaviť susedov",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Synchronizácia hodiniek po prihlávení",
"repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.",
"chat_sendMessage": "Odoslať správu",
"repeater_guest": "Informácie o opakovači",
"room_guest": "Informácie o serveri",
"repeater_guestTools": "Nástroje pre hostí"
} }
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).", "scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje", "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
"repeater_cliQuickDiscovery": "Odkrijte sosede", "repeater_cliQuickDiscovery": "Odkrijte sosede",
"repeater_cliQuickClockSync": "Usklajevanje ure" "repeater_cliQuickClockSync": "Usklajevanje ure",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.",
"repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi",
"repeater_guest": "Informacije o ponovljalniku",
"chat_sendMessage": "Pošlji sporočilo",
"room_guest": "Informacije o strežniku",
"repeater_guestTools": "Naložila za goste"
} }
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).", "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
"scanner_linuxPairingHidePin": "Dölj PIN", "scanner_linuxPairingHidePin": "Dölj PIN",
"repeater_cliQuickDiscovery": "Upptäck grannar", "repeater_cliQuickDiscovery": "Upptäck grannar",
"repeater_cliQuickClockSync": "Synkronisera klocka" "repeater_cliQuickClockSync": "Synkronisera klocka",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.",
"repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning",
"repeater_guest": "Information om repetorer",
"chat_sendMessage": "Skicka meddelande",
"repeater_guestTools": "Gästverktyg",
"room_guest": "Information om servern"
} }
+13 -1
View File
@@ -2061,5 +2061,17 @@
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN", "scanner_linuxPairingHidePin": "Приховати PIN",
"repeater_cliQuickClockSync": "Синхронізація годинника", "repeater_cliQuickClockSync": "Синхронізація годинника",
"repeater_cliQuickDiscovery": "Відкрити сусідів" "repeater_cliQuickDiscovery": "Відкрити сусідів",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
"repeater_guestTools": "Інструменти для гостей",
"repeater_guest": "Інформація про ретранслятор",
"room_guest": "Інформація про сервер кімнати",
"chat_sendMessage": "Надіслати повідомлення"
} }
+13 -1
View File
@@ -2066,5 +2066,17 @@
"translation_translationOptions": "翻译选项", "translation_translationOptions": "翻译选项",
"translation_systemLanguage": "系统语言", "translation_systemLanguage": "系统语言",
"repeater_cliQuickDiscovery": "发现邻居", "repeater_cliQuickDiscovery": "发现邻居",
"repeater_cliQuickClockSync": "同步时钟" "repeater_cliQuickClockSync": "同步时钟",
"@repeater_clockSyncAfterLogin": {
"description": "Repeater setting: auto sync device clock after successful login"
},
"@repeater_clockSyncAfterLoginSubtitle": {
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "登录后,自动同步时钟",
"repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。",
"repeater_guestTools": "访客工具",
"repeater_guest": "重复器信息",
"chat_sendMessage": "发送消息",
"room_guest": "服务器信息"
} }
+4 -2
View File
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../services/app_debug_log_service.dart'; import '../services/app_debug_log_service.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
class AppDebugLogScreen extends StatelessWidget { class AppDebugLogScreen extends StatelessWidget {
const AppDebugLogScreen({super.key}); const AppDebugLogScreen({super.key});
@@ -34,8 +35,9 @@ class AppDebugLogScreen extends StatelessWidget {
.join('\n'); .join('\n');
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.debugLog_copied)), context,
content: Text(context.l10n.debugLog_copied),
); );
} }
: null, : null,
+68 -70
View File
@@ -10,6 +10,7 @@ import '../services/app_settings_service.dart';
import '../services/notification_service.dart'; import '../services/notification_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'map_cache_screen.dart'; import 'map_cache_screen.dart';
class AppSettingsScreen extends StatelessWidget { class AppSettingsScreen extends StatelessWidget {
@@ -151,13 +152,12 @@ class AppSettingsScreen extends StatelessWidget {
.requestPermissions(); .requestPermissions();
if (!granted) { if (!granted) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.appSettings_notificationPermissionDenied, context.l10n.appSettings_notificationPermissionDenied,
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
} }
return; return;
@@ -166,15 +166,14 @@ class AppSettingsScreen extends StatelessWidget {
await settingsService.setNotificationsEnabled(value); await settingsService.setNotificationsEnabled(value);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_notificationsEnabled ? context.l10n.appSettings_notificationsEnabled
: context.l10n.appSettings_notificationsDisabled, : context.l10n.appSettings_notificationsDisabled,
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
} }
}, },
@@ -301,15 +300,14 @@ class AppSettingsScreen extends StatelessWidget {
value: settingsService.settings.clearPathOnMaxRetry, value: settingsService.settings.clearPathOnMaxRetry,
onChanged: (value) { onChanged: (value) {
settingsService.setClearPathOnMaxRetry(value); settingsService.setClearPathOnMaxRetry(value);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_pathsWillBeCleared ? context.l10n.appSettings_pathsWillBeCleared
: context.l10n.appSettings_pathsWillNotBeCleared, : context.l10n.appSettings_pathsWillNotBeCleared,
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
}, },
), ),
@@ -329,15 +327,14 @@ class AppSettingsScreen extends StatelessWidget {
value: settingsService.settings.autoRouteRotationEnabled, value: settingsService.settings.autoRouteRotationEnabled,
onChanged: (value) { onChanged: (value) {
settingsService.setAutoRouteRotationEnabled(value); settingsService.setAutoRouteRotationEnabled(value);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_autoRouteRotationEnabled ? context.l10n.appSettings_autoRouteRotationEnabled
: context.l10n.appSettings_autoRouteRotationDisabled, : context.l10n.appSettings_autoRouteRotationDisabled,
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
}, },
), ),
@@ -1065,25 +1062,25 @@ class AppSettingsScreen extends StatelessWidget {
children: [ children: [
Text(context.l10n.appSettings_showNodesDiscoveredWithin), Text(context.l10n.appSettings_showNodesDiscoveredWithin),
const SizedBox(height: 16), const SizedBox(height: 16),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_allTime), title: Text(context.l10n.appSettings_allTime),
leading: Radio<double>(value: 0), value: 0,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_lastHour), title: Text(context.l10n.appSettings_lastHour),
leading: Radio<double>(value: 1), value: 1,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_last6Hours), title: Text(context.l10n.appSettings_last6Hours),
leading: Radio<double>(value: 6), value: 6,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_last24Hours), title: Text(context.l10n.appSettings_last24Hours),
leading: Radio<double>(value: 24), value: 24,
), ),
ListTile( RadioListTile<double>(
title: Text(context.l10n.appSettings_lastWeek), title: Text(context.l10n.appSettings_lastWeek),
leading: Radio<double>(value: 168), value: 168,
), ),
], ],
), ),
@@ -1117,13 +1114,13 @@ class AppSettingsScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( RadioListTile<UnitSystem>(
title: Text(context.l10n.appSettings_unitsMetric), title: Text(context.l10n.appSettings_unitsMetric),
leading: const Radio<UnitSystem>(value: UnitSystem.metric), value: UnitSystem.metric,
), ),
ListTile( RadioListTile<UnitSystem>(
title: Text(context.l10n.appSettings_unitsImperial), title: Text(context.l10n.appSettings_unitsImperial),
leading: const Radio<UnitSystem>(value: UnitSystem.imperial), value: UnitSystem.imperial,
), ),
], ],
), ),
@@ -1164,8 +1161,9 @@ class AppSettingsScreen extends StatelessWidget {
String? id, String? id,
}) async { }) async {
if (sourceUrl.isEmpty) { if (sourceUrl.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.translation_enterUrlFirst)), context,
content: Text(context.l10n.translation_enterUrlFirst),
); );
return; return;
} }
@@ -1176,22 +1174,23 @@ class AppSettingsScreen extends StatelessWidget {
id: id, id: id,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.translation_modelDownloaded)), context,
content: Text(context.l10n.translation_modelDownloaded),
); );
await settingsService.setTranslationEnabled(true); await settingsService.setTranslationEnabled(true);
} on TranslationDownloadCancelled { } on TranslationDownloadCancelled {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.translation_downloadStopped)), context,
content: Text(context.l10n.translation_downloadStopped),
); );
} catch (error) { } catch (error) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.translation_downloadFailed(error.toString()), context.l10n.translation_downloadFailed(error.toString()),
),
), ),
); );
} }
@@ -1236,16 +1235,16 @@ class AppSettingsScreen extends StatelessWidget {
try { try {
await translationService.removeModel(model); await translationService.removeModel(model);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
// TODO: l10n // TODO: l10n
content: Text('Deleted ${translationModelFriendlyName(model)}.'), content: Text('Deleted ${translationModelFriendlyName(model)}.'),
),
); );
} catch (error) { } catch (error) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text('Delete failed: $error')), context,
content: Text('Delete failed: $error'),
); // TODO: l10n ); // TODO: l10n
} }
} }
@@ -1279,15 +1278,14 @@ class AppSettingsScreen extends StatelessWidget {
onChanged: (value) async { onChanged: (value) async {
await settingsService.setAppDebugLogEnabled(value); await settingsService.setAppDebugLogEnabled(value);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
value value
? context.l10n.appSettings_appDebugLoggingEnabled ? context.l10n.appSettings_appDebugLoggingEnabled
: context.l10n.appSettings_appDebugLoggingDisabled, : context.l10n.appSettings_appDebugLoggingDisabled,
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
}, },
), ),
+4 -4
View File
@@ -5,6 +5,7 @@ import '../l10n/l10n.dart';
import '../services/ble_debug_log_service.dart'; import '../services/ble_debug_log_service.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
enum _BleLogView { frames, rawLogRx } enum _BleLogView { frames, rawLogRx }
@@ -52,10 +53,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
.join('\n'); .join('\n');
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.debugLog_bleCopied), content: Text(context.l10n.debugLog_bleCopied),
),
); );
} }
: null, : null,
+23 -24
View File
@@ -11,8 +11,10 @@ import '../connector/meshcore_connector.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart'; import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/gif_helper.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../helpers/utf8_length_limiter.dart'; import '../helpers/utf8_length_limiter.dart';
import '../helpers/snack_bar_builder.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
@@ -143,11 +145,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Future<void> _scrollToMessage(String messageId) async { Future<void> _scrollToMessage(String messageId) async {
final key = _messageKeys[messageId]; final key = _messageKeys[messageId];
if (key == null) { if (key == null) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_originalMessageNotFound), content: Text(context.l10n.chat_originalMessageNotFound),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -355,7 +356,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final settingsService = context.watch<AppSettingsService>(); final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing; final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing; final isOutgoing = message.isOutgoing;
final gifId = _parseGifId(message.text); final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(message.text); final poi = _parsePoiMessage(message.text);
final translatedDisplayText = final translatedDisplayText =
message.translatedText != null && message.translatedText != null &&
@@ -699,7 +700,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
final gifId = _parseGifId(replyText); final gifId = GifHelper.parseGif(replyText);
final poi = _parsePoiMessage(replyText); final poi = _parsePoiMessage(replyText);
Widget contentPreview; Widget contentPreview;
@@ -811,12 +812,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
_PoiInfo? _parsePoiMessage(String text) { _PoiInfo? _parsePoiMessage(String text) {
final trimmed = text.trim(); final trimmed = text.trim();
final match = RegExp( final match = RegExp(
@@ -897,7 +892,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
isScrollControlled: true, isScrollControlled: true,
builder: (context) => GifPicker( builder: (context) => GifPicker(
onGifSelected: (gifId) { onGifSelected: (gifId) {
_textController.text = 'g:$gifId'; _textController.text = GifHelper.encodeGif(gifId);
}, },
), ),
); );
@@ -1053,7 +1048,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
child: ValueListenableBuilder<TextEditingValue>( child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController, valueListenable: _textController,
builder: (context, value, child) { builder: (context, value, child) {
final gifId = _parseGifId(value.text); final gifId = GifHelper.parseGif(value.text);
if (gifId != null) { if (gifId != null) {
return Focus( return Focus(
autofocus: true, autofocus: true,
@@ -1156,9 +1151,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final now = DateTime.now(); final now = DateTime.now();
if (_lastChannelSendAt != null && if (_lastChannelSendAt != null &&
now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) { now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); content: Text(context.l10n.chat_sendCooldown),
);
return; return;
} }
_lastChannelSendAt = now; _lastChannelSendAt = now;
@@ -1200,8 +1196,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final maxBytes = maxChannelMessageBytes(connector.selfName); final maxBytes = maxChannelMessageBytes(connector.selfName);
if (utf8.encode(messageText).length > maxBytes) { if (utf8.encode(messageText).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
); );
return; return;
} }
@@ -1322,23 +1319,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
message.senderName, message.senderName,
message.text, message.text,
); );
final reactionText = 'r:$hash:$emojiIndex'; final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
connector.sendChannelMessage(widget.channel, reactionText); connector.sendChannelMessage(widget.channel, reactionText);
} }
void _copyMessageText(String text) { void _copyMessageText(String text) {
Clipboard.setData(ClipboardData(text: text)); Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); content: Text(context.l10n.chat_messageCopied),
);
} }
Future<void> _deleteMessage(ChannelMessage message) async { Future<void> _deleteMessage(ChannelMessage message) async {
await context.read<MeshCoreConnector>().deleteChannelMessage(message); await context.read<MeshCoreConnector>().deleteChannelMessage(message);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); content: Text(context.l10n.chat_messageDeleted),
);
} }
String _formatPathPrefixes(Uint8List pathBytes) { String _formatPathPrefixes(Uint8List pathBytes) {
+78 -109
View File
@@ -24,6 +24,7 @@ import '../widgets/empty_state.dart';
import '../widgets/qr_code_display.dart'; import '../widgets/qr_code_display.dart';
import '../widgets/quick_switch_bar.dart'; import '../widgets/quick_switch_bar.dart';
import '../widgets/unread_badge.dart'; import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
import 'channel_chat_screen.dart'; import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart'; import 'community_qr_scanner_screen.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
@@ -809,15 +810,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar( content: Text(
SnackBar( dialogContext
content: Text( .l10n
dialogContext .channels_enterChannelName,
.l10n
.channels_enterChannelName,
),
), ),
); );
return; return;
@@ -837,13 +835,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
nextIndex, nextIndex,
); );
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelAdded( context.l10n.channels_channelAdded(name),
name,
),
),
), ),
); );
} }
@@ -897,15 +892,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
final name = nameController.text.trim(); final name = nameController.text.trim();
final pskHex = pskController.text.trim(); final pskHex = pskController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar( content: Text(
SnackBar( dialogContext
content: Text( .l10n
dialogContext .channels_enterChannelName,
.l10n
.channels_enterChannelName,
),
), ),
); );
return; return;
@@ -914,15 +906,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try { try {
psk = Channel.parsePskHex(pskHex); psk = Channel.parsePskHex(pskHex);
} on FormatException { } on FormatException {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar( content: Text(
SnackBar( dialogContext
content: Text( .l10n
dialogContext .channels_pskMustBe32Hex,
.l10n
.channels_pskMustBe32Hex,
),
), ),
); );
return; return;
@@ -930,13 +919,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.setChannel(nextIndex, name, psk); connector.setChannel(nextIndex, name, psk);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelAdded( context.l10n.channels_channelAdded(name),
name,
),
),
), ),
); );
} }
@@ -967,11 +953,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.setChannel(nextIndex, 'Public', psk); connector.setChannel(nextIndex, 'Public', psk);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_publicChannelAdded, context.l10n.channels_publicChannelAdded,
),
), ),
); );
} }
@@ -1097,15 +1082,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async { onPressed: () async {
var hashtag = hashtagController.text.trim(); var hashtag = hashtagController.text.trim();
if (hashtag.isEmpty) { if (hashtag.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar( content: Text(
SnackBar( dialogContext
content: Text( .l10n
dialogContext .channels_enterChannelName,
.l10n
.channels_enterChannelName,
),
), ),
); );
return; return;
@@ -1125,15 +1107,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
} else { } else {
// Community hashtag - HMAC derivation from community secret // Community hashtag - HMAC derivation from community secret
if (selectedCommunity == null) { if (selectedCommunity == null) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, dialogContext,
).showSnackBar( content: Text(
SnackBar( dialogContext
content: Text( .l10n
dialogContext .community_selectCommunity,
.l10n
.community_selectCommunity,
),
), ),
); );
return; return;
@@ -1159,12 +1138,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
psk, psk,
); );
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelAdded( context.l10n.channels_channelAdded(
channelName, channelName,
),
), ),
), ),
); );
@@ -1259,13 +1237,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of( showDismissibleSnackBar(
dialogContext, context,
).showSnackBar( content: Text(
SnackBar( dialogContext.l10n.community_enterName,
content: Text(
dialogContext.l10n.community_enterName,
),
), ),
); );
return; return;
@@ -1301,11 +1276,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
_loadCommunities(); _loadCommunities();
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.community_created(name), context.l10n.community_created(name),
),
), ),
); );
@@ -1494,10 +1468,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
try { try {
psk = Channel.parsePskHex(pskHex); psk = Channel.parsePskHex(pskHex);
} on FormatException { } on FormatException {
ScaffoldMessenger.of(dialogContext).showSnackBar( showDismissibleSnackBar(
SnackBar( dialogContext,
content: Text(dialogContext.l10n.channels_pskMustBe32Hex), content: Text(dialogContext.l10n.channels_pskMustBe32Hex),
),
); );
return; return;
} }
@@ -1510,16 +1483,16 @@ class _ChannelsScreenState extends State<ChannelsScreen>
smazEnabled, smazEnabled,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.channels_channelUpdated(name)), content: Text(context.l10n.channels_channelUpdated(name)),
),
); );
} catch (e, st) { } catch (e, st) {
debugPrint(st.toString()); debugPrint(st.toString());
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text('Failed to update channel: $e')), context,
content: Text('Failed to update channel: $e'),
); );
} }
}, },
@@ -1559,21 +1532,19 @@ class _ChannelsScreenState extends State<ChannelsScreen>
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelDeleted(channel.name), context.l10n.channels_channelDeleted(channel.name),
),
), ),
); );
} catch (e, st) { } catch (e, st) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.channels_channelDeleteFailed(channel.name), context.l10n.channels_channelDeleteFailed(channel.name),
),
), ),
); );
@@ -1594,8 +1565,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _addPublicChannel(BuildContext context, MeshCoreConnector connector) { void _addPublicChannel(BuildContext context, MeshCoreConnector connector) {
final psk = Channel.parsePskHex(Channel.publicChannelPsk); final psk = Channel.parsePskHex(Channel.publicChannelPsk);
connector.setChannel(0, 'Public', psk); connector.setChannel(0, 'Public', psk);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.channels_publicChannelAdded)), context,
content: Text(context.l10n.channels_publicChannelAdded),
); );
} }
@@ -1810,12 +1782,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
_loadCommunities(); _loadCommunities();
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(context.l10n.community_deleted(community.name)),
context.l10n.community_deleted(community.name),
),
),
); );
} }
}, },
+42 -54
View File
@@ -16,6 +16,7 @@ import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart'; import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart'; import '../helpers/chat_scroll_controller.dart';
import '../helpers/gif_helper.dart';
import '../helpers/path_helper.dart'; import '../helpers/path_helper.dart';
import '../helpers/utf8_length_limiter.dart'; import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
@@ -42,6 +43,7 @@ import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart'; import '../widgets/translated_message_content.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../helpers/snack_bar_builder.dart';
import 'telemetry_screen.dart'; import 'telemetry_screen.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
@@ -523,7 +525,7 @@ class _ChatScreenState extends State<ChatScreen> {
child: ValueListenableBuilder<TextEditingValue>( child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController, valueListenable: _textController,
builder: (context, value, child) { builder: (context, value, child) {
final gifId = _parseGifId(value.text); final gifId = GifHelper.parseGif(value.text);
if (gifId != null) { if (gifId != null) {
return Focus( return Focus(
autofocus: true, autofocus: true,
@@ -601,19 +603,13 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
void _showGifPicker(BuildContext context) { void _showGifPicker(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => GifPicker( builder: (context) => GifPicker(
onGifSelected: (gifId) { onGifSelected: (gifId) {
_textController.text = 'g:$gifId'; _textController.text = GifHelper.encodeGif(gifId);
}, },
), ),
); );
@@ -638,9 +634,10 @@ class _ChatScreenState extends State<ChatScreen> {
final now = DateTime.now(); final now = DateTime.now();
if (_lastTextSendAt != null && if (_lastTextSendAt != null &&
now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) { now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); content: Text(context.l10n.chat_sendCooldown),
);
return; return;
} }
_lastTextSendAt = now; _lastTextSendAt = now;
@@ -676,8 +673,9 @@ class _ChatScreenState extends State<ChatScreen> {
} }
final maxBytes = maxContactMessageBytes(); final maxBytes = maxContactMessageBytes();
if (utf8.encode(outgoingText).length > maxBytes) { if (utf8.encode(outgoingText).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
); );
return; return;
} }
@@ -865,15 +863,12 @@ class _ChatScreenState extends State<ChatScreen> {
_showFullPathDialog(context, path.pathBytes), _showFullPathDialog(context, path.pathBytes),
onTap: () async { onTap: () async {
if (path.pathBytes.isEmpty) { if (path.pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context context.l10n.chat_pathDetailsNotAvailable,
.l10n
.chat_pathDetailsNotAvailable,
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
return; return;
} }
@@ -957,11 +952,10 @@ class _ChatScreenState extends State<ChatScreen> {
_resolveContact(connector), _resolveContact(connector),
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_pathCleared), content: Text(context.l10n.chat_pathCleared),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
@@ -987,11 +981,10 @@ class _ChatScreenState extends State<ChatScreen> {
pathLen: -1, pathLen: -1,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_floodModeEnabled), content: Text(context.l10n.chat_floodModeEnabled),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
@@ -1025,11 +1018,10 @@ class _ChatScreenState extends State<ChatScreen> {
void _showFullPathDialog(BuildContext context, List<int> pathBytes) { void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
if (pathBytes.isEmpty) { if (pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_pathDetailsNotAvailable), content: Text(context.l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -1142,11 +1134,10 @@ class _ChatScreenState extends State<ChatScreen> {
: (verified : (verified
? context.l10n.chat_pathDeviceConfirmed ? context.l10n.chat_pathDeviceConfirmed
: context.l10n.chat_pathDeviceNotConfirmed); : context.l10n.chat_pathDeviceNotConfirmed);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.chat_pathSetHops(hopCount, status)), content: Text(context.l10n.chat_pathSetHops(hopCount, status)),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
),
); );
} }
@@ -1495,26 +1486,29 @@ class _ChatScreenState extends State<ChatScreen> {
void _copyMessageText(String text) { void _copyMessageText(String text) {
Clipboard.setData(ClipboardData(text: text)); Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); content: Text(context.l10n.chat_messageCopied),
);
} }
Future<void> _deleteMessage(Message message) async { Future<void> _deleteMessage(Message message) async {
await context.read<MeshCoreConnector>().deleteMessage(message); await context.read<MeshCoreConnector>().deleteMessage(message);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); content: Text(context.l10n.chat_messageDeleted),
);
} }
void _retryMessage(Message message) { void _retryMessage(Message message) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Retry using the contact's current path override setting // Retry using the contact's current path override setting
connector.sendMessage(_resolveContact(connector), message.text); connector.sendMessage(_resolveContact(connector), message.text);
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage))); content: Text(context.l10n.chat_retryingMessage),
);
} }
void _showEmojiPicker(Message message, Contact senderContact) { void _showEmojiPicker(Message message, Contact senderContact) {
@@ -1546,7 +1540,7 @@ class _ChatScreenState extends State<ChatScreen> {
senderName, senderName,
message.text, message.text,
); );
final reactionText = 'r:$hash:$emojiIndex'; final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
connector.sendMessage(_resolveContact(connector), reactionText); connector.sendMessage(_resolveContact(connector), reactionText);
} }
} }
@@ -1576,7 +1570,7 @@ class _MessageBubble extends StatelessWidget {
final enableTracing = settingsService.settings.enableMessageTracing; final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing; final isOutgoing = message.isOutgoing;
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final gifId = _parseGifId(message.text); final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(message.text); final poi = _parsePoiMessage(message.text);
final isFailed = message.status == MessageStatus.failed; final isFailed = message.status == MessageStatus.failed;
final bubbleColor = isFailed final bubbleColor = isFailed
@@ -1850,12 +1844,6 @@ class _MessageBubble extends StatelessWidget {
); );
} }
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
_PoiInfo? _parsePoiMessage(String text) { _PoiInfo? _parsePoiMessage(String text) {
final trimmed = text.trim(); final trimmed = text.trim();
final match = RegExp( final match = RegExp(
+14 -16
View File
@@ -8,6 +8,7 @@ import '../models/community.dart';
import '../storage/community_store.dart'; import '../storage/community_store.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/qr_scanner_widget.dart'; import '../widgets/qr_scanner_widget.dart';
import '../helpers/snack_bar_builder.dart';
/// Screen for scanning community QR codes to join communities. /// Screen for scanning community QR codes to join communities.
/// ///
@@ -76,11 +77,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
} }
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.community_invalidQrCode), content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} finally { } finally {
@@ -93,12 +93,11 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
} }
void _showInvalidQrError(BuildContext context) { void _showInvalidQrError(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.community_invalidQrCode), content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
} }
@@ -229,11 +228,10 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
} }
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.community_joined(community.name)), content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
// Return to previous screen // Return to previous screen
+62 -60
View File
@@ -27,6 +27,7 @@ import '../widgets/quick_switch_bar.dart';
import '../widgets/repeater_login_dialog.dart'; import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart'; import '../widgets/room_login_dialog.dart';
import '../widgets/unread_badge.dart'; import '../widgets/unread_badge.dart';
import '../helpers/snack_bar_builder.dart';
import 'channels_screen.dart'; import 'channels_screen.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
import 'discovery_screen.dart'; import 'discovery_screen.dart';
@@ -150,9 +151,10 @@ class _ContactsScreenState extends State<ContactsScreen>
} }
void _showGroupsUnavailableMessage(BuildContext context) { void _showGroupsUnavailableMessage(BuildContext context) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(context.l10n.common_loading))); content: Text(context.l10n.common_loading),
);
} }
void _setupFrameListener() { void _setupFrameListener() {
@@ -169,10 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
// Validate packet has expected minimum size (98+ bytes per protocol) // Validate packet has expected minimum size (98+ bytes per protocol)
if (advertPacket.length < 98) { if (advertPacket.length < 98) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_invalidAdvertFormat), content: Text(context.l10n.contacts_invalidAdvertFormat),
),
); );
} }
_pendingOperations.remove(ContactOperationType.export); _pendingOperations.remove(ContactOperationType.export);
@@ -187,24 +188,23 @@ class _ContactsScreenState extends State<ContactsScreen>
if (!mounted) return; if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) { if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImported)), context,
content: Text(context.l10n.contacts_contactImported),
); );
} }
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_zeroHopContactAdvertSent), content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
),
); );
} }
if (_pendingOperations.contains(ContactOperationType.export)) { if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_contactAdvertCopied), content: Text(context.l10n.contacts_contactAdvertCopied),
),
); );
} }
@@ -216,25 +216,22 @@ class _ContactsScreenState extends State<ContactsScreen>
if (!mounted) return; if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) { if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_contactImportFailed), content: Text(context.l10n.contacts_contactImportFailed),
),
); );
} }
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
),
); );
} }
if (_pendingOperations.contains(ContactOperationType.export)) { if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_contactAdvertCopyFailed), content: Text(context.l10n.contacts_contactAdvertCopyFailed),
),
); );
} }
@@ -271,8 +268,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final clipboardData = await Clipboard.getData('text/plain'); final clipboardData = await Clipboard.getData('text/plain');
if (clipboardData == null || clipboardData.text == null) { if (clipboardData == null || clipboardData.text == null) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), context,
content: Text(context.l10n.contacts_clipboardEmpty),
); );
} }
return; return;
@@ -280,8 +278,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final text = clipboardData.text!.trim(); final text = clipboardData.text!.trim();
if (!text.startsWith('meshcore://')) { if (!text.startsWith('meshcore://')) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), context,
content: Text(context.l10n.contacts_invalidAdvertFormat),
); );
} }
return; return;
@@ -294,8 +293,9 @@ class _ContactsScreenState extends State<ContactsScreen>
connector.importContact(importContactFrame); connector.importContact(importContactFrame);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), context,
content: Text(context.l10n.contacts_invalidAdvertFormat),
); );
} }
} }
@@ -330,10 +330,9 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
onTap: () => { onTap: () => {
connector.sendSelfAdvert(flood: false), connector.sendSelfAdvert(flood: false),
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.settings_advertisementSent), content: Text(context.l10n.settings_advertisementSent),
),
), ),
}, },
), ),
@@ -347,10 +346,9 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
onTap: () => { onTap: () => {
connector.sendSelfAdvert(flood: true), connector.sendSelfAdvert(flood: true),
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.settings_advertisementSent), content: Text(context.l10n.settings_advertisementSent),
),
), ),
}, },
), ),
@@ -963,13 +961,16 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context, context: context,
builder: (context) => RepeaterLoginDialog( builder: (context) => RepeaterLoginDialog(
repeater: repeater, repeater: repeater,
onLogin: (password) { onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login // Navigate to repeater hub screen after successful login
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => RepeaterHubScreen(
RepeaterHubScreen(repeater: repeater, password: password), repeater: repeater,
password: password,
isAdmin: isAdmin,
),
), ),
); );
}, },
@@ -986,14 +987,18 @@ class _ContactsScreenState extends State<ContactsScreen>
context: context, context: context,
builder: (context) => RoomLoginDialog( builder: (context) => RoomLoginDialog(
room: room, room: room,
onLogin: (password) { onLogin: (password, isAdmin) {
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex); context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
destination == RoomLoginDestination.management destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password) ? RepeaterHubScreen(
repeater: room,
password: password,
isAdmin: isAdmin,
)
: ChatScreen(contact: room), : ChatScreen(contact: room),
), ),
); );
@@ -1146,19 +1151,17 @@ class _ContactsScreenState extends State<ContactsScreen>
onPressed: () async { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_groupNameRequired), content: Text(context.l10n.contacts_groupNameRequired),
),
); );
return; return;
} }
if (name.toLowerCase() == if (name.toLowerCase() ==
contactsAllGroupsValue.toLowerCase()) { contactsAllGroupsValue.toLowerCase()) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.contacts_groupNameReserved), content: Text(context.l10n.contacts_groupNameReserved),
),
); );
return; return;
} }
@@ -1167,11 +1170,10 @@ class _ContactsScreenState extends State<ContactsScreen>
return g.name.toLowerCase() == name.toLowerCase(); return g.name.toLowerCase() == name.toLowerCase();
}); });
if (exists) { if (exists) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.contacts_groupAlreadyExists(name), context.l10n.contacts_groupAlreadyExists(name),
),
), ),
); );
return; return;
+4 -2
View File
@@ -12,6 +12,7 @@ import '../utils/contact_search.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart'; import '../widgets/list_filter_widget.dart';
import '../helpers/snack_bar_builder.dart';
enum DiscoverySortOption { lastSeen, name, type } enum DiscoverySortOption { lastSeen, name, type }
@@ -234,8 +235,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
final hexString = pubKeyToHex(contact.rawPacket!); final hexString = pubKeyToHex(contact.rawPacket!);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), context,
content: Text(context.l10n.contacts_contactAdvertCopied),
); );
break; break;
case 'delete_contact': case 'delete_contact':
+11 -9
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart'; import '../services/map_tile_cache_service.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
class MapCacheScreen extends StatefulWidget { class MapCacheScreen extends StatefulWidget {
const MapCacheScreen({super.key}); const MapCacheScreen({super.key});
@@ -112,15 +113,17 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
Future<void> _startDownload() async { Future<void> _startDownload() async {
final bounds = _selectedBounds; final bounds = _selectedBounds;
if (bounds == null) { if (bounds == null) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.mapCache_selectAreaFirst)), context,
content: Text(context.l10n.mapCache_selectAreaFirst),
); );
return; return;
} }
if (_estimatedTiles == 0) { if (_estimatedTiles == 0) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.mapCache_noTilesToDownload)), context,
content: Text(context.l10n.mapCache_noTilesToDownload),
); );
return; return;
} }
@@ -182,9 +185,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
result.failed, result.failed,
) )
: context.l10n.mapCache_cachedTiles(result.downloaded); : context.l10n.mapCache_cachedTiles(result.downloaded);
ScaffoldMessenger.of( showDismissibleSnackBar(context, content: Text(message));
context,
).showSnackBar(SnackBar(content: Text(message)));
} }
Future<void> _clearCache() async { Future<void> _clearCache() async {
@@ -210,8 +211,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
final cacheService = context.read<MapTileCacheService>(); final cacheService = context.read<MapTileCacheService>();
await cacheService.clearCache(); await cacheService.clearCache();
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.mapCache_offlineCacheCleared)), context,
content: Text(context.l10n.mapCache_offlineCacheCleared),
); );
} }
+19 -9
View File
@@ -29,6 +29,7 @@ import 'chat_screen.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import '../widgets/repeater_login_dialog.dart'; import '../widgets/repeater_login_dialog.dart';
import '../widgets/room_login_dialog.dart'; import '../widgets/room_login_dialog.dart';
import '../helpers/snack_bar_builder.dart';
import 'repeater_hub_screen.dart'; import 'repeater_hub_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import 'line_of_sight_map_screen.dart'; import 'line_of_sight_map_screen.dart';
@@ -1366,13 +1367,16 @@ class _MapScreenState extends State<MapScreen> {
context: context, context: context,
builder: (context) => RepeaterLoginDialog( builder: (context) => RepeaterLoginDialog(
repeater: repeater, repeater: repeater,
onLogin: (password) { onLogin: (password, isAdmin) {
// Navigate to repeater hub screen after successful login // Navigate to repeater hub screen after successful login
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => RepeaterHubScreen(
RepeaterHubScreen(repeater: repeater, password: password), repeater: repeater,
password: password,
isAdmin: isAdmin,
),
), ),
); );
}, },
@@ -1385,7 +1389,8 @@ class _MapScreenState extends State<MapScreen> {
context: context, context: context,
builder: (context) => RoomLoginDialog( builder: (context) => RoomLoginDialog(
room: room, room: room,
onLogin: (password) { // onLogin(password, isAdmin) isAdmin not used for room caht screen
onLogin: (password, _) {
// Navigate to chat screen after successful login // Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex); context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push( Navigator.push(
@@ -1659,7 +1664,10 @@ class _MapScreenState extends State<MapScreen> {
); );
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!mounted) return; if (!mounted) return;
messenger.showSnackBar(SnackBar(content: Text(successMsg))); showDismissibleSnackBar(
messenger.context,
content: Text(successMsg),
);
}, },
), ),
ListTile( ListTile(
@@ -1681,8 +1689,9 @@ class _MapScreenState extends State<MapScreen> {
required String flags, required String flags,
}) async { }) async {
if (!connector.isConnected) { if (!connector.isConnected) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(context.l10n.map_connectToShareMarkers)), context,
content: Text(context.l10n.map_connectToShareMarkers),
); );
return; return;
} }
@@ -2271,8 +2280,9 @@ class _MapScreenState extends State<MapScreen> {
_points.clear(); _points.clear();
_polylines.clear(); _polylines.clear();
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.map_pathTraceCancelled)), context,
content: Text(l10n.map_pathTraceCancelled),
); );
}, },
tooltip: l10n.common_cancel, tooltip: l10n.common_cancel,
+13 -15
View File
@@ -11,6 +11,7 @@ import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart'; import '../widgets/snr_indicator.dart';
import '../helpers/snack_bar_builder.dart';
class NeighborsScreen extends StatefulWidget { class NeighborsScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -163,11 +164,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_neighborCount = neighborCount; _neighborCount = neighborCount;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.neighbors_receivedData), content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
_statusTimeout?.cancel(); _statusTimeout?.cancel();
if (!mounted) return; if (!mounted) return;
@@ -224,11 +224,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_isLoading = false; _isLoading = false;
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.neighbors_requestTimedOut), content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
_recordStatusResult(false); _recordStatusResult(false);
}); });
@@ -239,11 +238,10 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.neighbors_errorLoading(e.toString())), content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+4 -2
View File
@@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart';
import '../widgets/debug_frame_viewer.dart'; import '../widgets/debug_frame_viewer.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterCliScreen extends StatefulWidget { class RepeaterCliScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -336,8 +337,9 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
if (_commandController.text.trim().isNotEmpty) { if (_commandController.text.trim().isNotEmpty) {
_sendCommand(showDebug: true); _sendCommand(showDebug: true);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.repeater_enterCommandFirst)), context,
content: Text(l10n.repeater_enterCommandFirst),
); );
} }
}, },
+105 -91
View File
@@ -13,11 +13,13 @@ import 'neighbors_screen.dart';
class RepeaterHubScreen extends StatelessWidget { class RepeaterHubScreen extends StatelessWidget {
final Contact repeater; final Contact repeater;
final String password; final String password;
final bool isAdmin;
const RepeaterHubScreen({ const RepeaterHubScreen({
super.key, super.key,
required this.repeater, required this.repeater,
required this.password, required this.password,
required this.isAdmin,
}); });
@override @override
@@ -33,11 +35,18 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( if (isAdmin)
repeater.type == advTypeRepeater Text(
? l10n.repeater_management repeater.type == advTypeRepeater
: l10n.room_management, ? l10n.repeater_management
), : l10n.room_management,
),
if (!isAdmin)
Text(
repeater.type == advTypeRepeater
? l10n.repeater_guest
: l10n.room_guest,
),
Text( Text(
repeater.name, repeater.name,
style: const TextStyle( style: const TextStyle(
@@ -113,64 +122,67 @@ class RepeaterHubScreen extends StatelessWidget {
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Card( if (isAdmin)
child: Padding( Card(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), child: Padding(
child: Column( padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
children: [ Row(
const Icon(Icons.battery_full), children: [
const SizedBox(width: 10), const Icon(Icons.battery_full),
Expanded( const SizedBox(width: 10),
child: Text( Expanded(
l10n.appSettings_batteryChemistry, child: Text(
style: const TextStyle( l10n.appSettings_batteryChemistry,
fontSize: 16, style: const TextStyle(
fontWeight: FontWeight.w600, fontSize: 16,
fontWeight: FontWeight.w600,
),
), ),
), ),
), ],
],
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: chemistry,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
), ),
onChanged: (value) { const SizedBox(height: 12),
if (value == null) return; DropdownButtonFormField<String>(
settingsService.setBatteryChemistryForRepeater( initialValue: chemistry,
repeater.publicKeyHex, isExpanded: true,
value, decoration: const InputDecoration(
); border: UnderlineInputBorder(),
}, isDense: true,
items: [
DropdownMenuItem(
value: 'nmc',
child: Text(l10n.appSettings_batteryNmc),
), ),
DropdownMenuItem( onChanged: (value) {
value: 'lifepo4', if (value == null) return;
child: Text(l10n.appSettings_batteryLifepo4), settingsService.setBatteryChemistryForRepeater(
), repeater.publicKeyHex,
DropdownMenuItem( value,
value: 'lipo', );
child: Text(l10n.appSettings_batteryLipo), },
), items: [
], DropdownMenuItem(
), value: 'nmc',
], child: Text(l10n.appSettings_batteryNmc),
),
DropdownMenuItem(
value: 'lifepo4',
child: Text(l10n.appSettings_batteryLifepo4),
),
DropdownMenuItem(
value: 'lipo',
child: Text(l10n.appSettings_batteryLipo),
),
],
),
],
),
), ),
), ),
),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
l10n.repeater_managementTools, isAdmin
? l10n.repeater_managementTools
: l10n.repeater_guestTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -210,26 +222,27 @@ class RepeaterHubScreen extends StatelessWidget {
); );
}, },
), ),
const SizedBox(height: 12), if (isAdmin) const SizedBox(height: 12),
// CLI button // CLI button
_buildManagementCard( if (isAdmin)
context, _buildManagementCard(
icon: Icons.terminal, context,
title: l10n.repeater_cli, icon: Icons.terminal,
subtitle: l10n.repeater_cliSubtitle, title: l10n.repeater_cli,
color: Colors.green, subtitle: l10n.repeater_cliSubtitle,
onTap: () { color: Colors.green,
Navigator.push( onTap: () {
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => RepeaterCliScreen( MaterialPageRoute(
repeater: repeater, builder: (context) => RepeaterCliScreen(
password: password, repeater: repeater,
password: password,
),
), ),
), );
); },
}, ),
),
const SizedBox(height: 12), const SizedBox(height: 12),
// Neighbors button // Neighbors button
_buildManagementCard( _buildManagementCard(
@@ -248,26 +261,27 @@ class RepeaterHubScreen extends StatelessWidget {
); );
}, },
), ),
const SizedBox(height: 12), if (isAdmin) const SizedBox(height: 12),
// Settings button // Settings button
_buildManagementCard( if (isAdmin)
context, _buildManagementCard(
icon: Icons.settings, context,
title: l10n.repeater_settings, icon: Icons.settings,
subtitle: l10n.repeater_settingsSubtitle, title: l10n.repeater_settings,
color: Colors.deepOrange, subtitle: l10n.repeater_settingsSubtitle,
onTap: () { color: Colors.deepOrange,
Navigator.push( onTap: () {
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => RepeaterSettingsScreen( MaterialPageRoute(
repeater: repeater, builder: (context) => RepeaterSettingsScreen(
password: password, repeater: repeater,
password: password,
),
), ),
), );
); },
}, ),
),
], ],
), ),
), ),
+56 -30
View File
@@ -8,7 +8,9 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../services/app_debug_log_service.dart'; import '../services/app_debug_log_service.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../services/storage_service.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterSettingsScreen extends StatefulWidget { class RepeaterSettingsScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -25,6 +27,8 @@ class RepeaterSettingsScreen extends StatefulWidget {
} }
class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> { class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
final StorageService _storage = StorageService();
bool _isLoading = false; bool _isLoading = false;
bool _hasChanges = false; bool _hasChanges = false;
bool _refreshingBasic = false; bool _refreshingBasic = false;
@@ -59,6 +63,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
bool _repeatEnabled = true; bool _repeatEnabled = true;
bool _allowReadOnly = true; bool _allowReadOnly = true;
bool _privacyMode = false; bool _privacyMode = false;
bool _autoClockSyncAfterLogin = false;
// Advertisement settings // Advertisement settings
bool _advertEnable = true; bool _advertEnable = true;
@@ -464,18 +469,16 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (mounted) { if (mounted) {
if (successCount > 0) { if (successCount > 0) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.repeater_refreshed(label)), content: Text(l10n.repeater_refreshed(label)),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.repeater_errorRefreshing(label)), content: Text(l10n.repeater_errorRefreshing(label)),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
@@ -566,6 +569,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_lonController.text = widget.repeater.longitude?.toString() ?? ''; _lonController.text = widget.repeater.longitude?.toString() ?? '';
} }
}); });
final autoClockSync = await _storage
.getRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
);
if (!mounted) return;
setState(() {
_autoClockSyncAfterLogin = autoClockSync;
});
} }
Future<void> _saveSettings() async { Future<void> _saveSettings() async {
@@ -653,11 +665,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
}); });
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.repeater_settingsSaved), content: Text(context.l10n.repeater_settingsSaved),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
} }
} catch (e) { } catch (e) {
@@ -666,13 +677,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
}); });
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
context.l10n.repeater_errorSavingSettings(e.toString()), context.l10n.repeater_errorSavingSettings(e.toString()),
),
backgroundColor: Colors.red,
), ),
backgroundColor: Colors.red,
); );
} }
} }
@@ -1139,6 +1149,21 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
onRefresh: _refreshAllowReadOnly, onRefresh: _refreshAllowReadOnly,
refreshTooltip: l10n.repeater_refreshGuestAccess, refreshTooltip: l10n.repeater_refreshGuestAccess,
), ),
SwitchListTile(
title: Text(l10n.repeater_clockSyncAfterLogin),
subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle),
value: _autoClockSyncAfterLogin,
onChanged: (value) async {
setState(() {
_autoClockSyncAfterLogin = value;
});
await _storage.setRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
value,
);
},
contentPadding: EdgeInsets.zero,
),
// Privacy mode - hidden until fully implemented // Privacy mode - hidden until fully implemented
// _buildFeatureToggleRow( // _buildFeatureToggleRow(
// title: l10n.repeater_privacyMode, // title: l10n.repeater_privacyMode,
@@ -1401,9 +1426,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (command == 'erase') { if (command == 'erase') {
if (mounted) { if (mounted) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly))); content: Text(l10n.repeater_eraseSerialOnly),
);
} }
return; return;
} }
@@ -1425,17 +1451,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
await connector.sendFrame(frame); await connector.sendFrame(frame);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.repeater_commandSent(command))), context,
content: Text(l10n.repeater_commandSent(command)),
); );
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.repeater_errorSendingCommand(e.toString())), content: Text(l10n.repeater_errorSendingCommand(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+9 -12
View File
@@ -12,6 +12,7 @@ import '../services/app_settings_service.dart';
import '../services/repeater_command_service.dart'; import '../services/repeater_command_service.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/snack_bar_builder.dart';
class RepeaterStatusScreen extends StatefulWidget { class RepeaterStatusScreen extends StatefulWidget {
final Contact repeater; final Contact repeater;
@@ -309,11 +310,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.repeater_statusRequestTimeout), content: Text(context.l10n.repeater_statusRequestTimeout),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
_recordStatusResult(false); _recordStatusResult(false);
}); });
@@ -323,13 +323,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
_isLoading = false; _isLoading = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
context.l10n.repeater_errorLoadingStatus(e.toString()), backgroundColor: Colors.red,
),
backgroundColor: Colors.red,
),
); );
} }
_recordStatusResult(false); _recordStatusResult(false);
+5 -5
View File
@@ -10,6 +10,7 @@ import '../services/linux_ble_error_classifier.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart'; import '../widgets/device_tile.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'tcp_screen.dart'; import 'tcp_screen.dart';
import 'usb_screen.dart'; import 'usb_screen.dart';
@@ -317,11 +318,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
return; return;
} }
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.scanner_connectionFailed(e.toString())), content: Text(context.l10n.scanner_connectionFailed(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+52 -38
View File
@@ -11,6 +11,7 @@ import '../l10n/l10n.dart';
import '../models/radio_settings.dart'; import '../models/radio_settings.dart';
import '../services/app_debug_log_service.dart'; import '../services/app_debug_log_service.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import '../helpers/snack_bar_builder.dart';
import 'app_settings_screen.dart'; import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart'; import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart'; import 'ble_debug_log_screen.dart';
@@ -513,8 +514,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setNodeName(controller.text); await connector.setNodeName(controller.text);
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_nodeNameUpdated)), context,
content: Text(l10n.settings_nodeNameUpdated),
); );
}, },
child: Text(l10n.common_save), child: Text(l10n.common_save),
@@ -628,10 +630,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final interval = int.tryParse(intervalText); final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) { if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.settings_locationIntervalInvalid), content: Text(l10n.settings_locationIntervalInvalid),
),
); );
return; return;
} }
@@ -639,8 +640,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setCustomVar("gps_interval:$interval"); await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)), context,
content: Text(l10n.settings_locationUpdated),
); );
} }
@@ -660,15 +662,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
: currentLon; : currentLon;
if (lat == null || lon == null) { if (lat == null || lon == null) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)), context,
content: Text(l10n.settings_locationBothRequired),
); );
return; return;
} }
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)), context,
content: Text(l10n.settings_locationInvalid),
); );
return; return;
} }
@@ -676,8 +680,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
await connector.setNodeLocation(lat: lat, lon: lon); await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)), context,
content: Text(l10n.settings_locationUpdated),
); );
}, },
child: Text(l10n.common_save), child: Text(l10n.common_save),
@@ -691,9 +696,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _syncTime(BuildContext context, MeshCoreConnector connector) { void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
connector.syncTime(); connector.syncTime();
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized))); content: Text(l10n.settings_timeSynchronized),
);
} }
void _confirmReboot(BuildContext context, MeshCoreConnector connector) { void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
@@ -758,23 +764,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (!mounted) return; if (!mounted) return;
switch (result) { switch (result) {
case gpxExportSuccess: case gpxExportSuccess:
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); content: Text(l10n.settings_gpxExportSuccess),
);
case gpxExportNoContacts: case gpxExportNoContacts:
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), context,
content: Text(l10n.settings_gpxExportNoContacts),
); );
break; break;
case gpxExportNotAvailable: case gpxExportNotAvailable:
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), context,
content: Text(l10n.settings_gpxExportNotAvailable),
); );
break; break;
case gpxExportFailed: case gpxExportFailed:
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); content: Text(l10n.settings_gpxExportError),
);
break; break;
} }
} }
@@ -1077,8 +1087,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
); );
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_telemetryModeUpdated)), context,
content: Text(l10n.settings_telemetryModeUpdated),
); );
}, },
child: Text(l10n.common_save), child: Text(l10n.common_save),
@@ -1410,18 +1421,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final txPower = int.tryParse(_txPowerController.text); final txPower = int.tryParse(_txPowerController.text);
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) { if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
ScaffoldMessenger.of( showDismissibleSnackBar(
context, context,
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid))); content: Text(l10n.settings_frequencyInvalid),
);
return; return;
} }
final maxTxPower = widget.connector.maxTxPower ?? 22; final maxTxPower = widget.connector.maxTxPower ?? 22;
if (txPower == null || txPower < 0 || txPower > maxTxPower) { if (txPower == null || txPower < 0 || txPower > maxTxPower) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
),
); );
return; return;
} }
@@ -1441,8 +1452,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (knownRepeat) { if (knownRepeat) {
const validRepeatFreqsKHz = {433000, 869000, 918000}; const validRepeatFreqsKHz = {433000, 869000, 918000};
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)), context,
content: Text(l10n.settings_clientRepeatFreqWarning),
); );
return; return;
} }
@@ -1472,14 +1484,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (!mounted) return; if (!mounted) return;
_logRadioSettingsState('Radio settings saved successfully'); _logRadioSettingsState('Radio settings saved successfully');
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), context,
content: Text(l10n.settings_radioSettingsUpdated),
); );
} catch (e) { } catch (e) {
_appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings'); _appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings');
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(l10n.settings_error(e.toString()))), context,
content: Text(l10n.settings_error(e.toString())),
); );
} }
Navigator.pop(context); Navigator.pop(context);
+5 -2
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'usb_screen.dart'; import 'usb_screen.dart';
@@ -270,8 +271,10 @@ class _TcpScreenState extends State<TcpScreen> {
void _showError(String message) { void _showError(String message) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red), context,
content: Text(message),
backgroundColor: Colors.red,
); );
} }
+13 -15
View File
@@ -14,6 +14,7 @@ import '../utils/app_logger.dart';
import '../widgets/path_management_dialog.dart'; import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart'; import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../helpers/snack_bar_builder.dart';
class TelemetryScreen extends StatefulWidget { class TelemetryScreen extends StatefulWidget {
final Contact contact; final Contact contact;
@@ -86,11 +87,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoading = false; _isLoading = false;
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.telemetry_requestTimeout), content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
_recordTelemetryResult(false); _recordTelemetryResult(false);
}); });
@@ -137,11 +137,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_parsedTelemetry = parsedTelemetry; _parsedTelemetry = parsedTelemetry;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.telemetry_receivedData), content: Text(context.l10n.telemetry_receivedData),
backgroundColor: Colors.green, backgroundColor: Colors.green,
),
); );
_statusTimeout?.cancel(); _statusTimeout?.cancel();
if (!mounted) return; if (!mounted) return;
@@ -182,11 +181,10 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoaded = false; _isLoaded = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.telemetry_errorLoading(e.toString())), content: Text(context.l10n.telemetry_errorLoading(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
+5 -5
View File
@@ -10,6 +10,7 @@ import '../utils/app_logger.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../utils/usb_port_labels.dart'; import '../utils/usb_port_labels.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
import 'scanner_screen.dart'; import 'scanner_screen.dart';
import 'tcp_screen.dart'; import 'tcp_screen.dart';
@@ -383,11 +384,10 @@ class _UsbScreenState extends State<UsbScreen> {
void _showError(Object error) { void _showError(Object error) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(_friendlyErrorMessage(error)), content: Text(_friendlyErrorMessage(error)),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
+34
View File
@@ -7,8 +7,42 @@ class StorageService {
static const String _pathHistoryPrefix = 'path_history_'; static const String _pathHistoryPrefix = 'path_history_';
static const String _pendingMessagesKey = 'pending_messages'; static const String _pendingMessagesKey = 'pending_messages';
static const String _repeaterPasswordsKey = 'repeater_passwords'; static const String _repeaterPasswordsKey = 'repeater_passwords';
static const String _repeaterAutoClockSyncAfterLoginKey =
'repeater_auto_clock_sync_after_login';
static const String _deliveryObservationsKey = 'delivery_observations'; static const String _deliveryObservationsKey = 'delivery_observations';
Future<Map<String, bool>> _loadRepeaterAutoClockSyncAfterLogin() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey);
if (jsonStr == null) return {};
try {
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
return json.map((key, value) => MapEntry(key, value == true));
} catch (e) {
return {};
}
}
Future<bool> getRepeaterAutoClockSyncAfterLoginEnabled(
String repeaterPubKeyHex,
) async {
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
return settings[repeaterPubKeyHex] ?? false;
}
Future<void> setRepeaterAutoClockSyncAfterLoginEnabled(
String repeaterPubKeyHex,
bool enabled,
) async {
final prefs = PrefsManager.instance;
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
settings[repeaterPubKeyHex] = enabled;
final jsonStr = jsonEncode(settings);
await prefs.setString(_repeaterAutoClockSyncAfterLoginKey, jsonStr);
}
Future<void> savePathHistory( Future<void> savePathHistory(
String contactPubKeyHex, String contactPubKeyHex,
ContactPathHistory history, ContactPathHistory history,
+5 -2
View File
@@ -6,6 +6,7 @@ import 'package:llamadart/llamadart.dart';
import '../models/app_settings.dart'; import '../models/app_settings.dart';
import '../models/translation_support.dart'; import '../models/translation_support.dart';
import '../helpers/gif_helper.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import 'app_settings_service.dart'; import 'app_settings_service.dart';
import 'translation_file_store.dart'; import 'translation_file_store.dart';
@@ -509,8 +510,10 @@ class TranslationService extends ChangeNotifier {
if (trimmed.isEmpty) { if (trimmed.isEmpty) {
return false; return false;
} }
return !(trimmed.startsWith('g:') || if (GifHelper.parseGif(trimmed) != null) {
trimmed.startsWith('m:') || return false;
}
return !(trimmed.startsWith('m:') ||
trimmed.startsWith('V1|') || trimmed.startsWith('V1|') ||
trimmed.startsWith('r:')); trimmed.startsWith('r:'));
} }
+27 -32
View File
@@ -11,6 +11,7 @@ import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../helpers/path_helper.dart'; import '../helpers/path_helper.dart';
import '../services/path_history_service.dart'; import '../services/path_history_service.dart';
import '../helpers/snack_bar_builder.dart';
import 'path_selection_dialog.dart'; import 'path_selection_dialog.dart';
class PathManagementDialog { class PathManagementDialog {
@@ -65,11 +66,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
void _showFullPathDialog(BuildContext context, List<int> pathBytes) { void _showFullPathDialog(BuildContext context, List<int> pathBytes) {
final l10n = context.l10n; final l10n = context.l10n;
if (pathBytes.isEmpty) { if (pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_pathDetailsNotAvailable), content: Text(l10n.chat_pathDetailsNotAvailable),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
return; return;
} }
@@ -159,11 +159,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_hopsCount(result.length)), content: Text(l10n.chat_hopsCount(result.length)),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
} }
} }
@@ -337,13 +336,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
_showFullPathDialog(context, path.pathBytes), _showFullPathDialog(context, path.pathBytes),
onTap: () async { onTap: () async {
if (path.pathBytes.isEmpty) { if (path.pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
l10n.chat_pathDetailsNotAvailable, l10n.chat_pathDetailsNotAvailable,
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
return; return;
} }
@@ -361,13 +359,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
if (!context.mounted) return; if (!context.mounted) return;
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(
l10n.path_usingHopsPath(path.hopCount), l10n.path_usingHopsPath(path.hopCount),
),
duration: const Duration(seconds: 2),
), ),
duration: const Duration(seconds: 2),
); );
}, },
), ),
@@ -459,11 +456,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
onTap: () async { onTap: () async {
await connector.clearContactPath(currentContact); await connector.clearContactPath(currentContact);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_pathCleared), content: Text(l10n.chat_pathCleared),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
@@ -489,11 +485,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
pathLen: -1, pathLen: -1,
); );
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.chat_floodModeEnabled), content: Text(l10n.chat_floodModeEnabled),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
),
); );
Navigator.pop(context); Navigator.pop(context);
}, },
+11 -14
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../helpers/snack_bar_builder.dart';
class PathSelectionDialog extends StatefulWidget { class PathSelectionDialog extends StatefulWidget {
final List<Contact> availableContacts; final List<Contact> availableContacts;
@@ -138,26 +139,22 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
// Show error for invalid prefixes // Show error for invalid prefixes
if (invalidPrefixes.isNotEmpty) { if (invalidPrefixes.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text( content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))),
l10n.path_invalidHexPrefixes(invalidPrefixes.join(", ")), duration: const Duration(seconds: 3),
), backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
backgroundColor: Colors.red,
),
); );
return; return;
} }
// Check max path length (64 hops) // Check max path length (64 hops)
if (pathBytesList.length > 64) { if (pathBytesList.length > 64) {
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(l10n.path_tooLong), content: Text(l10n.path_tooLong),
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
return; return;
} }
+34 -6
View File
@@ -14,7 +14,7 @@ import 'path_management_dialog.dart';
class RepeaterLoginDialog extends StatefulWidget { class RepeaterLoginDialog extends StatefulWidget {
final Contact repeater; final Contact repeater;
final Function(String password) onLogin; final Function(String password, bool isAdmin) onLogin;
const RepeaterLoginDialog({ const RepeaterLoginDialog({
super.key, super.key,
@@ -119,6 +119,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
: '${selection.hopCount} hops'; : '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin'); appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin');
bool? loginResult; bool? loginResult;
bool isAdmin = false;
for (int attempt = 0; attempt < _maxAttempts; attempt++) { for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@@ -131,7 +132,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
); );
await _connector.sendFrame(loginFrame); await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout); (loginResult, isAdmin) = await _awaitLoginResponse(timeout);
if (loginResult == true) { if (loginResult == true) {
appLogger.info( appLogger.info(
'Login succeeded for ${repeater.name}', 'Login succeeded for ${repeater.name}',
@@ -187,9 +188,32 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex); await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex);
} }
final autoClockSync = await _storage
.getRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
);
if (autoClockSync) {
try {
final timestampSeconds =
DateTime.now().millisecondsSinceEpoch ~/ 1000;
await _connector.sendFrame(
buildSendCliCommandFrame(
repeater.publicKey,
'clock sync',
timestampSeconds: timestampSeconds,
),
);
} catch (e) {
appLogger.warn(
'Auto clock sync failed for ${repeater.name}: $e',
tag: 'RepeaterLogin',
);
}
}
if (mounted) { if (mounted) {
Navigator.pop(context, password); Navigator.pop(context, password);
Future.microtask(() => widget.onLogin(password)); Future.microtask(() => widget.onLogin(password, isAdmin));
} }
} catch (e) { } catch (e) {
final repeater = _resolveRepeater(_connector); final repeater = _resolveRepeater(_connector);
@@ -206,17 +230,21 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
} }
} }
Future<bool?> _awaitLoginResponse(Duration timeout) async { // _awaitLoginResponse returns a record of bool, for success and if the client is an admin
Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async {
final completer = Completer<bool?>(); final completer = Completer<bool?>();
Timer? timer; Timer? timer;
StreamSubscription<Uint8List>? subscription; StreamSubscription<Uint8List>? subscription;
final targetPrefix = widget.repeater.publicKey.sublist(0, 6); final targetPrefix = widget.repeater.publicKey.sublist(0, 6);
bool isAdmin = false;
subscription = _connector.receivedFrames.listen((frame) { subscription = _connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return; if (frame.isEmpty) return;
final code = frame[0]; final code = frame[0];
if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return;
if (frame.length < 8) return; if (frame.length < 8) return;
// NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the
// expected client permissions
isAdmin = (frame[1] == 1);
final prefix = frame.sublist(2, 8); final prefix = frame.sublist(2, 8);
if (!listEquals(prefix, targetPrefix)) return; if (!listEquals(prefix, targetPrefix)) return;
@@ -235,7 +263,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
final result = await completer.future; final result = await completer.future;
timer.cancel(); timer.cancel();
await subscription.cancel(); await subscription.cancel();
return result; return (result, isAdmin);
} }
@override @override
+15 -10
View File
@@ -10,11 +10,12 @@ import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../helpers/snack_bar_builder.dart';
import 'path_management_dialog.dart'; import 'path_management_dialog.dart';
class RoomLoginDialog extends StatefulWidget { class RoomLoginDialog extends StatefulWidget {
final Contact room; final Contact room;
final Function(String password) onLogin; final Function(String password, bool isAdmin) onLogin;
const RoomLoginDialog({super.key, required this.room, required this.onLogin}); const RoomLoginDialog({super.key, required this.room, required this.onLogin});
@@ -114,6 +115,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
: '${selection.hopCount} hops'; : '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin'); appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin');
bool? loginResult; bool? loginResult;
bool isAdmin = false;
for (int attempt = 0; attempt < _maxAttempts; attempt++) { for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@@ -126,7 +128,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
); );
await _connector.sendFrame(loginFrame); await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout); (loginResult, isAdmin) = await _awaitLoginResponse(timeout);
if (loginResult == true) { if (loginResult == true) {
appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin'); appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin');
break; break;
@@ -166,7 +168,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
if (mounted) { if (mounted) {
Navigator.pop(context, password); Navigator.pop(context, password);
Future.microtask(() => widget.onLogin(password)); Future.microtask(() => widget.onLogin(password, isAdmin));
} }
} catch (e) { } catch (e) {
final room = _resolveRepeater(_connector); final room = _resolveRepeater(_connector);
@@ -175,26 +177,29 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
setState(() { setState(() {
_isLoggingIn = false; _isLoggingIn = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( showDismissibleSnackBar(
SnackBar( context,
content: Text(context.l10n.login_failed(e.toString())), content: Text(context.l10n.login_failed(e.toString())),
backgroundColor: Colors.red, backgroundColor: Colors.red,
),
); );
} }
} }
} }
Future<bool?> _awaitLoginResponse(Duration timeout) async { Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async {
final completer = Completer<bool?>(); final completer = Completer<bool?>();
Timer? timer; Timer? timer;
StreamSubscription<Uint8List>? subscription; StreamSubscription<Uint8List>? subscription;
final targetPrefix = widget.room.publicKey.sublist(0, 6); final targetPrefix = widget.room.publicKey.sublist(0, 6);
bool isAdmin = false;
subscription = _connector.receivedFrames.listen((frame) { subscription = _connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return; if (frame.isEmpty) return;
final code = frame[0]; final code = frame[0];
if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return;
// NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the
// expected client permissions
isAdmin = (frame[1] == 1);
if (frame.length < 8) return; if (frame.length < 8) return;
final prefix = frame.sublist(2, 8); final prefix = frame.sublist(2, 8);
if (!listEquals(prefix, targetPrefix)) return; if (!listEquals(prefix, targetPrefix)) return;
@@ -214,7 +219,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
final result = await completer.future; final result = await completer.future;
timer.cancel(); timer.cancel();
await subscription.cancel(); await subscription.cancel();
return result; return (result, isAdmin);
} }
@override @override
-1
View File
@@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial flserial
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 7.0.0+9 version: 8.0.0+10
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2
+1 -69
View File
@@ -1,69 +1 @@
{ {}
"bg": [
"chat_sendMessage"
],
"de": [
"chat_sendMessage"
],
"es": [
"chat_sendMessage"
],
"fr": [
"chat_sendMessage"
],
"hu": [
"chat_sendMessage"
],
"it": [
"chat_sendMessage"
],
"ja": [
"chat_sendMessage"
],
"ko": [
"chat_sendMessage"
],
"nl": [
"chat_sendMessage"
],
"pl": [
"chat_sendMessage"
],
"pt": [
"chat_sendMessage"
],
"ru": [
"chat_sendMessage"
],
"sk": [
"chat_sendMessage"
],
"sl": [
"chat_sendMessage"
],
"sv": [
"chat_sendMessage"
],
"uk": [
"chat_sendMessage"
],
"zh": [
"chat_sendMessage"
]
}
-1
View File
@@ -11,7 +11,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial flserial
flutter_local_notifications_windows flutter_local_notifications_windows
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)