From 4c45bca56b95ef106779fbb359ca67cecb43ce23 Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Sat, 29 Jun 2024 05:53:50 +0800 Subject: [PATCH] webview fixes (#88) - Added restart button - Handle localhost URLs - Handle in-page navigation(SPA) --- frontend/app/view/webview.less | 13 +++++ frontend/app/view/webview.tsx | 93 +++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/frontend/app/view/webview.less b/frontend/app/view/webview.less index f6c968ada..daf37799a 100644 --- a/frontend/app/view/webview.less +++ b/frontend/app/view/webview.less @@ -12,6 +12,19 @@ border: 1px solid var(--border-color); border-right: none; border-top-left-radius: 4px; + + .button { + padding: 6px 12px; + + i { + font-size: 16px; + margin: 0; + + &.fa-rotate-right { + font-size: 12px; + } + } + } } .url-input-wrapper { diff --git a/frontend/app/view/webview.tsx b/frontend/app/view/webview.tsx index 8808ba44d..06f9135ff 100644 --- a/frontend/app/view/webview.tsx +++ b/frontend/app/view/webview.tsx @@ -17,12 +17,14 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { const [url, setUrl] = useState(initialUrl); const [inputUrl, setInputUrl] = useState(initialUrl); // Separate state for the input field const [webViewHeight, setWebViewHeight] = useState(0); + const [isLoading, setIsLoading] = useState(false); const webviewRef = useRef(null); const inputRef = useRef(null); const historyStack = useRef([]); const historyIndex = useRef(-1); + const recentUrls = useRef<{ [key: string]: number }>({}); const getWebViewHeight = () => { const inputHeight = inputRef.current?.getBoundingClientRect().height; @@ -44,11 +46,12 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { const normalizedLastUrl = normalizeUrl(historyStack.current[historyIndex.current]); if (normalizedLastUrl !== normalizedNewUrl) { - setUrl(newUrl); - setInputUrl(newUrl); // Update input field as well + setUrl(normalizedNewUrl); + setInputUrl(normalizedNewUrl); // Update input field as well historyIndex.current += 1; historyStack.current = historyStack.current.slice(0, historyIndex.current); - historyStack.current.push(newUrl); + historyStack.current.push(normalizedNewUrl); + updateRecentUrls(normalizedNewUrl); } }; @@ -59,6 +62,8 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { webview.addEventListener("did-navigate", navigateListener); webview.addEventListener("did-navigate-in-page", navigateListener); + webview.addEventListener("did-start-loading", () => setIsLoading(true)); + webview.addEventListener("did-stop-loading", () => setIsLoading(false)); // Handle new-window event webview.addEventListener("new-window", (event: any) => { @@ -102,6 +107,11 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { inputRef.current.focus(); inputRef.current.select(); } + } else if ((event.ctrlKey || event.metaKey) && event.key === "r") { + event.preventDefault(); + if (webviewRef.current) { + webviewRef.current.reload(); + } } }; @@ -135,7 +145,11 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { }, []); const ensureUrlScheme = (url: string) => { - if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) { + if (/^(localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?/.test(url)) { + // If the URL starts with localhost or an IP address (with optional port) + return `http://${url}`; + } else if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) { + // If the URL doesn't start with a protocol return `https://${url}`; } return url; @@ -147,11 +161,25 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { if (parsedUrl.hostname.startsWith("www.")) { parsedUrl.hostname = parsedUrl.hostname.slice(4); } - parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, ""); // Remove trailing slashes - parsedUrl.search = ""; // Remove query parameters + + // Ensure pathname ends with a trailing slash + if (!parsedUrl.pathname.endsWith("/")) { + parsedUrl.pathname += "/"; + } + + // Ensure hash fragments end with a trailing slash + if (parsedUrl.hash && !parsedUrl.hash.endsWith("/")) { + parsedUrl.hash += "/"; + } + + // Ensure search parameters end with a trailing slash + if (parsedUrl.search && !parsedUrl.search.endsWith("/")) { + parsedUrl.search += "/"; + } + return parsedUrl.href; } catch (e) { - return url.replace(/\/+$/, ""); // Fallback for invalid URLs + return url.replace(/\/+$/, "") + "/"; } }; @@ -161,20 +189,24 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { const normalizedLastUrl = normalizeUrl(historyStack.current[historyIndex.current]); if (normalizedLastUrl !== normalizedFinalUrl) { - setUrl(finalUrl); - setInputUrl(finalUrl); + setUrl(normalizedFinalUrl); + setInputUrl(normalizedFinalUrl); historyIndex.current += 1; historyStack.current = historyStack.current.slice(0, historyIndex.current); - historyStack.current.push(finalUrl); + historyStack.current.push(normalizedFinalUrl); if (webviewRef.current) { - webviewRef.current.src = finalUrl; + webviewRef.current.src = normalizedFinalUrl; } + updateRecentUrls(normalizedFinalUrl); } }; const handleBack = () => { if (historyIndex.current > 0) { - historyIndex.current -= 1; + do { + historyIndex.current -= 1; + } while (historyIndex.current > 0 && isRecentUrl(historyStack.current[historyIndex.current])); + const prevUrl = historyStack.current[historyIndex.current]; setUrl(prevUrl); setInputUrl(prevUrl); @@ -186,7 +218,13 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { const handleForward = () => { if (historyIndex.current < historyStack.current.length - 1) { - historyIndex.current += 1; + do { + historyIndex.current += 1; + } while ( + historyIndex.current < historyStack.current.length - 1 && + isRecentUrl(historyStack.current[historyIndex.current]) + ); + const nextUrl = historyStack.current[historyIndex.current]; setUrl(nextUrl); setInputUrl(nextUrl); @@ -196,6 +234,16 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { } }; + const handleRefresh = () => { + if (webviewRef.current) { + if (isLoading) { + webviewRef.current.stop(); + } else { + webviewRef.current.reload(); + } + } + }; + const handleUrlChange = (event: React.ChangeEvent) => { setInputUrl(event.target.value); }; @@ -210,6 +258,22 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { event.target.select(); }; + const updateRecentUrls = (url: string) => { + if (recentUrls.current[url]) { + recentUrls.current[url]++; + } else { + recentUrls.current[url] = 1; + } + // Clean up old entries after a certain threshold + if (Object.keys(recentUrls.current).length > 50) { + recentUrls.current = {}; + } + }; + + const isRecentUrl = (url: string) => { + return recentUrls.current[url] > 1; + }; + return (
@@ -224,6 +288,9 @@ const WebView = ({ parentRef, initialUrl }: WebViewProps) => { > +