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 createState() => _WebViewPageState(); } class _WebViewPageState extends State { 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 _loadSavedUrl() async { await _initializeWebView(); _prefs = await SharedPreferences.getInstance(); setState(() { _currentUrl = _prefs.getString('webAppUrl') ?? _currentUrl; _urlController.text = _currentUrl; }); } Future _saveUrl(String url) async { await _prefs.setString('webAppUrl', url); } Future _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 _reloadWebView() async { setState(() { _currentUrl = _urlController.text; _isEditingUrl = false; }); await _saveUrl(_currentUrl); _controller.reload(); } Future _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; // 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 _playWord(String word, String? lang) async { // TODO: Implement word playback print('Playing word: $word${lang != null ? " in $lang" : ""}'); } Future> _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 _getCustomBackground(String word) async { return 'bg.png'; } Future>> _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(); } }