html content

This commit is contained in:
='fauz
2025-07-04 16:04:09 +07:00
parent a3c27a23b9
commit a211b0675d
41 changed files with 878 additions and 276 deletions
+105
View File
@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
class _ListDetailScreen extends StatefulWidget {
final String title;
final String? filepath;
const _ListDetailScreen({super.key, required this.title, this.filepath});
@override
State<_ListDetailScreen> createState() => _ListDetailScreenState();
}
class _ListDetailScreenState extends State<_ListDetailScreen> {
List<bool> checklist = [];
late WebViewController _controller;
late String? fPath = widget.filepath;
@override
void initState() {
super.initState();
_controller =
WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted);
checklist = List.generate(paragraphs.length, (index) => false);
if (fPath != null && fPath!.isNotEmpty) {
_loadContent(fPath!); // <- ini penting
}
}
final List<String> paragraphs = [
"Paragraf pertama tentang ${"widget.title"}.",
"Paragraf kedua dengan informasi tambahan.",
"Paragraf ketiga yang menjelaskan detail lebih lanjut.",
"Paragraf keempat yang melengkapi pembahasan.",
];
void _loadContent(String path) async {
try {
if (path.startsWith('http')) {
_controller.loadRequest(Uri.parse(path));
} else {
String fileHtml = await rootBundle.loadString(path);
_controller.loadHtmlString(fileHtml);
}
} catch (e) {
debugPrint("Error loading HTML: $e");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text(widget.title)),
body:
(fPath != null && fPath!.isNotEmpty)
? WebViewWidget(controller: _controller)
: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/default.png',
width: 100,
height: 100,
fit: BoxFit.cover,
),
const SizedBox(width: 16),
Expanded(
child: Text(
"Ini adalah detail dari ${widget.title}.",
style: const TextStyle(fontSize: 18),
),
),
],
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: paragraphs.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(paragraphs[index]),
trailing: Checkbox(
value: checklist[index],
onChanged: (bool? value) {
setState(() {
checklist[index] = value!;
});
},
),
);
},
),
),
],
),
),
);
}
}
+107 -77
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'dart:convert';
class ListDetailScreen extends StatefulWidget {
final String title;
@@ -13,89 +14,118 @@ class ListDetailScreen extends StatefulWidget {
}
class _ListDetailScreenState extends State<ListDetailScreen> {
List<bool> checklist = [];
late WebViewController _controller;
late String? fPath = widget.filepath;
@override
void initState() {
super.initState();
checklist = List.generate(paragraphs.length, (index) => false);
}
final List<String> paragraphs = [
"Paragraf pertama tentang ${"widget.title"}.",
"Paragraf kedua dengan informasi tambahan.",
"Paragraf ketiga yang menjelaskan detail lebih lanjut.",
"Paragraf keempat yang melengkapi pembahasan.",
];
void _loadContent(String path) async {
if (path.startsWith('http')) {
// Load dari Internet
_controller.loadRequest(Uri.parse(path));
} else {
// Load dari assets lokal
String fileHtml = await rootBundle.loadString(path);
_controller.loadHtmlString(fileHtml);
}
}
late InAppWebViewController _webViewController;
@override
Widget build(BuildContext context) {
String _getMimeType(String path) {
if (path.endsWith('.png')) return 'image/png';
if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return 'image/jpeg';
if (path.endsWith('.svg')) return 'image/svg+xml';
if (path.endsWith('.html')) return 'text/html';
if (path.endsWith('.css')) return 'text/css';
if (path.endsWith('.js')) return 'application/javascript';
return 'application/octet-stream';
}
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: Text(widget.title)),
body:
(fPath != null && fPath!.isNotEmpty)
? WebViewWidget(controller: _controller)
: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Gambar di kiri atas dengan teks di kanan
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
'assets/images/default.png', // Ganti dengan gambar yang sesuai
width: 100,
height: 100,
fit: BoxFit.cover,
),
const SizedBox(width: 16),
Expanded(
child: Text(
"Ini adalah detail dari ${widget.title}.",
style: const TextStyle(fontSize: 18),
),
),
],
),
const SizedBox(height: 20),
// Paragraf dengan checklist
Expanded(
child: ListView.builder(
itemCount: paragraphs.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(paragraphs[index]),
trailing: Checkbox(
value: checklist[index],
onChanged: (bool? value) {
setState(() {
checklist[index] = value!;
});
},
),
);
},
),
),
],
widget.filepath != null
? InAppWebView(
initialUrlRequest: URLRequest(url: WebUri("about:blank")),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
javaScriptEnabled: true,
allowFileAccessFromFileURLs: true,
allowUniversalAccessFromFileURLs: true,
),
),
),
onWebViewCreated: (controller) async {
_webViewController = controller;
final html = await rootBundle.loadString(widget.filepath!);
final base = widget.filepath!.substring(
0,
widget.filepath!.lastIndexOf('/') + 1,
);
await controller.loadData(
data: html,
baseUrl: WebUri(
"http://localhost/$base",
), // agar path relatif seperti img1/* jalan
mimeType: 'text/html',
encoding: 'utf-8',
);
},
onLoadStop: (controller, url) async {
debugPrint("🚀 onLoadStop called. URL: $url");
var result = await controller.evaluateJavascript(
source: """
(() => {
const board = document.getElementById('game-board');
const hasRender = typeof renderGrid === 'function';
return { hasRender, boardExists: board !== null };
})();
""",
);
debugPrint("📊 Evaluated JS result: $result");
final jsCheck = await controller.evaluateJavascript(
source: """
typeof renderGrid === 'function' &&
document.querySelector('#game-board') !== null
""",
);
debugPrint(
"Apakah renderGrid tersedia dan board muncul? => $jsCheck",
);
final imageLoaded = await controller.evaluateJavascript(
source: """
Array.from(document.images).every(img => img.complete)
""",
);
debugPrint("Semua gambar berhasil dimuat? => $imageLoaded");
},
// (Opsional) tangkap log dari JS
onConsoleMessage: (controller, consoleMessage) {
debugPrint("📢 JS Console: ${consoleMessage.message}");
},
shouldInterceptRequest: (controller, request) async {
final url = request.url;
if (url == null || url.host != 'localhost') return null;
try {
// Strip "http://localhost/" from the path
final path = url.path.replaceFirst('/', '');
final asset = await rootBundle.load(path);
final mimeType = _getMimeType(path);
return WebResourceResponse(
contentType: mimeType,
data: asset.buffer.asUint8List(),
statusCode: 200,
reasonPhrase: 'OK',
);
} catch (e) {
print("⚠️ Asset not found: ${url.path} -> $e");
return WebResourceResponse(
contentType: 'text/plain',
data: Uint8List.fromList(utf8.encode('404 Not Found')),
statusCode: 404,
reasonPhrase: 'Not Found',
);
}
},
)
: const Center(child: Text("Tidak ada file HTML")),
);
}
}
+87 -33
View File
@@ -6,12 +6,14 @@ import 'package:flutter/services.dart';
import 'package:freekake/components/card_list.dart';
import 'package:freekake/components/collection_container%20copy.dart';
import 'package:freekake/helpers/color_helper.dart';
import 'package:freekake/screen/pustaka/list_detail_screen.dart';
import 'package:list_detail_extension/list_detail_extension.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
class ListEducation extends StatefulWidget {
const ListEducation({super.key});
final String? selectedCategory;
const ListEducation({super.key, this.selectedCategory});
@override
State<ListEducation> createState() => _ListEducationState();
@@ -22,8 +24,7 @@ class _ListEducationState extends State<ListEducation> {
late final WebViewController _controller;
late String urlOrAssetPath;
bool _shouldShowWebView = false;
// load extension
String? selectedCategory;
final listDetailExtension = ListDetailExtension();
Future<void> _initExtension() async {
@@ -69,18 +70,55 @@ class _ListEducationState extends State<ListEducation> {
""",
"file": "assets/html/index.html",
},
{
"title": "Memori Game",
"image": "assets/html/freekake.png",
"color": "#cef1da",
"category": "Gizi",
"type": "Materi",
"point": "100",
"coint": "20",
"body":
"""Game untuk melatih daya ingat siswa, sehingga bisa diharapkan bisa
menemukan cara sendiri untuk mengingat sesuatu
""",
"file": "assets/html/memory/memory.html",
},
{
"title": "Find Words",
"image": "assets/html/freekake.png",
"color": "#cef1da",
"category": "Kesehatan",
"type": "Materi",
"point": "100",
"coint": "20",
"body": """
No Descripptions
""",
"file": "assets/html/findwords/findwords.html",
},
];
List<Map<String, dynamic>> filteredItems = [];
@override
void initState() {
super.initState();
filteredItems = List.from(_listContent);
selectedCategory = widget.selectedCategory;
print(selectedCategory);
if (selectedCategory != null && selectedCategory!.isNotEmpty) {
filteredItems =
_listContent
.where((item) => item["category"] == selectedCategory)
.toList();
} else {
filteredItems = List.from(_listContent);
}
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
_shouldShowWebView = true;
_controller =
WebViewController()..setJavaScriptMode(JavaScriptMode.unrestricted);
urlOrAssetPath = "assets/makanan_bergizi/index.html";
urlOrAssetPath = "assets/html/index.html";
_loadContent(urlOrAssetPath);
_initExtension();
}
@@ -92,18 +130,20 @@ class _ListEducationState extends State<ListEducation> {
// Load dari Internet
_controller.loadRequest(Uri.parse(path));
} else {
// Load dari assets lokal
// String fileHtml = await rootBundle.loadString(path);
String fileHtml = await DefaultAssetBundle.of(
context,
).loadString('assets/html/index.html');
_controller.loadHtmlString(fileHtml);
try {
await rootBundle.load(path);
await _controller.loadFlutterAsset(path);
} catch (e) {
print("Gagal memuat file HTML: $path\nError: $e");
}
_controller.loadFlutterAsset(path);
}
}
}
void filterSearch(String query) {
setState(() {
selectedCategory = null;
if (query.isEmpty) {
filteredItems = List.from(_listContent);
} else {
@@ -117,6 +157,7 @@ class _ListEducationState extends State<ListEducation> {
void filterByCategory(String category) {
setState(() {
selectedCategory = category;
filteredItems =
_listContent.where((item) {
return item["category"].toLowerCase() == category.toLowerCase();
@@ -126,7 +167,6 @@ class _ListEducationState extends State<ListEducation> {
@override
Widget build(BuildContext context) {
String searchQuery = "";
final List<Map<String, dynamic>> collections = [
{
"label": "Kesehatan",
@@ -158,7 +198,8 @@ class _ListEducationState extends State<ListEducation> {
collections
.where(
(item) => item["label"].toLowerCase().contains(
searchQuery.toLowerCase(),
// searchQuery.toLowerCase(),
searchController.text.toLowerCase(),
),
)
.toList();
@@ -176,7 +217,7 @@ class _ListEducationState extends State<ListEducation> {
body: Column(
children: [
// Header Section
headerContainer(context, filteredCollections),
headerContainer(context, collections),
// Collection Items dengan Filter
SizedBox(height: 20),
Expanded(child: contentContainer(context)),
@@ -255,6 +296,7 @@ class _ListEducationState extends State<ListEducation> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children:
filteredCollections.map((item) {
final isSelected = selectedCategory == item["category"];
return CollectionContainer(
label: item["label"]!,
imageSvg: item["image"]!,
@@ -262,9 +304,16 @@ class _ListEducationState extends State<ListEducation> {
iconh: 20,
width: 70,
height: 70,
textColor: Colors.black,
textColor:
isSelected
? const Color.fromARGB(255, 230, 48, 160)
: Colors.black,
lblSize: 10,
colorContiner: ColorHelper.hexToColor(item["color"]),
// colorContiner: ColorHelper.hexToColor(item["color"]),
colorContiner:
isSelected
? const Color.fromARGB(255, 84, 3, 224)
: ColorHelper.hexToColor(item["color"]),
onTapAc:
() => {
searchController.clear(),
@@ -347,36 +396,41 @@ class _ListEducationState extends State<ListEducation> {
GridView _generateList() {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1, // 2 Kolom
crossAxisCount: 1,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 2.5,
),
itemCount: _listContent.length,
itemCount: filteredItems.length,
itemBuilder: (context, index) {
return GestureDetector(
child: CardList(
title: _listContent[index]["title"] ?? "",
body: _listContent[index]["body"] ?? "",
gambar: _listContent[index]['image'],
point: _listContent[index]['coint'],
coint: _listContent[index]['point'],
tipe: _listContent[index]['tipe'],
title: filteredItems[index]["title"] ?? "",
body: filteredItems[index]["body"] ?? "",
gambar: filteredItems[index]['image'],
point: filteredItems[index]['point'],
coint: filteredItems[index]['coint'],
tipe: filteredItems[index]['type'],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => listDetailExtension.buildPage({
'title': filteredItems[index]['category'],
'imagePath': filteredItems[index]['image'],
'filepath': filteredItems[index]['file'],
'paragraphs':
"""
"""
"<h1> ${_listContent[index]["title"]} </h1> <p> ${_listContent[index]["body"]}",
}),
// (context) => listDetailExtension.buildPage({
// 'title': filteredItems[index]['category'],
// 'imagePath': filteredItems[index]['image'],
// 'filepath': filteredItems[index]['file'],
// 'paragraphs':
// """
// """
// "<h1> ${filteredItems[index]["title"]} </h1> <p> ${filteredItems[index]["body"]}",
// }),
(context) => ListDetailScreen(
title: filteredItems[index]['category'],
// 'imagePath': filteredItems[index]['image'],
filepath: filteredItems[index]['file'],
),
),
);
},
+1 -96
View File
@@ -74,106 +74,11 @@ class _ListDetailScreenState extends State<ListPustakaDetailScreen> {
),
),
),
// Padding(
// padding: const EdgeInsets.all(20.0),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Image.asset(
// widget.imagePath,
// width: 100,
// height: 100,
// fit: BoxFit.cover,
// ),
// const SizedBox(width: 16),
// Expanded(
// child: Center(
// child: Text(
// widget.title,
// style: const TextStyle(
// fontSize: 22,
// fontWeight: FontWeight.bold,
// color: Colors.black,
// ),
// ),
// ),
// ),
// ],
// ),
// ),
const SizedBox(height: 0),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(left: 18.0, right: 18, bottom: 18),
child:
// ListView.builder(
// itemCount: widget.paragraphs.length,
// itemBuilder: (context, index) {
// return
// Padding(
// padding: const EdgeInsets.only(bottom: 16.0),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Expanded(
// child:
// // Text(
// // widget.paragraphs[index] +
// // style: TextStyle(color: Colors.black),
// Html(
// // style: {
// // "body": Style(
// // color: const Color.fromRGBO(0, 0, 0, 1),
// // ),
// // },
// data:
// widget.paragraphs +
// """
// <html>
// <!-- Text between angle brackets is an HTML tag and is not displayed.
// Most tags, such as the HTML and /HTML tags that surround the contents of
// a page, come in pairs; some tags, like HR, for a horizontal rule, stand
// alone. Comments, such as the text you're reading, are not displayed when
// the Web page is shown. The information between the HEAD and /HEAD tags is
// not displayed. The information between the BODY and /BODY tags is displayed.-->
// <head>
// <title>Enter a title, displayed at the top of the window.</title>
// </head>
// <!-- The information between the BODY and /BODY tags is displayed.-->
// <body>
// <h1>Enter the main heading, usually the same as the title.</h1>
// <p>Be <b>bold</b> in stating your key points. Put them in a list: </p>
// <ul>
// <li>The first item in your list</li>
// <li>The second item; <i>italicize</i> key words</li>
// </ul>
// <p>Improve your image by including an image. </p>
// <p><img src="http://www.mygifs.com/CoverImage.gif" alt="A Great HTML Resource"></p>
// <p>Add a link to your favorite <a href="https://www.dummies.com/">Web site</a>.
// Break up your page with a horizontal rule or two. </p>
// <hr>
// <p>Finally, link to <a href="page2.html">another page</a> in your own Web site.</p>
// <!-- And add a copyright notice.-->
// <p>© Wiley Publishing, 2011</p>
// </body>
// </html>
// """,
// ),
// ),
// // ),
// // Checkbox(
// // value: _readStatus[index],
// // onChanged: (bool? value) {
// // _toggleReadStatus(index);
// // },
// // ),
// ],
// ),
// );
// },
// ),
htmlBody(),
child: htmlBody(),
),
),
],