mnemo_cards/funny_letters/example/lib/main.dart
2025-11-11 02:55:41 +03:00

337 lines
9.4 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Funny Letters',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const WebViewPage(),
);
}
}
class WebViewPage extends StatefulWidget {
const WebViewPage({super.key});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
late final WebViewController _controller;
bool _isDarkMode = false;
bool _isMusicEnabled = true;
bool _isSoundEnabled = true;
String _currentUrl = 'http://192.168.31.193:8888';
final TextEditingController _urlController = TextEditingController();
bool _isEditingUrl = false;
late final SharedPreferences _prefs;
@override
void initState() {
super.initState();
_loadSavedUrl();
}
Future<void> _loadSavedUrl() async {
await _initializeWebView();
_prefs = await SharedPreferences.getInstance();
setState(() {
_currentUrl = _prefs.getString('webAppUrl') ?? _currentUrl;
_urlController.text = _currentUrl;
});
}
Future<void> _saveUrl(String url) async {
await _prefs.setString('webAppUrl', url);
}
Future<void> _initializeWebView() async {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'FunnyLettersChannel',
onMessageReceived: _handleJavaScriptMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('funny_letters://')) {
_handleFunnyLettersScheme(request.url);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onPageFinished: (String url) {
// Inject the sendMessageToHost function after the page loads
_injectJavaScript();
},
),
)
..loadRequest(Uri.parse(_currentUrl));
}
Future<void> _reloadWebView() async {
setState(() {
_currentUrl = _urlController.text;
_isEditingUrl = false;
});
await _saveUrl(_currentUrl);
_controller.reload();
}
Future<void> _injectJavaScript() async {
await _controller.runJavaScript('''
window.sendMessageToHost = function(message) {
FunnyLettersChannel.postMessage(message);
};
''');
}
void _handleJavaScriptMessage(JavaScriptMessage message) async {
try {
final data = jsonDecode(message.message);
final type = data['type'] as String;
final payload = data['data'] as Map<String, dynamic>;
// json or array
dynamic response = null;
switch (type) {
case 'playWord':
final word = payload['word'] as String;
final lang = payload['lang'] as String?;
await _playWord(word, lang);
response = {'success': true};
break;
case 'loadFileBytes':
final path = payload['path'] as String;
final bytes = await _loadFileBytes(path);
response = base64Encode(bytes);
break;
case 'getCustomBackground':
final word = payload['word'] as String;
final background = await _getCustomBackground(word);
response = background;
break;
case 'loadSettings':
response = {
'musicEnabled': _isMusicEnabled,
'soundEnabled': _isSoundEnabled,
'isDarkMode': _isDarkMode,
};
break;
case 'loadWordsWithTranslations':
final words = await _loadWordsWithTranslations();
response = words;
break;
case 'setMusicEnabled':
setState(() {
_isMusicEnabled = payload['enabled'] as bool;
});
response = {'success': true};
break;
case 'setSoundEnabled':
setState(() {
_isSoundEnabled = payload['enabled'] as bool;
});
response = {'success': true};
break;
case 'getMusicEnabled':
response = {'enabled': _isMusicEnabled};
break;
case 'getSoundEnabled':
response = {'enabled': _isSoundEnabled};
break;
case 'setDarkMode':
setState(() {
_isDarkMode = payload['enabled'] as bool;
});
response = {'success': true};
break;
case 'getDarkMode':
response = {'enabled': _isDarkMode};
break;
}
// Send response back to web app
await _controller.runJavaScript('''
if (window.handleMessageFromHost) {
window.handleMessageFromHost('${jsonEncode({'type': type, 'data': response}).replaceAll("'", "\\'")}');
}
''');
} catch (e) {
print('Error handling message: $e');
// Send error response back to web app
await _controller.runJavaScript('''
if (window.handleMessageFromHost) {
window.handleMessageFromHost('${jsonEncode({'type': 'error', 'error': e.toString()}).replaceAll("'", "\\'")}');
}
''');
}
}
Future<void> _playWord(String word, String? lang) async {
// TODO: Implement word playback
print('Playing word: $word${lang != null ? " in $lang" : ""}');
}
Future<List<int>> _loadFileBytes(String path) async {
if (path == 'bg.png') {
return rootBundle
.load('assets/bg.png')
.then((value) => value.buffer.asUint8List());
}
print('file not found: $path');
return [];
}
Future<String?> _getCustomBackground(String word) async {
return 'bg.png';
}
Future<List<Map<String, dynamic>>> _loadWordsWithTranslations() async {
return [
{'word': 'hello', 'translation': 'привет', 'lang': 'ru'},
{'word': 'world', 'translation': 'мир', 'lang': 'ru'},
];
}
void _handleFunnyLettersScheme(String url) async {
final uri = Uri.parse(url);
final params = uri.queryParameters;
final funnyLettersUrl = Uri(
scheme: 'funny_letters',
host: 'open',
queryParameters: params,
);
try {
if (await canLaunchUrl(funnyLettersUrl)) {
await launchUrl(funnyLettersUrl);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not launch funny_letters app')),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: _isEditingUrl
? Row(
children: [
Expanded(
child: TextField(
controller: _urlController,
decoration: InputDecoration(
hintText: 'Enter URL',
suffixIcon: IconButton(
icon: const Icon(Icons.check),
onPressed: _reloadWebView,
),
),
onSubmitted: (_) => _reloadWebView(),
),
),
],
)
: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_isEditingUrl = true;
});
},
child: Text(
_currentUrl,
style: Theme.of(context).textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () async {
return _controller.reload();
},
),
IconButton(
icon: Icon(_isDarkMode ? Icons.light_mode : Icons.dark_mode),
onPressed: () {
setState(() {
_isDarkMode = !_isDarkMode;
});
},
),
IconButton(
icon: Icon(_isMusicEnabled ? Icons.music_note : Icons.music_off),
onPressed: () {
setState(() {
_isMusicEnabled = !_isMusicEnabled;
});
},
),
IconButton(
icon: Icon(_isSoundEnabled ? Icons.volume_up : Icons.volume_off),
onPressed: () {
setState(() {
_isSoundEnabled = !_isSoundEnabled;
});
},
),
],
),
body: WebViewWidget(controller: _controller),
);
}
@override
void dispose() {
_urlController.dispose();
super.dispose();
}
}