fix: resolve windows documentation crash and refine theme toggle and UI indicators
This commit is contained in:
1 parent
f890058cf3
commit
0e99b73989
6 files changed
+294
-188
No files matched your search
@@ -60,11 +60,15 @@ class DocumentationServerService {
|
||||
response.add(bytes);
|
||||
await response.close();
|
||||
} catch (_) {
|
||||
final response = request.response;
|
||||
response.statusCode = HttpStatus.notFound;
|
||||
response.headers.contentType = ContentType.text;
|
||||
response.write('Document asset not found');
|
||||
await response.close();
|
||||
try {
|
||||
final response = request.response;
|
||||
response.statusCode = HttpStatus.notFound;
|
||||
response.headers.contentType = ContentType.text;
|
||||
response.write('Document asset not found');
|
||||
await response.close();
|
||||
} catch (_) {
|
||||
// Socket already closed by client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_windows/webview_windows.dart';
|
||||
@@ -242,6 +244,7 @@ class _WindowsDocView extends StatefulWidget {
|
||||
|
||||
class _WindowsDocViewState extends State<_WindowsDocView> {
|
||||
final WebviewController _controller = WebviewController();
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
bool _isInitialized = false;
|
||||
bool _hasError = false;
|
||||
String _errorMessage = '';
|
||||
@@ -255,13 +258,46 @@ class _WindowsDocViewState extends State<_WindowsDocView> {
|
||||
|
||||
Future<void> _initWebview() async {
|
||||
try {
|
||||
// 1. Verify WebView2 Runtime availability before calling initialize to prevent native C++ crash
|
||||
final version = await WebviewController.getWebViewVersion();
|
||||
if (version == null || version.trim().isEmpty) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
_errorMessage =
|
||||
'Microsoft Edge WebView2 Runtime is not installed on this computer.\n\n'
|
||||
'You can view the documentation in your default web browser or install the WebView2 Runtime.';
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Initialize environment with a writable user data path in LocalAppData
|
||||
// (prevents permission denied crashes when running from Program Files)
|
||||
try {
|
||||
final appSupport = await getApplicationSupportDirectory();
|
||||
final webviewDataDir = Directory(
|
||||
'${appSupport.path}${Platform.pathSeparator}tis_rms_webview2',
|
||||
);
|
||||
if (!await webviewDataDir.exists()) {
|
||||
await webviewDataDir.create(recursive: true);
|
||||
}
|
||||
await WebviewController.initializeEnvironment(
|
||||
userDataPath: webviewDataDir.path,
|
||||
);
|
||||
} catch (_) {
|
||||
// Environment may already be initialized; proceed safely
|
||||
}
|
||||
|
||||
// 3. Initialize controller
|
||||
await _controller.initialize();
|
||||
|
||||
_controller.title.listen((title) {
|
||||
_subscriptions.add(_controller.title.listen((title) {
|
||||
if (mounted) setState(() => _pageTitle = title);
|
||||
});
|
||||
}));
|
||||
|
||||
_controller.url.listen((url) {
|
||||
_subscriptions.add(_controller.url.listen((url) {
|
||||
if (!mounted) return;
|
||||
// Intercept external links clicked inside WebView
|
||||
if (!url.startsWith('http://127.0.0.1') &&
|
||||
!url.startsWith('http://localhost') &&
|
||||
@@ -269,11 +305,12 @@ class _WindowsDocViewState extends State<_WindowsDocView> {
|
||||
_controller.stop();
|
||||
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
await _controller.setBackgroundColor(
|
||||
widget.isDark ? const Color(0xFF161D19) : Colors.white,
|
||||
);
|
||||
await _controller.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny);
|
||||
await _controller.loadUrl(widget.url);
|
||||
|
||||
if (mounted) {
|
||||
@@ -286,8 +323,8 @@ class _WindowsDocViewState extends State<_WindowsDocView> {
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
_errorMessage =
|
||||
'Microsoft Edge WebView2 runtime error: $e.\n'
|
||||
'Please verify WebView2 Runtime is installed.';
|
||||
'Microsoft Edge WebView2 initialization error: $e.\n\n'
|
||||
'You can open the documentation in your web browser instead.';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -295,60 +332,130 @@ class _WindowsDocViewState extends State<_WindowsDocView> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
for (final sub in _subscriptions) {
|
||||
sub.cancel();
|
||||
}
|
||||
_subscriptions.clear();
|
||||
|
||||
if (_isInitialized) {
|
||||
try {
|
||||
_controller.dispose();
|
||||
} catch (e) {
|
||||
debugPrint('[DocumentationScreen] webview dispose error: $e');
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openExternal() async {
|
||||
try {
|
||||
await launchUrl(Uri.parse(widget.url), mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Could not open browser: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_hasError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, size: 48, color: Colors.orange),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'WebView2 Required',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.isDark ? Colors.white : AppColors.textPrimary,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
color: widget.isDark ? AppColors.darkSurfaceCard : Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.menu_book_rounded, size: 44, color: Colors.amber),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
'Documentation Viewer',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
_errorMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
height: 1.5,
|
||||
color: widget.isDark ? Colors.white70 : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _openExternal,
|
||||
icon: const Icon(Icons.open_in_browser_rounded),
|
||||
label: const Text('Open in Web Browser'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size.fromHeight(46),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse('https://developer.microsoft.com/en-us/microsoft-edge/webview2/'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.download_rounded),
|
||||
label: const Text('Download WebView2 Runtime'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(44),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_hasError = false;
|
||||
_isInitialized = false;
|
||||
});
|
||||
_initWebview();
|
||||
},
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: const Text('Retry In-App Viewer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_errorMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: widget.isDark ? Colors.white70 : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse('https://developer.microsoft.com/en-us/microsoft-edge/webview2/'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.download_rounded),
|
||||
label: const Text('Download WebView2 Runtime'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_isInitialized) {
|
||||
if (!_isInitialized || !_controller.value.isInitialized) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primaryGreen),
|
||||
);
|
||||
|
||||
@@ -683,45 +683,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen> with SingleTickerProv
|
||||
isLoading: isLoading,
|
||||
onPressed: _handleLogin,
|
||||
),
|
||||
const SizedBox(height: AppSizes.p12),
|
||||
// Server endpoint indicator & switcher
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: _showServerConfigDialog,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.dns_outlined,
|
||||
size: 13,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Server: ${ApiConstants.baseUrl}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
size: 12,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1812,34 +1812,95 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
final themeNotifier =
|
||||
ref.watch(themeModeProvider.notifier);
|
||||
final currentKey = themeNotifier.currentKey;
|
||||
final isSystem = currentKey == 'system';
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildThemeOptionChip(
|
||||
label: 'System',
|
||||
icon: Icons.brightness_auto_rounded,
|
||||
value: 'system',
|
||||
currentValue: currentKey,
|
||||
onSelect: () => themeNotifier
|
||||
.setThemeMode('system'),
|
||||
// System Theme Mode Switch Toggle
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.brightness_auto_rounded,
|
||||
color: isSystem
|
||||
? AppColors.primaryGreen
|
||||
: (isDark ? Colors.white54 : Colors.grey.shade600),
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: AppSizes.p12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'System Theme',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Match device system theme automatically',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark
|
||||
? AppColors.darkTextSecondary
|
||||
: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: isSystem,
|
||||
activeThumbColor: AppColors.primaryGreen,
|
||||
onChanged: (val) {
|
||||
if (val) {
|
||||
themeNotifier.setThemeMode('system');
|
||||
} else {
|
||||
final sysDark = MediaQuery.of(context).platformBrightness == Brightness.dark;
|
||||
themeNotifier.setThemeMode(sysDark ? 'dark' : 'light');
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildThemeOptionChip(
|
||||
label: 'Light',
|
||||
icon: Icons.light_mode_rounded,
|
||||
value: 'light',
|
||||
currentValue: currentKey,
|
||||
onSelect: () => themeNotifier
|
||||
.setThemeMode('light'),
|
||||
),
|
||||
_buildThemeOptionChip(
|
||||
label: 'Dark',
|
||||
icon: Icons.dark_mode_rounded,
|
||||
value: 'dark',
|
||||
currentValue: currentKey,
|
||||
onSelect: () => themeNotifier
|
||||
.setThemeMode('dark'),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
|
||||
// Light / Dark Theme selector (disabled/inactive when System is toggled on)
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: isSystem ? 0.38 : 1.0,
|
||||
child: IgnorePointer(
|
||||
ignoring: isSystem,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildThemeOptionCard(
|
||||
label: 'Light',
|
||||
icon: Icons.light_mode_rounded,
|
||||
value: 'light',
|
||||
currentValue: currentKey,
|
||||
isActive: !isSystem,
|
||||
onSelect: () => themeNotifier.setThemeMode('light'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildThemeOptionCard(
|
||||
label: 'Dark',
|
||||
icon: Icons.dark_mode_rounded,
|
||||
value: 'dark',
|
||||
currentValue: currentKey,
|
||||
isActive: !isSystem,
|
||||
onSelect: () => themeNotifier.setThemeMode('dark'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -2914,35 +2975,59 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeOptionChip({
|
||||
Widget _buildThemeOptionCard({
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required String value,
|
||||
required String currentValue,
|
||||
required bool isActive,
|
||||
required VoidCallback onSelect,
|
||||
}) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final isSelected = value == currentValue;
|
||||
return ChoiceChip(
|
||||
showCheckmark: false,
|
||||
avatar: Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: isSelected ? Colors.white : AppColors.primaryGreen,
|
||||
),
|
||||
label: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
final isSelected = isActive && (value == currentValue);
|
||||
|
||||
return InkWell(
|
||||
onTap: isActive ? onSelect : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
? AppColors.primaryGreen.withValues(alpha: isDark ? 0.2 : 0.1)
|
||||
: (isDark ? AppColors.darkSurface2 : AppColors.pageBackground),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColors.primaryGreen
|
||||
: (isDark ? Colors.white12 : Colors.grey.shade300),
|
||||
width: isSelected ? 1.8 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 19,
|
||||
color: isSelected
|
||||
? AppColors.primaryGreen
|
||||
: (isDark ? Colors.white70 : AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
|
||||
color: isSelected
|
||||
? AppColors.primaryGreen
|
||||
: (isDark ? Colors.white : AppColors.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
selectedColor: AppColors.primaryGreen,
|
||||
backgroundColor: isDark ? AppColors.darkSurface2 : AppColors.pageBackground,
|
||||
onSelected: (_) => onSelect(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class _SplashScreenState extends ConsumerState<SplashScreen>
|
||||
|
||||
Timer? _quoteTimer;
|
||||
int _quoteIndex = 0;
|
||||
String _connectionStatus = 'Connecting to server…';
|
||||
|
||||
static const List<String> _entertainingPhrases = [
|
||||
'Sharpening digital pencils…',
|
||||
@@ -107,23 +106,13 @@ class _SplashScreenState extends ConsumerState<SplashScreen>
|
||||
|
||||
Future<bool> _resolveServer({bool isRetry = false}) async {
|
||||
try {
|
||||
final found = await ServerDiscoveryService.resolveServerWithFallback(
|
||||
onProgress: (status) {
|
||||
if (mounted) setState(() => _connectionStatus = status);
|
||||
},
|
||||
).timeout(
|
||||
final found = await ServerDiscoveryService.resolveServerWithFallback()
|
||||
.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
|
||||
if (found != null) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_connectionStatus = found.contains('tis-rms')
|
||||
? 'Connected to Cloud Tunnel'
|
||||
: 'Connected to Local Server ($found)';
|
||||
});
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
return true;
|
||||
}
|
||||
@@ -430,46 +419,6 @@ class _SplashScreenState extends ConsumerState<SplashScreen>
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Real-time network connection status badge
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: isDark ? 0.15 : 0.08),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.25),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1.8,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_connectionStatus,
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryGreen,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user