2024-06-26 18:39:41 +02:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2024-10-26 03:36:09 +02:00
import { BlockNodeModel } from "@/app/block/blocktypes" ;
2024-12-16 23:16:21 +01:00
import { getApi , getBlockMetaKeyAtom , getSettingsKeyAtom , openLink } from "@/app/store/global" ;
2024-08-30 01:06:15 +02:00
import { getSimpleControlShiftAtom } from "@/app/store/keymodel" ;
2024-10-08 02:20:18 +02:00
import { ObjectService } from "@/app/store/services" ;
2024-09-20 20:24:37 +02:00
import { RpcApi } from "@/app/store/wshclientapi" ;
2024-10-17 23:34:02 +02:00
import { TabRpcClient } from "@/app/store/wshrpcutil" ;
2024-08-30 01:06:15 +02:00
import { WOS , globalStore } from "@/store/global" ;
import { adaptFromReactOrNativeKeyEvent , checkKeyPressed } from "@/util/keyutil" ;
2024-08-29 22:37:05 +02:00
import { fireAndForget } from "@/util/util" ;
2024-07-18 08:41:33 +02:00
import clsx from "clsx" ;
2024-06-26 18:39:41 +02:00
import { WebviewTag } from "electron" ;
2024-10-08 02:20:18 +02:00
import { Atom , PrimitiveAtom , atom , useAtomValue } from "jotai" ;
2024-10-23 03:17:42 +02:00
import { Fragment , createRef , memo , useEffect , useRef , useState } from "react" ;
2024-11-22 01:05:04 +01:00
import "./webview.scss" ;
2024-06-26 18:39:41 +02:00
2024-10-05 01:34:05 +02:00
let webviewPreloadUrl = null ;
function getWebviewPreloadUrl() {
if ( webviewPreloadUrl == null ) {
webviewPreloadUrl = getApi ( ) . getWebviewPreload ( ) ;
console . log ( "webviewPreloadUrl" , webviewPreloadUrl ) ;
}
if ( webviewPreloadUrl == null ) {
return null ;
}
return "file://" + webviewPreloadUrl ;
}
2024-07-15 18:40:28 +02:00
export class WebViewModel implements ViewModel {
2024-08-23 01:25:53 +02:00
viewType : string ;
2024-07-09 00:04:48 +02:00
blockId : string ;
2024-10-08 02:20:18 +02:00
blockAtom : Atom < Block > ;
viewIcon : Atom < string | IconButtonDecl > ;
viewName : Atom < string > ;
viewText : Atom < HeaderElem [ ] > ;
url : PrimitiveAtom < string > ;
homepageUrl : Atom < string > ;
urlInputFocused : PrimitiveAtom < boolean > ;
isLoading : PrimitiveAtom < boolean > ;
urlWrapperClassName : PrimitiveAtom < string > ;
refreshIcon : PrimitiveAtom < string > ;
2024-07-15 18:40:28 +02:00
webviewRef : React.RefObject < WebviewTag > ;
urlInputRef : React.RefObject < HTMLInputElement > ;
2024-10-26 03:36:09 +02:00
nodeModel : BlockNodeModel ;
2024-10-08 02:20:18 +02:00
endIconButtons? : Atom < IconButtonDecl [ ] > ;
2024-10-16 00:15:33 +02:00
mediaPlaying : PrimitiveAtom < boolean > ;
mediaMuted : PrimitiveAtom < boolean > ;
2024-11-14 20:03:10 +01:00
modifyExternalUrl ? : ( url : string ) = > string ;
2024-12-16 23:16:21 +01:00
domReady : PrimitiveAtom < boolean > ;
2024-07-15 18:40:28 +02:00
2024-10-26 03:36:09 +02:00
constructor ( blockId : string , nodeModel : BlockNodeModel ) {
2024-08-30 01:06:15 +02:00
this . nodeModel = nodeModel ;
2024-08-23 01:25:53 +02:00
this . viewType = "web" ;
2024-07-15 18:40:28 +02:00
this . blockId = blockId ;
this . blockAtom = WOS . getWaveObjectAtom < Block > ( ` block: ${ blockId } ` ) ;
2024-10-08 02:20:18 +02:00
this . url = atom ( ) ;
const defaultUrlAtom = getSettingsKeyAtom ( "web:defaulturl" ) ;
this . homepageUrl = atom ( ( get ) = > {
const defaultUrl = get ( defaultUrlAtom ) ;
const pinnedUrl = get ( this . blockAtom ) . meta . pinnedurl ;
return pinnedUrl ? ? defaultUrl ;
} ) ;
this . urlWrapperClassName = atom ( "" ) ;
this . urlInputFocused = atom ( false ) ;
this . isLoading = atom ( false ) ;
this . refreshIcon = atom ( "rotate-right" ) ;
this . viewIcon = atom ( "globe" ) ;
this . viewName = atom ( "Web" ) ;
2024-10-23 03:17:42 +02:00
this . urlInputRef = createRef < HTMLInputElement > ( ) ;
this . webviewRef = createRef < WebviewTag > ( ) ;
2024-12-16 23:16:21 +01:00
this . domReady = atom ( false ) ;
2024-06-28 03:09:30 +02:00
2024-10-16 00:15:33 +02:00
this . mediaPlaying = atom ( false ) ;
this . mediaMuted = atom ( false ) ;
2024-10-08 02:20:18 +02:00
this . viewText = atom ( ( get ) = > {
2024-10-16 00:15:33 +02:00
const homepageUrl = get ( this . homepageUrl ) ;
const metaUrl = get ( this . blockAtom ) ? . meta ? . url ;
2024-09-10 23:59:34 +02:00
const currUrl = get ( this . url ) ;
2024-10-16 00:15:33 +02:00
const urlWrapperClassName = get ( this . urlWrapperClassName ) ;
const refreshIcon = get ( this . refreshIcon ) ;
const mediaPlaying = get ( this . mediaPlaying ) ;
const mediaMuted = get ( this . mediaMuted ) ;
const url = currUrl ? ? metaUrl ? ? homepageUrl ;
2024-07-15 18:40:28 +02:00
return [
{
elemtype : "iconbutton" ,
icon : "chevron-left" ,
click : this.handleBack.bind ( this ) ,
2024-10-09 01:54:41 +02:00
disabled : this.shouldDisableBackButton ( ) ,
2024-07-15 18:40:28 +02:00
} ,
{
elemtype : "iconbutton" ,
icon : "chevron-right" ,
click : this.handleForward.bind ( this ) ,
2024-10-09 01:54:41 +02:00
disabled : this.shouldDisableForwardButton ( ) ,
2024-07-15 18:40:28 +02:00
} ,
2024-10-08 02:20:18 +02:00
{
elemtype : "iconbutton" ,
icon : "house" ,
click : this.handleHome.bind ( this ) ,
2024-10-09 01:54:41 +02:00
disabled : this.shouldDisableHomeButton ( ) ,
2024-10-08 02:20:18 +02:00
} ,
2024-07-15 18:40:28 +02:00
{
elemtype : "div" ,
2024-10-16 00:15:33 +02:00
className : clsx ( "block-frame-div-url" , urlWrapperClassName ) ,
2024-07-15 18:40:28 +02:00
onMouseOver : this.handleUrlWrapperMouseOver.bind ( this ) ,
onMouseOut : this.handleUrlWrapperMouseOut.bind ( this ) ,
children : [
{
elemtype : "input" ,
value : url ,
ref : this.urlInputRef ,
className : "url-input" ,
onChange : this.handleUrlChange.bind ( this ) ,
onKeyDown : this.handleKeyDown.bind ( this ) ,
onFocus : this.handleFocus.bind ( this ) ,
onBlur : this.handleBlur.bind ( this ) ,
} ,
2024-10-16 00:15:33 +02:00
mediaPlaying && {
elemtype : "iconbutton" ,
icon : mediaMuted ? "volume-slash" : "volume" ,
click : this.handleMuteChange.bind ( this ) ,
} ,
2024-07-15 18:40:28 +02:00
{
elemtype : "iconbutton" ,
2024-10-16 00:15:33 +02:00
icon : refreshIcon ,
2024-07-15 18:40:28 +02:00
click : this.handleRefresh.bind ( this ) ,
} ,
2024-10-16 00:15:33 +02:00
] . filter ( ( v ) = > v ) ,
2024-07-15 18:40:28 +02:00
} ,
] as HeaderElem [ ] ;
} ) ;
2024-09-20 20:24:37 +02:00
2024-10-08 02:20:18 +02:00
this . endIconButtons = atom ( ( get ) = > {
2024-10-16 00:15:33 +02:00
const url = get ( this . url ) ;
2024-09-20 20:24:37 +02:00
return [
{
elemtype : "iconbutton" ,
icon : "arrow-up-right-from-square" ,
title : "Open in External Browser" ,
click : ( ) = > {
2024-12-12 01:09:47 +01:00
console . log ( "open external" , url ) ;
2024-09-20 20:24:37 +02:00
if ( url != null && url != "" ) {
2024-11-14 20:03:10 +01:00
const externalUrl = this . modifyExternalUrl ? . ( url ) ? ? url ;
return getApi ( ) . openExternal ( externalUrl ) ;
2024-09-20 20:24:37 +02:00
}
} ,
} ,
] ;
} ) ;
2024-07-15 18:40:28 +02:00
}
2024-06-26 18:39:41 +02:00
2024-08-29 22:37:05 +02:00
/ * *
* Whether the back button in the header should be disabled .
* @returns True if the WebView cannot go back or if the WebView call fails . False otherwise .
* /
2024-10-09 01:54:41 +02:00
shouldDisableBackButton() {
2024-08-29 22:37:05 +02:00
try {
return ! this . webviewRef . current ? . canGoBack ( ) ;
} catch ( _ ) { }
return true ;
2024-07-15 18:40:28 +02:00
}
2024-06-26 18:39:41 +02:00
2024-08-29 22:37:05 +02:00
/ * *
* Whether the forward button in the header should be disabled .
* @returns True if the WebView cannot go forward or if the WebView call fails . False otherwise .
* /
2024-10-09 01:54:41 +02:00
shouldDisableForwardButton() {
2024-08-29 22:37:05 +02:00
try {
return ! this . webviewRef . current ? . canGoForward ( ) ;
} catch ( _ ) { }
return true ;
2024-07-15 18:40:28 +02:00
}
2024-06-26 18:39:41 +02:00
2024-10-08 02:20:18 +02:00
/ * *
* Whether the home button in the header should be disabled .
* @returns True if the current url is the pinned url or the pinned url is not set . False otherwise .
* /
2024-10-09 01:54:41 +02:00
shouldDisableHomeButton() {
2024-10-08 02:20:18 +02:00
try {
const homepageUrl = globalStore . get ( this . homepageUrl ) ;
return ! homepageUrl || this . getUrl ( ) === homepageUrl ;
} catch ( _ ) { }
return true ;
}
handleHome ( e? : React.MouseEvent < HTMLDivElement , MouseEvent > ) {
if ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
this . loadUrl ( globalStore . get ( this . homepageUrl ) , "home" ) ;
}
2024-10-16 00:15:33 +02:00
setMediaPlaying ( isPlaying : boolean ) {
globalStore . set ( this . mediaPlaying , isPlaying ) ;
}
handleMuteChange ( e : React.ChangeEvent < HTMLInputElement > ) {
if ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
try {
const newMutedVal = ! this . webviewRef . current ? . isAudioMuted ( ) ;
globalStore . set ( this . mediaMuted , newMutedVal ) ;
this . webviewRef . current ? . setAudioMuted ( newMutedVal ) ;
} catch ( e ) {
console . error ( "Failed to change mute value" , e ) ;
}
}
2024-07-15 18:40:28 +02:00
handleUrlWrapperMouseOver ( e : React.MouseEvent < HTMLDivElement , MouseEvent > ) {
const urlInputFocused = globalStore . get ( this . urlInputFocused ) ;
if ( e . type === "mouseover" && ! urlInputFocused ) {
globalStore . set ( this . urlWrapperClassName , "hovered" ) ;
}
}
2024-06-26 18:39:41 +02:00
2024-07-15 18:40:28 +02:00
handleUrlWrapperMouseOut ( e : React.MouseEvent < HTMLDivElement , MouseEvent > ) {
const urlInputFocused = globalStore . get ( this . urlInputFocused ) ;
if ( e . type === "mouseout" && ! urlInputFocused ) {
globalStore . set ( this . urlWrapperClassName , "" ) ;
2024-06-26 18:39:41 +02:00
}
2024-07-15 18:40:28 +02:00
}
2024-06-26 18:39:41 +02:00
2024-08-30 01:06:15 +02:00
handleBack ( e? : React.MouseEvent < HTMLDivElement , MouseEvent > ) {
if ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
2024-08-29 22:37:05 +02:00
this . webviewRef . current ? . goBack ( ) ;
2024-07-15 18:40:28 +02:00
}
2024-06-26 18:39:41 +02:00
2024-08-30 01:06:15 +02:00
handleForward ( e? : React.MouseEvent < HTMLDivElement , MouseEvent > ) {
if ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
2024-08-29 22:37:05 +02:00
this . webviewRef . current ? . goForward ( ) ;
2024-07-15 18:40:28 +02:00
}
handleRefresh ( e : React.MouseEvent < HTMLDivElement , MouseEvent > ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
2024-08-29 22:37:05 +02:00
try {
if ( this . webviewRef . current ) {
if ( globalStore . get ( this . isLoading ) ) {
this . webviewRef . current . stop ( ) ;
} else {
this . webviewRef . current . reload ( ) ;
}
2024-07-15 18:40:28 +02:00
}
2024-08-29 22:37:05 +02:00
} catch ( e ) {
console . warn ( "handleRefresh catch" , e ) ;
2024-07-15 18:40:28 +02:00
}
}
2024-06-28 03:09:30 +02:00
2024-07-15 18:40:28 +02:00
handleUrlChange ( event : React.ChangeEvent < HTMLInputElement > ) {
globalStore . set ( this . url , event . target . value ) ;
}
handleKeyDown ( event : React.KeyboardEvent < HTMLInputElement > ) {
2024-08-30 01:06:15 +02:00
const waveEvent = adaptFromReactOrNativeKeyEvent ( event ) ;
if ( checkKeyPressed ( waveEvent , "Enter" ) ) {
2024-08-29 22:37:05 +02:00
const url = globalStore . get ( this . url ) ;
2024-09-08 03:22:13 +02:00
this . loadUrl ( url , "enter" ) ;
2024-07-15 18:40:28 +02:00
this . urlInputRef . current ? . blur ( ) ;
2024-08-30 01:06:15 +02:00
return ;
}
if ( checkKeyPressed ( waveEvent , "Escape" ) ) {
this . webviewRef . current ? . focus ( ) ;
2024-07-15 18:40:28 +02:00
}
}
2024-06-26 18:39:41 +02:00
2024-07-15 18:40:28 +02:00
handleFocus ( event : React.FocusEvent < HTMLInputElement > ) {
globalStore . set ( this . urlWrapperClassName , "focused" ) ;
globalStore . set ( this . urlInputFocused , true ) ;
this . urlInputRef . current . focus ( ) ;
event . target . select ( ) ;
}
handleBlur ( event : React.FocusEvent < HTMLInputElement > ) {
globalStore . set ( this . urlWrapperClassName , "" ) ;
globalStore . set ( this . urlInputFocused , false ) ;
}
2024-08-29 22:37:05 +02:00
/ * *
* Update the URL in the state when a navigation event has occurred .
* @param url The URL that has been navigated to .
* /
handleNavigate ( url : string ) {
2024-12-06 03:09:54 +01:00
fireAndForget ( ( ) = > ObjectService . UpdateObjectMeta ( WOS . makeORef ( "block" , this . blockId ) , { url } ) ) ;
2024-08-29 22:37:05 +02:00
globalStore . set ( this . url , url ) ;
}
2024-09-20 20:24:37 +02:00
ensureUrlScheme ( url : string , searchTemplate : string ) {
if ( url == null ) {
url = "" ;
}
2024-08-08 20:58:22 +02:00
if ( /^(http|https):/ . test ( url ) ) {
// If the URL starts with http: or https:, return it as is
return url ;
}
// Check if the URL looks like a local URL
const isLocal = /^(localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?$/ . test ( url . split ( "/" ) [ 0 ] ) ;
if ( isLocal ) {
// If it is a local URL, ensure it has http:// scheme
2024-06-28 23:53:50 +02:00
return ` http:// ${ url } ` ;
2024-08-08 20:58:22 +02:00
}
// Check if the URL looks like a domain
const domainRegex = /^[a-z0-9.-]+\.[a-z]{2,}$/i ;
const isDomain = domainRegex . test ( url . split ( "/" ) [ 0 ] ) ;
if ( isDomain ) {
// If it looks like a domain, ensure it has https:// scheme
2024-06-26 18:39:41 +02:00
return ` https:// ${ url } ` ;
}
2024-08-08 20:58:22 +02:00
// Otherwise, treat it as a search query
2024-09-20 20:24:37 +02:00
if ( searchTemplate == null ) {
return ` https://www.google.com/search?q= ${ encodeURIComponent ( url ) } ` ;
}
return searchTemplate . replace ( "{query}" , encodeURIComponent ( url ) ) ;
2024-07-15 18:40:28 +02:00
}
2024-08-29 22:37:05 +02:00
/ * *
* Load a new URL in the webview .
* @param newUrl The new URL to load in the webview .
* /
2024-09-08 03:22:13 +02:00
loadUrl ( newUrl : string , reason : string ) {
2024-10-07 23:08:57 +02:00
const defaultSearchAtom = getSettingsKeyAtom ( "web:defaultsearch" ) ;
2024-09-20 20:24:37 +02:00
const searchTemplate = globalStore . get ( defaultSearchAtom ) ;
const nextUrl = this . ensureUrlScheme ( newUrl , searchTemplate ) ;
2024-12-16 23:16:21 +01:00
console . log ( "webview loadUrl" , reason , nextUrl , "cur=" , this . webviewRef . current . getURL ( ) ) ;
2024-09-08 03:22:13 +02:00
if ( ! this . webviewRef . current ) {
return ;
}
if ( this . webviewRef . current . getURL ( ) != nextUrl ) {
2024-12-12 01:09:47 +01:00
fireAndForget ( ( ) = > this . webviewRef . current . loadURL ( nextUrl ) ) ;
}
if ( newUrl != nextUrl ) {
globalStore . set ( this . url , nextUrl ) ;
2024-06-26 18:39:41 +02:00
}
2024-07-15 18:40:28 +02:00
}
2024-06-26 18:39:41 +02:00
2024-08-29 22:37:05 +02:00
/ * *
* Get the current URL from the state .
* @returns The URL from the state .
* /
getUrl() {
return globalStore . get ( this . url ) ;
2024-07-15 18:40:28 +02:00
}
2024-06-26 18:39:41 +02:00
2024-07-15 18:40:28 +02:00
setRefreshIcon ( refreshIcon : string ) {
globalStore . set ( this . refreshIcon , refreshIcon ) ;
}
2024-06-26 18:39:41 +02:00
2024-07-15 18:40:28 +02:00
setIsLoading ( isLoading : boolean ) {
globalStore . set ( this . isLoading , isLoading ) ;
}
2024-06-26 18:39:41 +02:00
2024-10-09 21:42:33 +02:00
async setHomepageUrl ( url : string , scope : "global" | "block" ) {
if ( url != null && url != "" ) {
switch ( scope ) {
case "block" :
2024-10-17 23:34:02 +02:00
await RpcApi . SetMetaCommand ( TabRpcClient , {
2024-10-09 21:42:33 +02:00
oref : WOS.makeORef ( "block" , this . blockId ) ,
meta : { pinnedurl : url } ,
} ) ;
break ;
case "global" :
2024-10-17 23:34:02 +02:00
await RpcApi . SetMetaCommand ( TabRpcClient , {
2024-10-09 21:42:33 +02:00
oref : WOS.makeORef ( "block" , this . blockId ) ,
meta : { pinnedurl : "" } ,
} ) ;
2024-10-17 23:34:02 +02:00
await RpcApi . SetConfigCommand ( TabRpcClient , { "web:defaulturl" : url } ) ;
2024-10-09 21:42:33 +02:00
break ;
}
}
}
2024-07-23 01:41:18 +02:00
giveFocus ( ) : boolean {
2024-08-30 01:06:15 +02:00
const ctrlShiftState = globalStore . get ( getSimpleControlShiftAtom ( ) ) ;
if ( ctrlShiftState ) {
// this is really weird, we don't get keyup events from webview
const unsubFn = globalStore . sub ( getSimpleControlShiftAtom ( ) , ( ) = > {
const state = globalStore . get ( getSimpleControlShiftAtom ( ) ) ;
if ( ! state ) {
unsubFn ( ) ;
const isStillFocused = globalStore . get ( this . nodeModel . isFocused ) ;
if ( isStillFocused ) {
this . webviewRef . current ? . focus ( ) ;
}
}
} ) ;
return false ;
2024-07-23 01:41:18 +02:00
}
2024-08-30 01:06:15 +02:00
this . webviewRef . current ? . focus ( ) ;
return true ;
2024-07-23 01:41:18 +02:00
}
2024-08-22 00:49:23 +02:00
keyDownHandler ( e : WaveKeyboardEvent ) : boolean {
if ( checkKeyPressed ( e , "Cmd:l" ) ) {
this . urlInputRef ? . current ? . focus ( ) ;
this . urlInputRef ? . current ? . select ( ) ;
return true ;
}
if ( checkKeyPressed ( e , "Cmd:r" ) ) {
2024-12-16 23:16:21 +01:00
this . webviewRef . current ? . reload ( ) ;
2024-08-22 00:49:23 +02:00
return true ;
}
2024-08-30 01:06:15 +02:00
if ( checkKeyPressed ( e , "Cmd:ArrowLeft" ) ) {
this . handleBack ( null ) ;
return true ;
}
if ( checkKeyPressed ( e , "Cmd:ArrowRight" ) ) {
this . handleForward ( null ) ;
return true ;
}
2024-08-22 00:49:23 +02:00
return false ;
}
2024-09-10 00:41:26 +02:00
2024-12-16 23:16:21 +01:00
setZoomFactor ( factor : number | null ) {
// null is ok (will reset to default)
if ( factor != null && factor < 0.1 ) {
factor = 0.1 ;
}
if ( factor != null && factor > 5 ) {
factor = 5 ;
}
const domReady = globalStore . get ( this . domReady ) ;
if ( ! domReady ) {
return ;
}
this . webviewRef . current ? . setZoomFactor ( factor || 1 ) ;
RpcApi . SetMetaCommand ( TabRpcClient , {
oref : WOS.makeORef ( "block" , this . blockId ) ,
meta : { "web:zoom" : factor } , // allow null so we can remove the zoom factor here
} ) ;
}
2024-09-20 20:24:37 +02:00
getSettingsMenuItems ( ) : ContextMenuItem [ ] {
2024-12-16 23:16:21 +01:00
const zoomSubMenu : ContextMenuItem [ ] = [ ] ;
let curZoom = 1 ;
if ( globalStore . get ( this . domReady ) ) {
curZoom = this . webviewRef . current ? . getZoomFactor ( ) || 1 ;
}
const model = this ; // for the closure to work (this is getting unset)
function makeZoomFactorMenuItem ( label : string , factor : number ) : ContextMenuItem {
return {
label : label ,
type : "checkbox" ,
click : ( ) = > {
model . setZoomFactor ( factor ) ;
} ,
checked : curZoom == factor ,
} ;
}
zoomSubMenu . push ( {
label : "Reset" ,
click : ( ) = > {
model . setZoomFactor ( null ) ;
} ,
} ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "25%" , 0.25 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "50%" , 0.5 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "70%" , 0.7 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "80%" , 0.8 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "90%" , 0.9 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "100%" , 1 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "110%" , 1.1 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "120%" , 1.2 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "130%" , 1.3 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "150%" , 1.5 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "175%" , 1.75 ) ) ;
zoomSubMenu . push ( makeZoomFactorMenuItem ( "200%" , 2 ) ) ;
2024-09-10 00:41:26 +02:00
return [
2024-09-20 20:24:37 +02:00
{
2024-10-08 17:46:50 +02:00
label : "Set Block Homepage" ,
2024-12-06 03:09:54 +01:00
click : ( ) = > fireAndForget ( ( ) = > this . setHomepageUrl ( this . getUrl ( ) , "block" ) ) ,
2024-09-20 20:24:37 +02:00
} ,
2024-10-08 17:46:50 +02:00
{
label : "Set Default Homepage" ,
2024-12-06 03:09:54 +01:00
click : ( ) = > fireAndForget ( ( ) = > this . setHomepageUrl ( this . getUrl ( ) , "global" ) ) ,
2024-10-08 17:46:50 +02:00
} ,
2024-09-20 20:24:37 +02:00
{
type : "separator" ,
} ,
2024-12-16 23:16:21 +01:00
{
label : "Set Zoom Factor" ,
submenu : zoomSubMenu ,
} ,
2024-09-10 00:41:26 +02:00
{
label : this.webviewRef.current?.isDevToolsOpened ( ) ? "Close DevTools" : "Open DevTools" ,
2024-12-06 03:09:54 +01:00
click : ( ) = > {
2024-09-10 00:41:26 +02:00
if ( this . webviewRef . current ) {
if ( this . webviewRef . current . isDevToolsOpened ( ) ) {
this . webviewRef . current . closeDevTools ( ) ;
} else {
this . webviewRef . current . openDevTools ( ) ;
}
}
} ,
} ,
] ;
}
2024-07-15 18:40:28 +02:00
}
2024-10-26 03:36:09 +02:00
function makeWebViewModel ( blockId : string , nodeModel : BlockNodeModel ) : WebViewModel {
2024-08-30 01:06:15 +02:00
const webviewModel = new WebViewModel ( blockId , nodeModel ) ;
2024-07-15 18:40:28 +02:00
return webviewModel ;
}
interface WebViewProps {
2024-08-22 00:49:23 +02:00
blockId : string ;
2024-07-15 18:40:28 +02:00
model : WebViewModel ;
2024-10-18 01:03:17 +02:00
onFailLoad ? : ( url : string ) = > void ;
2024-07-15 18:40:28 +02:00
}
2024-10-18 01:03:17 +02:00
const WebView = memo ( ( { model , onFailLoad } : WebViewProps ) = > {
2024-10-08 02:20:18 +02:00
const blockData = useAtomValue ( model . blockAtom ) ;
const defaultUrl = useAtomValue ( model . homepageUrl ) ;
2024-10-07 23:08:57 +02:00
const defaultSearchAtom = getSettingsKeyAtom ( "web:defaultsearch" ) ;
2024-10-08 02:20:18 +02:00
const defaultSearch = useAtomValue ( defaultSearchAtom ) ;
2024-09-20 20:34:04 +02:00
let metaUrl = blockData ? . meta ? . url || defaultUrl ;
metaUrl = model . ensureUrlScheme ( metaUrl , defaultSearch ) ;
2024-10-23 03:17:42 +02:00
const metaUrlRef = useRef ( metaUrl ) ;
2024-12-16 23:16:21 +01:00
const zoomFactor = useAtomValue ( getBlockMetaKeyAtom ( model . blockId , "web:zoom" ) ) || 1 ;
2024-08-29 22:37:05 +02:00
// The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview.
const [ metaUrlInitial ] = useState ( metaUrl ) ;
2024-09-10 21:50:55 +02:00
const [ webContentsId , setWebContentsId ] = useState ( null ) ;
2024-12-16 23:16:21 +01:00
const domReady = useAtomValue ( model . domReady ) ;
2024-09-10 21:50:55 +02:00
2024-10-23 03:17:42 +02:00
const [ errorText , setErrorText ] = useState ( "" ) ;
2024-09-30 22:51:01 +02:00
function setBgColor() {
const webview = model . webviewRef . current ;
if ( ! webview ) {
return ;
}
setTimeout ( ( ) = > {
webview
. executeJavaScript (
` !!document.querySelector('meta[name="color-scheme"]') && document.querySelector('meta[name="color-scheme"]').content?.includes('dark') || false `
)
. then ( ( hasDarkMode ) = > {
if ( hasDarkMode ) {
webview . style . backgroundColor = "black" ; // Dark mode background
} else {
webview . style . backgroundColor = "white" ; // Light mode background
}
} )
. catch ( ( e ) = > {
webview . style . backgroundColor = "black" ; // Dark mode background
console . log ( "Error getting color scheme, defaulting to dark" , e ) ;
} ) ;
} , 100 ) ;
}
2024-09-10 21:50:55 +02:00
useEffect ( ( ) = > {
2024-12-16 23:16:21 +01:00
return ( ) = > {
globalStore . set ( model . domReady , false ) ;
} ;
} , [ ] ) ;
useEffect ( ( ) = > {
if ( model . webviewRef . current == null || ! domReady ) {
return ;
}
try {
2024-09-10 21:50:55 +02:00
const wcId = model . webviewRef . current . getWebContentsId ? . ( ) ;
if ( wcId ) {
setWebContentsId ( wcId ) ;
2024-12-16 23:16:21 +01:00
if ( model . webviewRef . current . getZoomFactor ( ) != zoomFactor ) {
model . webviewRef . current . setZoomFactor ( zoomFactor ) ;
}
2024-09-10 21:50:55 +02:00
}
2024-12-16 23:16:21 +01:00
} catch ( e ) {
console . error ( "Failed to get webcontentsid / setzoomlevel (webview)" , e ) ;
2024-09-10 21:50:55 +02:00
}
2024-12-16 23:16:21 +01:00
} , [ model . webviewRef . current , domReady , zoomFactor ] ) ;
2024-09-10 21:50:55 +02:00
2024-08-29 22:37:05 +02:00
// Load a new URL if the block metadata is updated.
2024-08-01 09:35:44 +02:00
useEffect ( ( ) = > {
if ( metaUrlRef . current != metaUrl ) {
metaUrlRef . current = metaUrl ;
2024-09-08 03:22:13 +02:00
model . loadUrl ( metaUrl , "meta" ) ;
2024-08-01 09:35:44 +02:00
}
} , [ metaUrl ] ) ;
2024-07-15 18:40:28 +02:00
useEffect ( ( ) = > {
const webview = model . webviewRef . current ;
2024-10-06 22:55:26 +02:00
if ( ! webview ) {
return ;
2024-06-28 23:53:50 +02:00
}
2024-10-06 22:55:26 +02:00
const navigateListener = ( e : any ) = > {
2024-12-12 01:09:47 +01:00
setErrorText ( "" ) ;
2024-12-11 19:33:37 +01:00
if ( e . isMainFrame ) {
model . handleNavigate ( e . url ) ;
}
2024-10-06 22:55:26 +02:00
} ;
const newWindowHandler = ( e : any ) = > {
e . preventDefault ( ) ;
const newUrl = e . detail . url ;
fireAndForget ( ( ) = > openLink ( newUrl , true ) ) ;
} ;
const startLoadingHandler = ( ) = > {
model . setRefreshIcon ( "xmark-large" ) ;
model . setIsLoading ( true ) ;
webview . style . backgroundColor = "transparent" ;
} ;
const stopLoadingHandler = ( ) = > {
model . setRefreshIcon ( "rotate-right" ) ;
model . setIsLoading ( false ) ;
setBgColor ( ) ;
} ;
const failLoadHandler = ( e : any ) = > {
if ( e . errorCode === - 3 ) {
console . warn ( "Suppressed ERR_ABORTED error" , e ) ;
} else {
2024-10-23 03:17:42 +02:00
const errorMessage = ` Failed to load ${ e . validatedURL } : ${ e . errorDescription } ` ;
console . error ( errorMessage ) ;
setErrorText ( errorMessage ) ;
2024-10-18 01:03:17 +02:00
if ( onFailLoad ) {
2024-12-16 23:16:21 +01:00
const curUrl = model . webviewRef . current . getURL ( ) ;
2024-10-18 01:03:17 +02:00
onFailLoad ( curUrl ) ;
}
2024-10-06 22:55:26 +02:00
}
} ;
const webviewFocus = ( ) = > {
getApi ( ) . setWebviewFocus ( webview . getWebContentsId ( ) ) ;
model . nodeModel . focusNode ( ) ;
} ;
const webviewBlur = ( ) = > {
getApi ( ) . setWebviewFocus ( null ) ;
} ;
const handleDomReady = ( ) = > {
2024-12-16 23:16:21 +01:00
globalStore . set ( model . domReady , true ) ;
2024-10-06 22:55:26 +02:00
setBgColor ( ) ;
} ;
2024-10-16 00:15:33 +02:00
const handleMediaPlaying = ( ) = > {
model . setMediaPlaying ( true ) ;
} ;
const handleMediaPaused = ( ) = > {
model . setMediaPlaying ( false ) ;
} ;
2024-10-06 22:55:26 +02:00
2024-12-12 01:09:47 +01:00
webview . addEventListener ( "did-frame-navigate" , navigateListener ) ;
2024-10-06 22:55:26 +02:00
webview . addEventListener ( "did-navigate-in-page" , navigateListener ) ;
webview . addEventListener ( "did-navigate" , navigateListener ) ;
webview . addEventListener ( "did-start-loading" , startLoadingHandler ) ;
webview . addEventListener ( "did-stop-loading" , stopLoadingHandler ) ;
webview . addEventListener ( "new-window" , newWindowHandler ) ;
webview . addEventListener ( "did-fail-load" , failLoadHandler ) ;
webview . addEventListener ( "focus" , webviewFocus ) ;
webview . addEventListener ( "blur" , webviewBlur ) ;
webview . addEventListener ( "dom-ready" , handleDomReady ) ;
2024-10-16 00:15:33 +02:00
webview . addEventListener ( "media-started-playing" , handleMediaPlaying ) ;
webview . addEventListener ( "media-paused" , handleMediaPaused ) ;
2024-10-06 22:55:26 +02:00
// Clean up event listeners on component unmount
return ( ) = > {
2024-12-12 01:09:47 +01:00
webview . removeEventListener ( "did-frame-navigate" , navigateListener ) ;
2024-10-06 22:55:26 +02:00
webview . removeEventListener ( "did-navigate" , navigateListener ) ;
webview . removeEventListener ( "did-navigate-in-page" , navigateListener ) ;
webview . removeEventListener ( "new-window" , newWindowHandler ) ;
webview . removeEventListener ( "did-fail-load" , failLoadHandler ) ;
webview . removeEventListener ( "did-start-loading" , startLoadingHandler ) ;
webview . removeEventListener ( "did-stop-loading" , stopLoadingHandler ) ;
webview . removeEventListener ( "focus" , webviewFocus ) ;
webview . removeEventListener ( "blur" , webviewBlur ) ;
webview . removeEventListener ( "dom-ready" , handleDomReady ) ;
2024-10-16 00:15:33 +02:00
webview . removeEventListener ( "media-started-playing" , handleMediaPlaying ) ;
webview . removeEventListener ( "media-paused" , handleMediaPaused ) ;
2024-10-06 22:55:26 +02:00
} ;
2024-07-15 18:40:28 +02:00
} , [ ] ) ;
2024-08-29 22:37:05 +02:00
return (
2024-10-23 03:17:42 +02:00
< Fragment >
< webview
id = "webview"
className = "webview"
ref = { model . webviewRef }
src = { metaUrlInitial }
data - blockid = { model . blockId }
data - webcontentsid = { webContentsId } // needed for emain
preload = { getWebviewPreloadUrl ( ) }
// @ts-ignore This is a discrepancy between the React typing and the Chromium impl for webviewTag. Chrome webviewTag expects a string, while React expects a boolean.
allowpopups = "true"
/ >
{ errorText && (
< div className = "webview-error" >
< div > { errorText } < / div >
< / div >
) }
< / Fragment >
2024-08-29 22:37:05 +02:00
) ;
2024-07-03 23:32:55 +02:00
} ) ;
2024-06-26 18:39:41 +02:00
2024-07-15 18:40:28 +02:00
export { WebView , makeWebViewModel } ;