generated from dellevin/template
优化编辑新增界面
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'epub_stream_service.dart';
|
||||
import '../../utils/image_path_helper.dart';
|
||||
@@ -99,6 +100,8 @@ class EpubWebViewHandler {
|
||||
required WebUri requestUrl,
|
||||
}) async {
|
||||
try {
|
||||
debugPrint('[EPUB-Handler] customScheme: $requestUrl');
|
||||
|
||||
// Serve user-imported fonts.
|
||||
if (isFontRequest(requestUrl)) {
|
||||
final fontResult = await _readFontFile(requestUrl);
|
||||
@@ -144,6 +147,7 @@ class EpubWebViewHandler {
|
||||
) async {
|
||||
final prefix = "/book/$fileHash/";
|
||||
if (!requestUrl.path.startsWith(prefix)) {
|
||||
debugPrint('[EPUB-Handler] path mismatch: ${requestUrl.path} does not start with $prefix');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -156,9 +160,13 @@ class EpubWebViewHandler {
|
||||
targetFilePath: fileRelativePath,
|
||||
);
|
||||
|
||||
if (data == null) return null;
|
||||
if (data == null) {
|
||||
debugPrint('[EPUB-Handler] file not found in epub: $fileRelativePath');
|
||||
return null;
|
||||
}
|
||||
|
||||
final mimeType = _streamService.getMimeType(fileRelativePath);
|
||||
debugPrint('[EPUB-Handler] serving: $fileRelativePath ($mimeType, ${data.length} bytes)');
|
||||
return (data, mimeType);
|
||||
}
|
||||
|
||||
@@ -195,6 +203,38 @@ class EpubWebViewHandler {
|
||||
'woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
/// Read HTML content from EPUB for srcdoc injection (Windows WebView2).
|
||||
/// [url] is the virtual epub:// URL; extracts the relative path and reads the file.
|
||||
/// Returns (htmlContent, baseUrl) where baseUrl points to the file's directory
|
||||
/// so that relative URLs in the HTML resolve correctly.
|
||||
Future<(String, String)?> readHtmlContentWithBaseUrl({
|
||||
required String epubPath,
|
||||
required String fileHash,
|
||||
required String url,
|
||||
}) async {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
final prefix = "/book/$fileHash/";
|
||||
if (!uri.path.startsWith(prefix)) return null;
|
||||
final decodedPath = Uri.decodeFull(uri.path);
|
||||
final relativePath = decodedPath.substring(prefix.length).split('#')[0];
|
||||
final data = await _streamService.readFileFromEpub(
|
||||
epubPath: epubPath,
|
||||
targetFilePath: relativePath,
|
||||
);
|
||||
if (data == null) return null;
|
||||
final htmlContent = String.fromCharCodes(data);
|
||||
// Base URL should point to the directory containing the HTML file
|
||||
final dirPath = relativePath.contains('/')
|
||||
? relativePath.substring(0, relativePath.lastIndexOf('/') + 1)
|
||||
: '';
|
||||
final baseUrl = '$virtualScheme://$virtualDomain/book/$fileHash/$dirPath';
|
||||
return (htmlContent, baseUrl);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate base URL for a chapter.
|
||||
/// This URL should be used as the baseUrl parameter when loading HTML.
|
||||
static String getBaseUrl() {
|
||||
|
||||
@@ -66,6 +66,41 @@ String generateSkeletonHtml(
|
||||
<script id="skeleton-script">
|
||||
$kControllerJs
|
||||
</script>
|
||||
<script id="skeleton-srcdoc-patch">
|
||||
// Patch: loadFrameSrcdoc for Windows WebView2
|
||||
// Injects HTML content via srcdoc instead of src URL.
|
||||
// A <base> tag is prepended so relative URLs resolve to epub:// and
|
||||
// can be intercepted by shouldInterceptRequest.
|
||||
window.api.loadFrameSrcdoc = function(token, slot, htmlContent, baseUrl, anchors, properties) {
|
||||
var frame = this.frameMgr.getFrame(slot);
|
||||
if (!frame) { window.flutter_inappwebview.callHandler("onEventFinished", token); return; }
|
||||
this.state.anchors[slot] = anchors || [];
|
||||
this.state.properties[slot] = properties || [];
|
||||
// Store the URL anchor for onFrameLoad to scroll to
|
||||
var urlAnchor = '';
|
||||
if (baseUrl.indexOf('#') !== -1) {
|
||||
urlAnchor = baseUrl.split('#').pop();
|
||||
}
|
||||
frame.onload = null;
|
||||
// Prepend <base> tag for relative URL resolution
|
||||
var baseTag = '<base href="' + baseUrl + '">';
|
||||
var htmlWithBase = htmlContent;
|
||||
if (htmlWithBase.indexOf('<head') !== -1) {
|
||||
htmlWithBase = htmlWithBase.replace(/<head([^>]*)>/i, function(m, a) { return '<head' + a + '>' + baseTag; });
|
||||
} else if (htmlWithBase.indexOf('<html') !== -1) {
|
||||
htmlWithBase = htmlWithBase.replace(/<html([^>]*)>/i, function(m, a) { return '<html' + a + '><head>' + baseTag + '</head>'; });
|
||||
} else {
|
||||
htmlWithBase = baseTag + htmlWithBase;
|
||||
}
|
||||
var self = this;
|
||||
frame.onload = function() {
|
||||
// Set frame.src so onFrameLoad can extract the anchor for scrolling
|
||||
if (urlAnchor) frame.setAttribute('data-srcdoc-anchor', urlAnchor);
|
||||
self.onFrameLoad(frame, token);
|
||||
};
|
||||
frame.srcdoc = htmlWithBase;
|
||||
};
|
||||
</script>
|
||||
<script id="skeleton-variable-script">
|
||||
const initialConfig = $initialConfigJson;
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -75,9 +110,9 @@ String generateSkeletonHtml(
|
||||
</head>
|
||||
<body>
|
||||
<div id="frame-container">
|
||||
<iframe id="frame-prev" sandbox="allow-same-origin allow-scripts" scrolling="no" style="z-index: 1; opacity: 0;"></iframe>
|
||||
<iframe id="frame-curr" sandbox="allow-same-origin allow-scripts" scrolling="no" style="z-index: 2; opacity: 1;"></iframe>
|
||||
<iframe id="frame-next" sandbox="allow-same-origin allow-scripts" scrolling="no" style="z-index: 1; opacity: 0;"></iframe>
|
||||
<iframe id="frame-prev" scrolling="no" style="z-index: 1; opacity: 0;"></iframe>
|
||||
<iframe id="frame-curr" scrolling="no" style="z-index: 2; opacity: 1;"></iframe>
|
||||
<iframe id="frame-next" scrolling="no" style="z-index: 1; opacity: 0;"></iframe>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -33,6 +33,33 @@ class ReaderApi {
|
||||
(t) => "window.api.loadFrame($t, '$slot', '$url', $anchors, $properties)",
|
||||
);
|
||||
|
||||
/// Loads HTML content via srcdoc into the iframe identified by [slot].
|
||||
/// Used on Windows where iframe src with custom scheme doesn't load subresources.
|
||||
/// [htmlContent] is the raw HTML string to inject.
|
||||
/// [baseUrl] is used as the iframe's base URL for resolving relative paths.
|
||||
Future<int> loadFrameSrcdoc(
|
||||
String slot,
|
||||
String htmlContent,
|
||||
String baseUrl,
|
||||
String anchors,
|
||||
String properties,
|
||||
) {
|
||||
final escapedHtml = _escapeForJs(htmlContent);
|
||||
return _bridge.call(
|
||||
(t) => "window.api.loadFrameSrcdoc($t, '$slot', '$escapedHtml', '$baseUrl', $anchors, $properties)",
|
||||
);
|
||||
}
|
||||
|
||||
/// Escapes a string for safe embedding in a JS single-quoted string literal.
|
||||
String _escapeForJs(String s) {
|
||||
return s
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll("'", "\\'")
|
||||
.replaceAll('\n', '\\n')
|
||||
.replaceAll('\r', '\\r')
|
||||
.replaceAll(r'$', r'\$');
|
||||
}
|
||||
|
||||
/// Scrolls [slot]'s iframe to [pageIndex] without immediately awaiting.
|
||||
Future<int> jumpToPageFor(String slot, int pageIndex) =>
|
||||
_bridge.call((t) => "window.api.jumpToPageFor($t, '$slot', $pageIndex)");
|
||||
|
||||
Reference in New Issue
Block a user