Created
June 23, 2025 03:02
-
-
Save 3bugs/712dc6ded4e14d044b91b845f5279594 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:flutter_inappwebview/flutter_inappwebview.dart'; | |
| import 'package:google_fonts/google_fonts.dart'; | |
| import 'package:shared_preferences/shared_preferences.dart'; | |
| import 'package:url_launcher/url_launcher.dart'; | |
| void main() { | |
| WidgetsFlutterBinding.ensureInitialized(); | |
| SystemChrome.setSystemUIOverlayStyle( | |
| const SystemUiOverlayStyle( | |
| statusBarColor: | |
| Color.fromRGBO(76, 78, 79, 1.0), // Set your desired color here | |
| statusBarIconBrightness: Brightness.light, // For white icons | |
| ), | |
| ); | |
| runApp(const MyApp()); | |
| } | |
| const primaryColor = Color.fromRGBO(51, 74, 152, 1.0); | |
| class MyApp extends StatelessWidget { | |
| const MyApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| title: 'DISPLAY', | |
| theme: ThemeData( | |
| colorScheme: ColorScheme.fromSeed( | |
| seedColor: primaryColor, | |
| primary: primaryColor, | |
| onPrimary: Colors.white, | |
| secondary: primaryColor, | |
| onSecondary: Colors.white, | |
| surface: Colors.white, | |
| onSurface: Colors.black, | |
| ), | |
| useMaterial3: true, | |
| fontFamily: GoogleFonts.notoSansThai().fontFamily, | |
| ), | |
| home: const WebViewPage(), | |
| ); | |
| } | |
| } | |
| class WebViewPage extends StatefulWidget { | |
| const WebViewPage({super.key}); | |
| @override | |
| State<WebViewPage> createState() => _WebViewPageState(); | |
| } | |
| const String baseUrl = "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxx.com/Display?room="; | |
| const List<ConferenceRoom> conferenceRooms = [ | |
| ConferenceRoom(url: '${baseUrl}Room1', name: '6-01 ห้องประชุมอธิบดี'), | |
| ConferenceRoom( | |
| url: '${baseUrl}Room2', | |
| name: '5-02 ห้องประชุมสำนักงานช่วยเหลือทางการเงินฯ'), | |
| ConferenceRoom( | |
| url: '${baseUrl}Room3', name: '5-03 ห้องประชุมกองพิทักษ์สิทธิและเสรีภาพ'), | |
| ConferenceRoom( | |
| url: '${baseUrl}Room4', name: '5-04 ห้องประชุมสำนักคุ้มครองพยาน'), | |
| ConferenceRoom( | |
| url: '${baseUrl}Room6', | |
| name: '5-05 ห้องประชุมกองส่งเสริมสิทธิและเสรีภาพ'), | |
| ConferenceRoom( | |
| url: '${baseUrl}Room5', | |
| name: '5-06 ห้องประชุมกองส่งเสริมการระงับข้อพิพาท'), | |
| ConferenceRoom(url: '${baseUrl}Room7', name: '6-11 ห้องประชุม สลก.'), | |
| ConferenceRoom(url: '${baseUrl}Room8', name: '6-03 ห้องประชุม สลก2'), | |
| ]; | |
| class _WebViewPageState extends State<WebViewPage> { | |
| static const String keyPrefsUrl = 'saved_url'; | |
| static const String keyPrefsPageScale = 'page_scale'; | |
| InAppWebViewController? _webViewController; | |
| String? _url; | |
| bool _isLoading = true; | |
| double _pageScale = 1.0; | |
| List<String> _customUrlList = []; | |
| List<ConferenceRoom> _allRooms = []; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _loadCustomUrlListFromPrefs().then((urls) { | |
| _customUrlList = urls; | |
| // _combineRoomsWithCustomUrls(); | |
| _loadPageScaleFromPrefs().then((scale) { | |
| setState(() { | |
| _pageScale = scale; | |
| _initUrl(); | |
| }); | |
| }); | |
| }); | |
| } | |
| Future<void> _initUrl() async { | |
| final String url = await _loadUrlFromPrefs(); | |
| setState(() { | |
| _url = url; | |
| }); | |
| if (url.isEmpty) { | |
| // If no URL is saved, open settings dialog to set it | |
| WidgetsBinding.instance.addPostFrameCallback((_) { | |
| _openSettings(); | |
| }); | |
| } | |
| } | |
| /// Combine predefined rooms with custom URLs | |
| void _combineRoomsWithCustomUrls() { | |
| _allRooms = [ | |
| ...conferenceRooms, | |
| ..._customUrlList.map((url) { | |
| return ConferenceRoom(url: url, name: url); | |
| }), | |
| ]; | |
| } | |
| Future<void> _saveUrlToPrefs(String url) async { | |
| final prefs = await SharedPreferences.getInstance(); | |
| await prefs.setString(keyPrefsUrl, url); | |
| } | |
| Future<String> _loadUrlFromPrefs() async { | |
| final prefs = await SharedPreferences.getInstance(); | |
| return prefs.getString(keyPrefsUrl) ?? ''; | |
| } | |
| Future<void> _savePageScaleToPrefs(double scale) async { | |
| final prefs = await SharedPreferences.getInstance(); | |
| await prefs.setDouble(keyPrefsPageScale, scale); | |
| } | |
| Future<double> _loadPageScaleFromPrefs() async { | |
| final prefs = await SharedPreferences.getInstance(); | |
| return prefs.getDouble(keyPrefsPageScale) ?? 1.0; | |
| } | |
| Future<void> _saveCustomUrlListToPrefs(List<String> urls) async { | |
| final prefs = await SharedPreferences.getInstance(); | |
| await prefs.setStringList('saved_url_list', urls); | |
| } | |
| Future<List<String>> _loadCustomUrlListFromPrefs() async { | |
| final prefs = await SharedPreferences.getInstance(); | |
| return prefs.getStringList('saved_url_list') ?? []; | |
| } | |
| ConferenceRoom? _getMatchedRoomFromUrl(String url) { | |
| ConferenceRoom? matchedRoom; | |
| try { | |
| matchedRoom = _allRooms.firstWhere( | |
| (room) => room.url == url.trim(), | |
| orElse: () => throw Exception('No matching room found'), | |
| ); | |
| } catch (e) { | |
| matchedRoom = null; | |
| } | |
| return matchedRoom; | |
| } | |
| void _openSettings() async { | |
| _combineRoomsWithCustomUrls(); | |
| final String savedRoomUrl = await _loadUrlFromPrefs(); | |
| // Extract room ID from saved URL if it exists | |
| // String? roomId; | |
| // if (savedUrl.isNotEmpty) { | |
| // final uri = Uri.parse(savedUrl); | |
| // roomId = uri.queryParameters['room']; | |
| // } | |
| final matchedRoom = _getMatchedRoomFromUrl(savedRoomUrl); | |
| if (!mounted) return; | |
| showDialog( | |
| context: context, | |
| builder: (context) { | |
| debugPrint('🟠 Custom URL List: $_customUrlList'); | |
| final textTheme = Theme.of(context).textTheme; | |
| String? dropdownValue = matchedRoom?.url; | |
| final textController = TextEditingController( | |
| text: savedRoomUrl, | |
| ); | |
| final formKey = GlobalKey<FormState>(); | |
| handleClickSave() { | |
| if (formKey.currentState!.validate()) { | |
| final url = textController.text.trim(); | |
| _saveUrlToPrefs(url); | |
| _webViewController?.loadUrl( | |
| urlRequest: URLRequest( | |
| url: WebUri(url), | |
| ), | |
| ); | |
| setState(() { | |
| _url = url; | |
| }); | |
| final matchedRoom = _getMatchedRoomFromUrl(url); | |
| if (matchedRoom == null) { | |
| _customUrlList.add(url); | |
| _saveCustomUrlListToPrefs(_customUrlList); | |
| } | |
| Navigator.of(context).pop(); | |
| } | |
| } | |
| return StatefulBuilder( | |
| builder: (context, dialogSetState) { | |
| return LayoutBuilder(builder: (context, constraints) { | |
| final isSmallScreen = constraints.maxWidth < 600; | |
| return AlertDialog( | |
| insetPadding: isSmallScreen | |
| ? const EdgeInsets.symmetric(horizontal: 0.0) | |
| : null, | |
| actionsPadding: isSmallScreen ? EdgeInsets.zero : null, | |
| contentPadding: EdgeInsets.fromLTRB( | |
| isSmallScreen ? 12.0 : 48.0, | |
| isSmallScreen ? 24.0 : 32.0, | |
| isSmallScreen ? 12.0 : 48.0, | |
| isSmallScreen ? 24.0 : 32.0, | |
| ), | |
| title: const Row( | |
| children: [ | |
| Icon(Icons.settings, size: 28), | |
| SizedBox(width: 8), | |
| Text('ตั้งค่า URL', | |
| style: TextStyle(fontWeight: FontWeight.bold)), | |
| ], | |
| ), | |
| content: SizedBox( | |
| width: 600, | |
| child: Form( | |
| key: formKey, | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| crossAxisAlignment: CrossAxisAlignment.stretch, | |
| children: [ | |
| Text('เลือกห้องประชุม หรือกรอก URL ที่ต้องการ', | |
| style: textTheme.titleMedium), | |
| const SizedBox(height: 16), | |
| // Dropdown for selecting conference room | |
| DropdownButton<String>( | |
| value: dropdownValue, | |
| hint: const Text('เลือกห้อง'), | |
| items: _allRooms.map((ConferenceRoom room) { | |
| return DropdownMenuItem<String>( | |
| value: room.url, | |
| child: Padding( | |
| padding: const EdgeInsets.symmetric( | |
| horizontal: 16.0, vertical: 8.0), | |
| child: Text(room.name), | |
| ), | |
| ); | |
| }).toList(), | |
| isExpanded: true, | |
| onChanged: (String? newValue) { | |
| debugPrint("Selected room's URL: $newValue"); | |
| dialogSetState(() { | |
| dropdownValue = newValue!; | |
| textController.text = newValue; | |
| }); | |
| }, | |
| ), | |
| if (_customUrlList.isNotEmpty) | |
| Center( | |
| child: TextButton( | |
| onPressed: () { | |
| // Show confirmation dialog before clearing | |
| showDialog( | |
| context: context, | |
| builder: (context) { | |
| return AlertDialog( | |
| title: const Text('ยืนยันการล้าง URL'), | |
| content: const Text( | |
| 'คุณต้องการล้าง URL ที่กรอกเองทั้งหมดออกจากรายการหรือไม่?'), | |
| actions: [ | |
| TextButton( | |
| onPressed: () { | |
| Navigator.of(context).pop(); | |
| }, | |
| child: const Text('ยกเลิก'), | |
| ), | |
| TextButton( | |
| onPressed: () { | |
| _customUrlList.clear(); | |
| _saveCustomUrlListToPrefs( | |
| _customUrlList); | |
| Navigator.of(context).pop(); | |
| Navigator.of(context).pop(); | |
| Future.delayed( | |
| const Duration(milliseconds: 100), | |
| () => _openSettings(), | |
| ); | |
| }, | |
| child: const Text('ยืนยัน'), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| // _customUrlList.clear(); | |
| // _saveCustomUrlListToPrefs(_customUrlList); | |
| // Navigator.of(context).pop(); | |
| // _openSettings(); | |
| }, | |
| child: Text('ล้าง URL ที่กรอกเอง ออกจากรายการ', | |
| style: textTheme.bodySmall | |
| ?.copyWith(color: Colors.red)), | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| // Text field for entering URL | |
| TextFormField( | |
| controller: textController, | |
| decoration: InputDecoration( | |
| filled: true, | |
| fillColor: Colors.yellow.shade50, | |
| border: OutlineInputBorder( | |
| borderRadius: const BorderRadius.all( | |
| Radius.circular(12.0), | |
| ), | |
| borderSide: BorderSide( | |
| color: Colors.grey.shade300, | |
| width: 1.0, | |
| ), | |
| ), | |
| enabledBorder: OutlineInputBorder( | |
| borderRadius: const BorderRadius.all( | |
| Radius.circular(12.0), | |
| ), | |
| borderSide: BorderSide( | |
| color: Colors.grey.shade300, | |
| width: 1.0, | |
| ), | |
| ), | |
| focusedBorder: const OutlineInputBorder( | |
| borderRadius: BorderRadius.all( | |
| Radius.circular(12.0), | |
| ), | |
| borderSide: BorderSide( | |
| color: primaryColor, | |
| width: 2.0, | |
| ), | |
| ), | |
| hintText: 'URL', | |
| hintStyle: TextStyle(color: Colors.grey.shade600), | |
| suffixIcon: textController.text.isNotEmpty | |
| ? IconButton( | |
| icon: Icon(Icons.clear, | |
| color: Colors.red.shade600), | |
| onPressed: () { | |
| dialogSetState(() { | |
| textController.clear(); | |
| // Reset dropdownValue | |
| dropdownValue = null; | |
| }); | |
| }, | |
| ) | |
| : null, | |
| ), | |
| validator: (value) { | |
| final urlRegExp = RegExp( | |
| r'^(https?:\/\/)' // http:// or https:// | |
| r'(' | |
| r'([\w\-]+\.)+[\w\-]+' // domain | |
| r'|' // or | |
| r'(\d{1,3}\.){3}\d{1,3}' // IPv4 | |
| r')' | |
| r'(:\d+)?' // optional port | |
| r'(\/[^\s]*)?$', // optional path | |
| caseSensitive: false, | |
| ); | |
| if (value == null || value.trim().isEmpty) { | |
| return 'กรุณากรอก URL'; | |
| } | |
| final url = value.trim(); | |
| if (!urlRegExp.hasMatch(url)) { | |
| return 'รูปแบบ URL ไม่ถูกต้อง'; | |
| } | |
| return null; | |
| }, | |
| onFieldSubmitted: (value) { | |
| handleClickSave(); | |
| }, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| actions: [ | |
| Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| TextButton.icon( | |
| onPressed: () { | |
| Navigator.of(context).pop(); | |
| }, | |
| icon: Padding( | |
| padding: const EdgeInsets.all(4.0), | |
| child: Icon(Icons.cancel, color: Colors.red.shade600), | |
| ), | |
| label: Padding( | |
| padding: | |
| const EdgeInsets.fromLTRB(0.0, 16.0, 16.0, 16.0), | |
| child: Text('ยกเลิก', | |
| style: TextStyle(color: Colors.red.shade600)), | |
| ), | |
| ), | |
| TextButton.icon( | |
| onPressed: handleClickSave, | |
| icon: const Padding( | |
| padding: EdgeInsets.all(4.0), | |
| child: Icon(Icons.save, color: primaryColor), | |
| ), | |
| label: const Padding( | |
| padding: EdgeInsets.fromLTRB(0.0, 16.0, 16.0, 16.0), | |
| child: Text('บันทึก', | |
| style: TextStyle( | |
| color: primaryColor, | |
| fontWeight: FontWeight.bold)), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ); | |
| }); | |
| }, | |
| ); | |
| }, | |
| ); | |
| } | |
| Future<void> _setWebPageScale(double scale) async { | |
| _pageScale = scale; | |
| await _webViewController?.evaluateJavascript( | |
| source: "document.body.style.zoom = '$_pageScale';"); | |
| await _savePageScaleToPrefs(_pageScale); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final textTheme = Theme.of(context).textTheme; | |
| final logo = Image.asset( | |
| 'assets/images/logo.png', | |
| height: 200, | |
| ); | |
| final logoWithLoading = Center( | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| logo, | |
| const SizedBox(height: 32), | |
| const CircularProgressIndicator(), | |
| ], | |
| ), | |
| ); | |
| return Scaffold( | |
| backgroundColor: Colors.black, | |
| body: SafeArea( | |
| child: _url == null | |
| ? logoWithLoading | |
| : Stack( | |
| children: [ | |
| InAppWebView( | |
| initialUrlRequest: URLRequest( | |
| url: _url!.isEmpty ? null : WebUri(_url!), | |
| ), | |
| onWebViewCreated: (controller) { | |
| _webViewController = controller; | |
| }, | |
| onLoadStart: (controller, url) { | |
| if (_isLoading) return; | |
| setState(() { | |
| _isLoading = true; | |
| }); | |
| }, | |
| onLoadStop: (controller, url) async { | |
| Future.delayed(const Duration(seconds: 2), () { | |
| setState(() { | |
| _isLoading = false; | |
| }); | |
| }); | |
| await _setWebPageScale(_pageScale); | |
| }, | |
| onReceivedServerTrustAuthRequest: | |
| (controller, challenge) async { | |
| return ServerTrustAuthResponse( | |
| action: ServerTrustAuthResponseAction.PROCEED, | |
| ); | |
| }, | |
| ), | |
| if (_isLoading) logoWithLoading, | |
| if (_url!.isEmpty) | |
| Container( | |
| width: double.infinity, | |
| color: Colors.black, | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
| children: [ | |
| logo, | |
| ElevatedButton.icon( | |
| onPressed: _openSettings, | |
| icon: const Icon(Icons.settings, size: 28), | |
| label: Padding( | |
| padding: | |
| const EdgeInsets.symmetric(vertical: 12.0), | |
| child: Text('ตั้งค่า URL', | |
| style: textTheme.titleLarge?.copyWith( | |
| fontWeight: FontWeight.bold, | |
| color: primaryColor, | |
| )), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| // if (_url!.isEmpty) | |
| // Positioned( | |
| // top: 32, | |
| // left: 0, | |
| // right: 0, | |
| // child: Image.asset( | |
| // 'assets/images/logo.png', | |
| // height: 200, | |
| // ), | |
| // ), | |
| Positioned( | |
| bottom: 0, | |
| left: 0, | |
| right: 0, | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| // About button | |
| Container( | |
| margin: const EdgeInsets.only(bottom: 8, left: 8), | |
| decoration: BoxDecoration( | |
| color: const Color.fromRGBO(76, 78, 79, 1.0) | |
| .withOpacity(0.4), | |
| shape: BoxShape.circle, | |
| ), | |
| child: IconButton( | |
| icon: const Icon(Icons.info_outline, | |
| color: Colors.white), | |
| onPressed: () { | |
| showGeneralDialog( | |
| context: context, | |
| pageBuilder: (_, __, ___) { | |
| return AlertDialog( | |
| title: const Text('เกี่ยวกับแอปพลิเคชัน', | |
| style: TextStyle( | |
| fontWeight: FontWeight.bold)), | |
| actionsPadding: const EdgeInsets.fromLTRB( | |
| 16.0, | |
| 0.0, | |
| 16.0, | |
| 16.0, | |
| ), | |
| content: const AboutDialogBody(), | |
| actions: [ | |
| TextButton( | |
| onPressed: () { | |
| Navigator.of(context).pop(); | |
| }, | |
| child: const Text('ปิด'), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| }, | |
| ), | |
| ), | |
| Row( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| // Scale up | |
| Container( | |
| margin: | |
| const EdgeInsets.only(bottom: 8, right: 8), | |
| decoration: BoxDecoration( | |
| color: const Color.fromRGBO(76, 78, 79, 1.0) | |
| .withOpacity(0.4), | |
| shape: BoxShape.circle, | |
| ), | |
| child: IconButton( | |
| icon: | |
| const Icon(Icons.add, color: Colors.white), | |
| onPressed: () async { | |
| // await _webViewController.zoomIn(); | |
| if (_pageScale < 3.0) { | |
| _pageScale += 0.1; | |
| await _setWebPageScale(_pageScale); | |
| } | |
| }, | |
| ), | |
| ), | |
| // Scale down | |
| Container( | |
| margin: | |
| const EdgeInsets.only(bottom: 8, right: 8), | |
| decoration: BoxDecoration( | |
| color: const Color.fromRGBO(76, 78, 79, 1.0) | |
| .withOpacity(0.4), | |
| shape: BoxShape.circle, | |
| ), | |
| child: IconButton( | |
| icon: const Icon(Icons.remove, | |
| color: Colors.white), | |
| onPressed: () async { | |
| // await _webViewController.zoomOut(); | |
| if (_pageScale > 0.5) { | |
| _pageScale -= 0.1; | |
| await _setWebPageScale(_pageScale); | |
| } | |
| }, | |
| ), | |
| ), | |
| // Scale reset | |
| Container( | |
| margin: | |
| const EdgeInsets.only(bottom: 8, right: 8), | |
| decoration: BoxDecoration( | |
| color: const Color.fromRGBO(76, 78, 79, 1.0) | |
| .withOpacity(0.4), | |
| shape: BoxShape.circle, | |
| ), | |
| child: IconButton( | |
| icon: const Icon(Icons.fullscreen, | |
| color: Colors.white), | |
| onPressed: () async { | |
| _pageScale = 1.0; | |
| await _setWebPageScale(_pageScale); | |
| // await _webViewController?.reload(); | |
| }, | |
| ), | |
| ), | |
| const SizedBox(width: 8), | |
| // Reload button | |
| Container( | |
| margin: | |
| const EdgeInsets.only(bottom: 8, right: 8), | |
| decoration: BoxDecoration( | |
| color: const Color.fromRGBO(76, 78, 79, 1.0) | |
| .withOpacity(0.4), | |
| shape: BoxShape.circle, | |
| ), | |
| child: IconButton( | |
| icon: const Icon(Icons.refresh, | |
| color: Colors.white), | |
| onPressed: () async { | |
| if (_isLoading) return; | |
| await _webViewController?.reload(); | |
| }, | |
| ), | |
| ), | |
| const SizedBox(width: 8), | |
| // Settings button | |
| Container( | |
| margin: | |
| const EdgeInsets.only(bottom: 8, right: 8), | |
| decoration: BoxDecoration( | |
| color: const Color.fromRGBO(76, 78, 79, 1.0) | |
| .withOpacity(0.4), | |
| shape: BoxShape.circle, | |
| ), | |
| child: IconButton( | |
| icon: const Icon(Icons.settings, | |
| color: Colors.white), | |
| onPressed: _openSettings, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class AboutDialogBody extends StatelessWidget { | |
| const AboutDialogBody({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| final textTheme = Theme.of(context).textTheme; | |
| return SingleChildScrollView( | |
| child: ListBody( | |
| children: [ | |
| Row( | |
| children: [ | |
| ClipRRect( | |
| borderRadius: | |
| BorderRadius.circular(12.0), // Adjust the radius as needed | |
| child: Image.asset( | |
| 'assets/images/play_store_512.png', | |
| height: 64, | |
| width: 64, | |
| fit: BoxFit.cover, | |
| ), | |
| ), | |
| const SizedBox(width: 16), | |
| Expanded( | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| 'DISPLAY 1.0.0', | |
| style: textTheme.titleMedium?.copyWith( | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox(height: 4), | |
| Text( | |
| 'แอปพลิเคชันสำหรับแสดงหน้าเว็บเต็มจอบนอุปกรณ์ Android', | |
| style: textTheme.bodyMedium, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 24), | |
| Text( | |
| 'พัฒนาโดย พร้อมเลิศ หล่อวิจิตร', | |
| style: textTheme.bodyMedium, | |
| ), | |
| const SizedBox(height: 12), | |
| Row( | |
| children: [ | |
| Image.asset( | |
| 'assets/images/ic_web.png', | |
| height: 24, | |
| width: 24, | |
| ), | |
| const SizedBox(width: 8), | |
| Text( | |
| 'Website: ', | |
| style: textTheme.bodyMedium, | |
| ), | |
| GestureDetector( | |
| onTap: () async { | |
| // You can use url_launcher for external browser, or open in your webview | |
| const url = 'https://www.3bugs.com/'; | |
| if (await canLaunchUrl(Uri.parse(url))) { | |
| await launchUrl(Uri.parse(url), | |
| mode: LaunchMode.externalApplication); | |
| } | |
| }, | |
| child: Text( | |
| 'https://www.3bugs.com/', | |
| style: textTheme.bodyMedium?.copyWith( | |
| color: Colors.blue, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 8), | |
| Row( | |
| children: [ | |
| Image.asset( | |
| 'assets/images/ic_line.png', | |
| height: 24, | |
| width: 24, | |
| ), | |
| const SizedBox(width: 8), | |
| Text( | |
| 'LINE ID: ', | |
| style: textTheme.bodyMedium, | |
| ), | |
| GestureDetector( | |
| onTap: () async { | |
| const url = 'https://line.me/ti/p/GTP7I6E8zd'; | |
| if (await canLaunchUrl(Uri.parse(url))) { | |
| await launchUrl(Uri.parse(url), | |
| mode: LaunchMode.externalApplication); | |
| } | |
| }, | |
| child: Text( | |
| 'promlert', | |
| style: textTheme.bodyMedium?.copyWith( | |
| color: Colors.blue, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class ConferenceRoom { | |
| final String url; | |
| final String name; | |
| const ConferenceRoom({required this.url, required this.name}); | |
| @override | |
| String toString() => name; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment