mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-01 22:50:37 +10:00
Merge commit 'refs/pull/379/head' of https://github.com/zjs81/meshcore-open into test-regions
This commit is contained in:
@@ -6,6 +6,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:meshcore_open/screens/region_management_screen.dart';
|
||||
import 'package:meshcore_open/storage/region_store.dart';
|
||||
import 'package:meshcore_open/widgets/adaptive_app_bar_title.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
@@ -65,6 +68,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final Map<String, GlobalKey> _messageKeys = {};
|
||||
bool _isLoadingOlder = false;
|
||||
bool _communitiesLoaded = false;
|
||||
Region region = '';
|
||||
|
||||
MeshCoreConnector? _connector;
|
||||
DateTime? _lastChannelSendAt;
|
||||
@@ -81,6 +85,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
|
||||
_scrollController.onScrollNearTop = _loadOlderMessages;
|
||||
_scrollController.showJumpToBottom.addListener(_clearDividerAtBottom);
|
||||
region = context.read<MeshCoreConnector>().getChannelRegion(
|
||||
widget.channel.index,
|
||||
);
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
@@ -266,45 +274,82 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
|
||||
// Determine icon and colors based on channel type
|
||||
IconData icon = Icons.lock;
|
||||
Color iconColor = Colors.blue;
|
||||
Color bgColor = Colors.blue.withValues(alpha: 0.2);
|
||||
|
||||
// TODO(clauwn): add community handling
|
||||
final isCommunityChannel = false;
|
||||
final isCommunityPublic = false;
|
||||
|
||||
if (isCommunityChannel) {
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
icon = isCommunityPublic ? Icons.groups : Icons.tag;
|
||||
} else if (widget.channel.isPublicChannel) {
|
||||
icon = Icons.public;
|
||||
iconColor = Colors.green;
|
||||
bgColor = Colors.green.withValues(alpha: 0.2);
|
||||
} else if (widget.channel.isHashtagChannel) {
|
||||
icon = Icons.tag;
|
||||
}
|
||||
|
||||
final regionHeader = region != ''
|
||||
? context.l10n.channels_regionSetTo(region)
|
||||
: context.l10n.channels_regionNotSet;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
_channelIcon(widget.channel),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.channel.name.isEmpty
|
||||
? context.l10n.channels_channelIndex(
|
||||
widget.channel.index,
|
||||
)
|
||||
: widget.channel.name,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final unreadCount = connector
|
||||
.getUnreadCountForChannelIndex(widget.channel.index);
|
||||
final privacy = widget.channel.isPublicChannel
|
||||
? context.l10n.channels_public
|
||||
: context.l10n.channels_private;
|
||||
return Text(
|
||||
'$privacy • ${context.l10n.chat_unread(unreadCount)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
title: GestureDetector(
|
||||
onTap: () => openRegionSelectDialog(widget.channel),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: bgColor,
|
||||
child: Icon(icon, color: iconColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
return Text(
|
||||
widget.channel.name.isEmpty
|
||||
? context.l10n.channels_channelIndex(
|
||||
widget.channel.index,
|
||||
)
|
||||
: widget.channel.name,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
);
|
||||
},
|
||||
),
|
||||
Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final unreadCount = connector
|
||||
.getUnreadCountForChannelIndex(
|
||||
widget.channel.index,
|
||||
);
|
||||
return Text(
|
||||
'$regionHeader • ${context.l10n.chat_unread(unreadCount)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
bottom: const SyncProgressAppBarBottom(),
|
||||
titleSpacing: 0,
|
||||
actions: [
|
||||
const RadioStatsIconButton(),
|
||||
PopupMenuButton<String>(
|
||||
@@ -1530,6 +1575,117 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
}
|
||||
|
||||
void openRegionSelectDialog(Channel channel) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => _RegionSelectDialog(channel: channel),
|
||||
);
|
||||
if (context.mounted) {
|
||||
await _connector?.loadChannelSettings();
|
||||
setState(() {
|
||||
region = _connector?.getChannelRegion(channel.index) ?? '';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _RegionSelectDialog extends StatefulWidget {
|
||||
final Channel channel;
|
||||
|
||||
const _RegionSelectDialog({required this.channel});
|
||||
|
||||
@override
|
||||
_RegionSelectDialogState createState() => _RegionSelectDialogState();
|
||||
}
|
||||
|
||||
class _RegionSelectDialogState extends State<_RegionSelectDialog> {
|
||||
final RegionStore regionStore = RegionStore();
|
||||
|
||||
List<Region> regions = [];
|
||||
int selectedIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadRegions();
|
||||
}
|
||||
|
||||
void loadRegions() {
|
||||
setState(() {
|
||||
regions = regionStore.loadRegions();
|
||||
Region channelRegion = context.read<MeshCoreConnector>().getChannelRegion(
|
||||
widget.channel.index,
|
||||
);
|
||||
selectedIndex = regions.indexOf(channelRegion);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
title: AdaptiveAppBarTitle(
|
||||
context.l10n.channels_regionSelect_Title,
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: context.l10n.channels_clearRegion,
|
||||
icon: const Icon(Icons.backspace_outlined),
|
||||
onPressed: () {
|
||||
context.read<MeshCoreConnector>().setChannelRegion(
|
||||
widget.channel.index,
|
||||
'',
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.settings_regionSettingsSubtitle,
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () async {
|
||||
await pushRegionManagementScreen(context);
|
||||
loadRegions();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: regions.length,
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
title: Text(regions[index]),
|
||||
tileColor: selectedIndex == index
|
||||
? Colors.blue.withValues(alpha: 0.2)
|
||||
: null,
|
||||
onTap: () {
|
||||
context.read<MeshCoreConnector>().setChannelRegion(
|
||||
widget.channel.index,
|
||||
regions[index],
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SwipeReplyBubble extends StatefulWidget {
|
||||
|
||||
@@ -367,15 +367,32 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
_communityIndex,
|
||||
);
|
||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||
final community = isCommunityChannel
|
||||
? _communityIndex.getCommunityForChannel(channel)
|
||||
: null;
|
||||
final region = connector.hasChannelRegion(channel.index)
|
||||
? context.l10n.channels_regionSetTo(
|
||||
connector.getChannelRegion(channel.index),
|
||||
)
|
||||
: context.l10n.channels_regionNotSet;
|
||||
String subtitle = region;
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
icon = Icons.groups;
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
if (community != null) {
|
||||
subtitle =
|
||||
'${context.l10n.community_publicChannel} • ${community.name}';
|
||||
}
|
||||
case ChannelType.communityHashtag:
|
||||
icon = Icons.tag;
|
||||
iconColor = Colors.purple;
|
||||
bgColor = Colors.purple.withValues(alpha: 0.2);
|
||||
if (community != null) {
|
||||
subtitle =
|
||||
'${context.l10n.community_hashtagChannel} • ${community.name}';
|
||||
}
|
||||
case ChannelType.public:
|
||||
icon = Icons.public;
|
||||
iconColor = Colors.green;
|
||||
@@ -446,6 +463,11 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
: channel.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/l10n/l10n.dart';
|
||||
import 'package:meshcore_open/storage/region_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Future<void> pushRegionManagementScreen(BuildContext context) {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const RegionManagementScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class RegionManagementScreen extends StatefulWidget {
|
||||
const RegionManagementScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RegionManagementScreen> createState() => _RegionManagementScreenState();
|
||||
}
|
||||
|
||||
class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
||||
final RegionStore _regionStore = RegionStore();
|
||||
List<Region> _regions = [];
|
||||
|
||||
String region = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_regionStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
_loadRegions();
|
||||
}
|
||||
|
||||
void _loadRegions() {
|
||||
context.read<MeshCoreConnector>().loadChannelSettings();
|
||||
|
||||
final regions = _regionStore.loadRegions();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_regions = regions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settings_regionManagement_screenTitle),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: l10n.settings_regionAddRegion,
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showAddRegionDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 88),
|
||||
itemCount: _regions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final region = _regions[index];
|
||||
return _buildRegionTile(context, region);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddRegionDialog(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final controller = TextEditingController(text: region);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.settings_regionName),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _handleAddRegion(controller.text, context),
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.settings_regionNameHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(RegExp("[a-z0-9-]")),
|
||||
],
|
||||
maxLength: 30,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _handleAddRegion(controller.text, context),
|
||||
child: Text(l10n.common_add),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleAddRegion(Region region, BuildContext context) {
|
||||
Navigator.pop(context);
|
||||
_regionStore.addRegion(region);
|
||||
_loadRegions();
|
||||
}
|
||||
|
||||
Widget _buildRegionTile(BuildContext context, Region region) {
|
||||
return Card(
|
||||
key: ValueKey(region),
|
||||
child: ListTile(
|
||||
dense: false,
|
||||
title: Text(region),
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.delete_outline),
|
||||
onPressed: () => _confirmDelete(context, region),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, Region region) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.settings_deleteRegion),
|
||||
content: Text(context.l10n.settings_deleteRegionConfirm(region)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
await _regionStore.removeRegion(region);
|
||||
_loadRegions();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.settings_regionDeleted)),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.common_delete,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import 'region_management_screen.dart';
|
||||
|
||||
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
||||
/// to the UI enum range (always 5-8).
|
||||
@@ -290,6 +291,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: () => _showRadioSettings(context, connector),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.landscape),
|
||||
title: Text(l10n.settings_regionSettings),
|
||||
subtitle: Text(l10n.settings_regionSettingsSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => pushRegionManagementScreen(context),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sensors_outlined),
|
||||
title: Text(l10n.radioStats_settingsTile),
|
||||
|
||||
Reference in New Issue
Block a user