338 lines
9.4 KiB
Dart
338 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();
|
||
|
|
}
|
||
|
|
}
|