feat: add permission-denied dialog for macOS network/mic access

When macOS denies network access (PermissionDenied), show a localized
dialog explaining how to grant permission in System Settings, with
an 'Open System Settings' button that opens directly to the Local
Network privacy pane.

Also adds author info (Edison Jwa) to About dialog and moves
diagnostics button from AppBar into About dialog.
This commit is contained in:
Edison Jwa
2026-05-17 21:57:56 +09:00
parent b97cc9589b
commit 4ecea09bbc
8 changed files with 163 additions and 16 deletions
Generated
+7 -7
View File
@@ -345,7 +345,7 @@ dependencies = [
[[package]]
name = "chanora_audio"
version = "0.0.1-pre"
version = "0.2.0-beta.1"
dependencies = [
"audiopus",
"chanora_protocol",
@@ -368,7 +368,7 @@ dependencies = [
[[package]]
name = "chanora_bridge"
version = "0.0.1-pre"
version = "0.2.0-beta.1"
dependencies = [
"chanora_core",
"chanora_protocol",
@@ -386,7 +386,7 @@ dependencies = [
[[package]]
name = "chanora_core"
version = "0.0.1-pre"
version = "0.2.0-beta.1"
dependencies = [
"chanora_audio",
"chanora_diagnostics",
@@ -401,7 +401,7 @@ dependencies = [
[[package]]
name = "chanora_diagnostics"
version = "0.0.1-pre"
version = "0.2.0-beta.1"
dependencies = [
"thiserror 2.0.18",
"tracing",
@@ -410,7 +410,7 @@ dependencies = [
[[package]]
name = "chanora_protocol"
version = "0.0.1-pre"
version = "0.2.0-beta.1"
dependencies = [
"async-trait",
"futures",
@@ -426,7 +426,7 @@ dependencies = [
[[package]]
name = "chanora_state"
version = "0.0.1-pre"
version = "0.2.0-beta.1"
dependencies = [
"thiserror 2.0.18",
"tracing",
@@ -434,7 +434,7 @@ dependencies = [
[[package]]
name = "chanora_storage"
version = "0.0.1-pre"
version = "0.2.0-beta.1"
dependencies = [
"base64",
"chacha20poly1305",
+8 -1
View File
@@ -22,6 +22,7 @@
"@aboutVersion": {
"placeholders": { "version": { "type": "String" } }
},
"aboutAuthor": "Author: Edison Jwa",
"aboutNonAffiliation": "Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.",
"aboutLicenseHeading": "License",
"aboutLicenseBody": "Chanora is dual-licensed under the Apache License, Version 2.0 or the MIT License, at your option. The full license texts ship as LICENSE-APACHE and LICENSE-MIT at the repository root.",
@@ -154,5 +155,11 @@
"placeholders": {
"message": { "type": "String" }
}
}
},
"networkPermissionTitle": "Network Permission Required",
"networkPermissionBody": "Chanora needs permission to access the network. On macOS, go to System Settings → Privacy & Security → Local Network and enable Chanora, then try again.",
"networkPermissionOpenSettings": "Open System Settings",
"microphonePermissionTitle": "Microphone Permission Required",
"microphonePermissionBody": "Chanora needs permission to access the microphone. On macOS, go to System Settings → Privacy & Security → Microphone and enable Chanora.",
"permissionDenied": "Permission Denied"
}
+8 -1
View File
@@ -18,6 +18,7 @@
"diagnosticsAction": "诊断信息",
"aboutAction": "关于",
"aboutVersion": "版本 {version}",
"aboutAuthor": "作者: Edison Jwa",
"aboutNonAffiliation": "Chanora 是独立项目,与 TeamSpeak 之间不存在任何附属、认可、赞助或官方关联关系。",
"aboutLicenseHeading": "许可协议",
"aboutLicenseBody": "Chanora 采用 Apache License 2.0 或 MIT License 双协议授权,使用者可任选其一。完整协议文本以 LICENSE-APACHE 与 LICENSE-MIT 形式随仓库一同分发。",
@@ -111,5 +112,11 @@
"placeholders": {
"message": { "type": "String" }
}
}
},
"networkPermissionTitle": "需要网络权限",
"networkPermissionBody": "Chanora 需要网络访问权限。请前往系统设置 → 隐私与安全性 → 本地网络,启用 Chanora,然后重试。",
"networkPermissionOpenSettings": "打开系统设置",
"microphonePermissionTitle": "需要麦克风权限",
"microphonePermissionBody": "Chanora 需要麦克风访问权限。请前往系统设置 → 隐私与安全性 → 麦克风,启用 Chanora。",
"permissionDenied": "权限被拒绝"
}
@@ -181,6 +181,12 @@ abstract class AppL10n {
/// **'Version {version}'**
String aboutVersion(String version);
/// No description provided for @aboutAuthor.
///
/// In en, this message translates to:
/// **'Author: Edison Jwa'**
String get aboutAuthor;
/// No description provided for @aboutNonAffiliation.
///
/// In en, this message translates to:
@@ -684,6 +690,42 @@ abstract class AppL10n {
/// In en, this message translates to:
/// **'Could not join channel: {message}'**
String channelJoinFailedGeneric(String message);
/// No description provided for @networkPermissionTitle.
///
/// In en, this message translates to:
/// **'Network Permission Required'**
String get networkPermissionTitle;
/// No description provided for @networkPermissionBody.
///
/// In en, this message translates to:
/// **'Chanora needs permission to access the network. On macOS, go to System Settings → Privacy & Security → Local Network and enable Chanora, then try again.'**
String get networkPermissionBody;
/// No description provided for @networkPermissionOpenSettings.
///
/// In en, this message translates to:
/// **'Open System Settings'**
String get networkPermissionOpenSettings;
/// No description provided for @microphonePermissionTitle.
///
/// In en, this message translates to:
/// **'Microphone Permission Required'**
String get microphonePermissionTitle;
/// No description provided for @microphonePermissionBody.
///
/// In en, this message translates to:
/// **'Chanora needs permission to access the microphone. On macOS, go to System Settings → Privacy & Security → Microphone and enable Chanora.'**
String get microphonePermissionBody;
/// No description provided for @permissionDenied.
///
/// In en, this message translates to:
/// **'Permission Denied'**
String get permissionDenied;
}
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
@@ -54,6 +54,9 @@ class AppL10nEn extends AppL10n {
return 'Version $version';
}
@override
String get aboutAuthor => 'Author: Edison Jwa';
@override
String get aboutNonAffiliation =>
'Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.';
@@ -339,4 +342,24 @@ class AppL10nEn extends AppL10n {
String channelJoinFailedGeneric(String message) {
return 'Could not join channel: $message';
}
@override
String get networkPermissionTitle => 'Network Permission Required';
@override
String get networkPermissionBody =>
'Chanora needs permission to access the network. On macOS, go to System Settings → Privacy & Security → Local Network and enable Chanora, then try again.';
@override
String get networkPermissionOpenSettings => 'Open System Settings';
@override
String get microphonePermissionTitle => 'Microphone Permission Required';
@override
String get microphonePermissionBody =>
'Chanora needs permission to access the microphone. On macOS, go to System Settings → Privacy & Security → Microphone and enable Chanora.';
@override
String get permissionDenied => 'Permission Denied';
}
@@ -52,6 +52,9 @@ class AppL10nZh extends AppL10n {
return '版本 $version';
}
@override
String get aboutAuthor => '作者: Edison Jwa';
@override
String get aboutNonAffiliation =>
'Chanora 是独立项目,与 TeamSpeak 之间不存在任何附属、认可、赞助或官方关联关系。';
@@ -331,4 +334,24 @@ class AppL10nZh extends AppL10n {
String channelJoinFailedGeneric(String message) {
return '无法加入频道:$message';
}
@override
String get networkPermissionTitle => '需要网络权限';
@override
String get networkPermissionBody =>
'Chanora 需要网络访问权限。请前往系统设置 → 隐私与安全性 → 本地网络,启用 Chanora,然后重试。';
@override
String get networkPermissionOpenSettings => '打开系统设置';
@override
String get microphonePermissionTitle => '需要麦克风权限';
@override
String get microphonePermissionBody =>
'Chanora 需要麦克风访问权限。请前往系统设置 → 隐私与安全性 → 麦克风,启用 Chanora。';
@override
String get permissionDenied => '权限被拒绝';
}
+52 -7
View File
@@ -11,7 +11,7 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'dart:io' show Platform;
import 'dart:io' show Platform, Process;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -429,13 +429,54 @@ class _BetaHomeState extends State<_BetaHome> {
});
} catch (e) {
if (!mounted) return;
final errorStr = e.toString();
if (errorStr.contains('PermissionDenied') ||
errorStr.contains('Operation not permitted')) {
_showPermissionDeniedDialog(errorStr);
}
setState(() {
_phase = _Phase.idle;
_error = e.toString();
_error = errorStr;
});
}
}
void _showPermissionDeniedDialog(String rawError) {
final l10n = AppL10n.of(context);
final isNetwork =
rawError.contains('9987') || rawError.contains('connect');
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title:
Text(isNetwork ? l10n.networkPermissionTitle : l10n.permissionDenied),
content: Text(
isNetwork ? l10n.networkPermissionBody : l10n.microphonePermissionBody),
actions: [
if (Platform.isMacOS)
TextButton(
onPressed: () {
Navigator.pop(ctx);
try {
Process.run(
'open',
[
'x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork',
],
);
} catch (_) {}
},
child: Text(l10n.networkPermissionOpenSettings),
),
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
),
);
}
// ignore: unused_element
Future<void> _setPtt(bool active) async {
try {
@@ -853,6 +894,8 @@ class _BetaHomeState extends State<_BetaHome> {
l10n.aboutVersion(_kAppVersion),
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 4),
Text(l10n.aboutAuthor, style: theme.textTheme.bodySmall),
const SizedBox(height: 16),
Text(l10n.aboutNonAffiliation, style: theme.textTheme.bodyMedium),
const SizedBox(height: 12),
@@ -870,6 +913,13 @@ class _BetaHomeState extends State<_BetaHome> {
),
),
actions: [
TextButton(
onPressed: () async {
Navigator.of(ctx).pop();
await _onShowDiagnostics(context);
},
child: Text(l10n.diagnosticsAction),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(l10n.closeAction),
@@ -1001,11 +1051,6 @@ class _BetaHomeState extends State<_BetaHome> {
icon: const Icon(Icons.info_outline),
onPressed: () => _onShowAbout(context),
),
IconButton(
tooltip: l10n.diagnosticsAction,
icon: const Icon(Icons.bug_report_outlined),
onPressed: () => _onShowDiagnostics(context),
),
if (_phase == _Phase.connected) ...[
// The previous Refresh action (Icons.refresh + _onRefresh)
// was removed in v1.0.0-rc.8 — the snapshot stream the