Skip to content

Instantly share code, notes, and snippets.

@3bugs
Created June 23, 2025 03:02
Show Gist options
  • Select an option

  • Save 3bugs/712dc6ded4e14d044b91b845f5279594 to your computer and use it in GitHub Desktop.

Select an option

Save 3bugs/712dc6ded4e14d044b91b845f5279594 to your computer and use it in GitHub Desktop.
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