diff --git a/README.md b/README.md index 1bf468b..5eb4e46 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,39 @@ --- +## 🚀 Quick Navigation + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
🚀 What Can You Build?🚀 Quick StartDraggable WebView2 Skin🆘 Troubleshooting
🎯 Key Features⚙️ Measure☄️ Virtual Host Setup📄 License
📋 Requirements🔥 JavaScript Integration💡 Examples🤝 Contributing
📥 Installation🌉 RainmeterAPI Bridge🛠️ Building from Source🙏 Acknowledgments
+
+ +--- + ## ✨ What Can You Build? @@ -106,18 +139,14 @@ Two-way communication between web and Rainmeter: ### 🎁 Method 1: One-Click Install (Recommended) -
- **The easiest way to get started!** -1. 📦 [Download the `.rmskin` file](../../releases/latest) +1. 📦 [Download the .rmskin file](../../releases/latest) 2. 🖱️ Double-click to install 3. ✨ Done! Plugin and examples are ready to use Rainmeter will automatically install everything you need -
- ### 🛠️ Method 2: Manual Installation
@@ -150,267 +179,1148 @@ Two-way communication between web and Rainmeter: Create a new skin with this minimal configuration: +
+skin.ini + ```ini [Rainmeter] Update=1000 -[MeasureWebView] +[WebView2] Measure=Plugin Plugin=WebView2 URL=file:///#@#index.html W=800 H=600 +X=0 +Y=0 + +[Background] +Meter=Image +W=800 +H=600 +x=0 +Y=0 +SolidColor=0,0,0,1 ``` +
Create `index.html` in your `@Resources` folder: +
+index.html + ```html - - - - -

🎉 Hello Rainmeter!

- + + + + +

🎉 Hello Rainmeter!

+

Hold CTRL to drag the skin

+

CTRL + Right Click to open the Skin Menu

+ ``` +
**That's it!** Load the skin and see your first WebView in action. --- -## ⚙️ Configuration Options +## ⚙️ Measure + +
+👑 Measure Values + +### 👑 Measure Values + +WebView's measure returns the following values: + +--- + +**Number Value** + +The number value represents the internal state of the WebView2 instance while it's initializing. When the WebView2 instance is initialized, the number value represent's the navigation state. +* `-2` WebView failed during initialization. +* `-1` WebView2 is idle. +* `0` WebView2 is initializing. +* `1` WebView2 is initialized. +* `100` Navigation has started. +* `200` Web content is loading. +* `300` Web is loaded. +* `400` Navigation has finished. + +Whenever the state changes, the plugin triggers `OnStateChangeAction`. + +--- + +**String Value** + +The string value represents the current URL. This URL will change when the user navigates through WebView either internally by clicking on links or externally by using commands. +* `CurrentURL` + +Whenever the URL changes, the plugin triggers `OnURLChangeAction`. + +
+ +
+🛠️ Measure Options + +### 🛠️ Measure Options +
- - - - + + + + + + + + - - - - + - - - - + + + + + + + - - - - + + + + + + + - - - - + - - - - + + + + + + + - - - - + + + + + - - - - + + + + + + + - - - - + - -
OptionDescriptionDefaultExampleOptionDescriptionDefaultExamples
URL🌐 HTML file or web URL
Supports: file:///, http://, https://
Requiredfile:///#@#index.htmlWebView Options
W📏 Width in pixels8001920
AutoStart +Automatically load WebView on skin load.
+0 = Disabled, +1 = Enabled +
1AutoStart=0
H📏 Height in pixels6001080URL +The URL WebView will navigate to when it starts.
+This is the URL the Navigate Home command navigates to.
+Supports: file paths and web URLs
+Supported schemes: file:///, http://, https://, view-source:
+Relative paths are also supported: URL=file.html
+Paths are relative to #CURRENTPATH#.
+When HostPath is set to a valid folder path, this option is relative to that folder and will navigate to the default virtual host: https://rootconfig/file.html
+See Setting Up a Virtual Host. +
Required +URL=file:///#@#file.html
+URL=http://example.com
+URL=path\to\file.html
+URL=path\to\img.gif
+URL=file.html +
X↔️ Horizontal position offset0100Virtual Host Options
Y↕️ Vertical position offset050
HostSecurity +Set a preferred protocol for the virtual host:
+0 = http (not secure), 1 = https (secure)
+Https allows to use JavaScript APIs that are normally blocked by CORS.
+See Setting Up a Virtual Host. +
1HostSecurity=0
Hidden👁️ Start hidden
0 = visible, 1 = hidden
01HostOrigin +Set a preferred host name for the virtual host:
+0 = current-config (not shared), 1 = rootconfig (shared)
+When current-config 0 is used, the local storage is isolated to the current skin's origin, eg. https://illustro-clock
+When rootconfig 1 is used, the local storage is shared between all configs that are part of the same suite, eg. https://illustro
+See Setting Up a Virtual Host. +
1HostOrigin=0
Clickthrough🖱️ Mouse interaction
0 = interactive, 1 = clickthrough
01HostPath +The path to the folder that contains the index.html that the virtual host will load.
+When this option is set to a folder path, the URL option will become relative to the path set on this option.
+See Setting Up a Virtual Host. +
"" +HostPath=path\to\folder
+HostPath=#CURRENTPATH#
+HostPath=#@# +
DynamicVariables🔄 Enable live updates01Window Options
+ -> **💡 Pro Tip:** When `DynamicVariables=1`, the WebView updates smartly: -> - **URL changes** → Navigates without recreating -> - **Size/Position changes** → Applied instantly, no flicker -> - **Visibility changes** → Instant toggle + +W +Window width (pixels) +800 +W=1920 + ---- + +H +Window height (pixels) +600 +H=1080 + -## 🎮 Bang Commands + +X +Horizontal position offset +0 +X=100 + -Control your WebView with Rainmeter bangs: + +Y +Vertical position offset +0 +Y=50 + - - + + + + -**Navigation Commands** + + + + + + -```ini -; Go to a URL -[!CommandMeasure MeasureWebView "Navigate https://example.com"] + + + + + + -; Reload current page -[!CommandMeasure MeasureWebView "Reload"] + -; Browser history -[!CommandMeasure MeasureWebView "GoBack"] -[!CommandMeasure MeasureWebView "GoForward"] -``` + + + + + + + - + + -**Control Commands** + + + + + + -```ini -; Execute JavaScript -[!CommandMeasure MeasureWebView "ExecuteScript alert('Hi!')"] + + + + + + -; Developer tools -[!CommandMeasure MeasureWebView "OpenDevTools"] -``` + + + + + + + + + + + +
+ZoomFactorSite zoom factor1.0ZoomFactor=1.5
Hidden +Window visibility
+ +0 = Visible, +1 = Hidden + +
0Hidden=1
Clickthrough +Mouse interaction mode
+ +0 = Interactive, +1 = Click Through, +2 = Press CTRL to toggle
+This option is very usefull to drag the skin, while set to 2 press CTRL to drag the skin and RMB to open the Skin Menu. +
+
2Clickthrough=1
Browser Options
UserAgent +Custom user-agent string override +""UserAgent=MyBrowser/1.0
ZoomControl +Allow user-controlled zoom through:
+CTRL+SCROLL, CTRL + PLUS, CTRL + MINUS and CTRL + 0
+0 = Disabled, +1 = Enabled +
1ZoomControl=0
NewWindow +Allow opening links in a new window
+0 = Disabled, +1 = Enabled +
0NewWindow=1
Notifications +Override JavaScript's Notification API permission.
+0 = Deny, +1 = Allow +
0Notifications=1
AssistiveFeatures +Allow Print, Find and Caret Browsing features.
+0 = Disabled, +1 = Enabled
1AssistiveFeatures=0
---- +> **💡 Pro Tip:** When `DynamicVariables=1`, the WebView updates smartly: +> - **URL changes** → Sets a new Home Page +> - **Size/Position changes** → Applied instantly, no flicker +> - **Visibility changes** → Instant toggle +> - All options can be updated dynamically -## 🔥 JavaScript Integration + -### Lifecycle Hooks +
+▶️ Measure Actions -Your JavaScript can respond to Rainmeter events: +### ▶️ Measure Actions -```javascript -// Called once when plugin is ready -window.OnInitialize = function() { - console.log("🚀 WebView initialized!"); - return "Ready!"; // This becomes the measure's value -}; -// Called on every Rainmeter update -window.OnUpdate = function() { - const now = new Date().toLocaleTimeString(); - return now; // Updates measure value -}; -``` + + + + + + + + -> ⚠️ **Note:** JavaScript execution is asynchronous, so there's a 1-update delay between JS return and Rainmeter display. This is normal! + -### Call JavaScript from Rainmeter + + + + -Use section variables to call any JavaScript function: + + + + + -```ini -[MeterTemperature] -Meter=String -Text=Current temp: [MeasureWebView:CallJS('getTemperature')]°C -DynamicVariables=1 -``` + + + + + -```javascript -// In your HTML -window.getTemperature = function() { - return 72; -}; -``` + + + + + ---- + + + + + -## 🌉 RainmeterAPI Bridge + -Access Rainmeter's full power from JavaScript: + + + + -### Read Skin Options + + + + + -```javascript -// Read from your measure -const refreshRate = await RainmeterAPI.ReadInt('UpdateRate', 1000); -const siteName = await RainmeterAPI.ReadString('SiteName', 'Default'); + + + + + -// Read from other sections -const cpuUsage = await RainmeterAPI.ReadStringFromSection('MeasureCPU', 'Value', '0'); -``` + + + + + -### Execute Bangs + + + + + -```javascript -// Set variables -await RainmeterAPI.Bang('!SetVariable MyVar "Hello World"'); + + + + + -// Control skins -await RainmeterAPI.Bang('!ActivateConfig "MySkin" "Variant.ini"'); + + + + + -// Update meters -await RainmeterAPI.Bang('!UpdateMeter MeterName'); -await RainmeterAPI.Bang('!Redraw'); -``` + + + + + + +
ActionDescriptionExample
WebView State Actions
OnWebViewLoadAction + Triggers when WebView starts. +OnWebViewLoadActio=[!log "WebView2 loaded succesfully!"]
OnWebViewFailAction +Triggers when WebView fails. +OnWebViewFailAction=[!log "WebView2 failed :("]
OnWebViewStopAction +Triggers when WebView stops. +OnWebViewStopAction=[!log "WebView2 has stopped!"]
OnStateChangeAction +Triggers when WebView initialization or navigation states change. +OnStateChangeAction=[!UpdateMeasure #CURRENTSECTION#][!UpdateMeter CurrentState][!Redraw]
Navigation Actions
OnUrlChangeAction +Triggers when the current URL changes. +OnUrlChangeAction=[!UpdateMeasure #CURRENTSECTION#][!UpdateMeter CurrentURL][!Redraw]
OnPageLoadStartAction +Triggers when navigation starts. +OnPageLoadStartAction=[!log "Navigation has started!"]
OnPageLoadingAction +Triggers when the page starts loading. +OnPageLoadingAction=[!log "Page is loading!"]
OnPageDOMLoadAction +Triggers when the DOM content is loaded. +OnPageDOMLoadAction=[!log "DOM content loaded!"]
OnPageFirstLoadAction +Triggers the first time a page is loaded. +OnPageFirstLoadAction=[!log "First time on this page!"]
OnPageReloadAction +Triggers when the page is reloaded. +OnPageReloadAction=[!log "Page has been reloaded!"]
OnPageLoadFinishAction +Triggers when the navigation is finished. +OnPageLoadFinishAction=[!log "Navigation has finished!"]
-### Get Skin Information +
-```javascript -const skinName = await RainmeterAPI.SkinName; -const measureName = await RainmeterAPI.MeasureName; +
+💥 Bang Commands -// Replace variables -const path = await RainmeterAPI.ReplaceVariables('#@#images/logo.png'); +### 💥 Bang Commands -// Get variable values -const theme = await RainmeterAPI.GetVariable('CurrentTheme'); -``` +Control your WebView with Rainmeter bangs: -### Logging + + + + + + + + + -```javascript -await RainmeterAPI.Log('Debug info', 'DEBUG'); -await RainmeterAPI.Log('Warning message', 'WARNING'); -await RainmeterAPI.Log('Error occurred', 'ERROR'); -``` + + + + + -### Complete API Reference + + + + + -
-📚 Click to see all available methods +
+ + + + -
+ + + + -**Reading Options** -- `ReadString(option, defaultValue)` → `Promise` -- `ReadInt(option, defaultValue)` → `Promise` -- `ReadDouble(option, defaultValue)` → `Promise` -- `ReadFormula(option, defaultValue)` → `Promise` -- `ReadPath(option, defaultValue)` → `Promise` + + + + + -**Reading from Sections** + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescriptionExample
WebView
WebView StartStarts the WebView instance.[!CommandMeasure WebView2 "WebView Start"]
WebView StopStops the WebView instance.[!CommandMeasure WebView2 "WebView Stop"]
WebView RestartRestarts the WebView instance.[!CommandMeasure WebView2 "WebView Restart"]
Navigate
Navigate HomeNavigates to URL option.[!CommandMeasure WebView2 "Navigate Home"]
Navigate BackNavigates to the previous page.[!CommandMeasure WebView2 "Navigate Back"]
Navigate ForwardNavigates to the next page.[!CommandMeasure WebView2 "Navigate Forward"]
Navigate ReloadReloads the current page.[!CommandMeasure WebView2 "Navigate Reload"]
Navigate StopStops any navigation.[!CommandMeasure WebView2 "Navigate Stop"]
Navigate URLNavigates to a URL.[!CommandMeasure WebView2 "Navigate http://example.com"]
Open
Open DevToolsOpens DeveloperTools.[!CommandMeasure WebView2 "Open DevTools"]
Open TaskManagerOpens the web task manager.[!CommandMeasure WebView2 "Open TaskManager"]
Execute
+Execute Script
+Execute File.js
+
Executes given JS script or .js script file +[!CommandMeasure WebView2 "Execute alert('Hello Rainmeter!')"]
+[!CommandMeasure WebView2 "Execute #@#script.js"] +
+ +
+ + +
+🗺️ Defaults Map + +### 🗺️ Defaults Map + + +```ini +[WebView2] +Measure=Plugin +Plugin=WebView2 + +; WebView Options +AutoStart=1 +URL="" + +; Virtual Host Options +HostSecurity=1 +HostOrigin=1 +HostPath="" + +; Window Options +W=800 +H=600 +X=0 +Y=0 +ZoomFactor=1.0 +Hidden=0 +Clickthrough=2 + +;Browser Options +UserAgent="" +ZoomControl=1 +NewWindow=0 +Notifications=0 +AssistiveFeatures=1 + +; WebView State Actions +OnWebViewLoadAction=[] +OnWebViewFailAction=[] +OnWebViewStopAction=[] + +; Navigation State Actions +OnStateChangeAction=[] +OnUrlChangeAction=[] +OnPageLoadStartAction=[] +OnPageLoadingAction=[] +OnPageDOMLoadAction=[] +OnPageFirstLoadAction=[] +OnPageReloadAction=[] +OnPageLoadFinishAction=[] + +--- + +; WebView Commands +[!CommandMeasure WebView2 "WebView Start"] +[!CommandMeasure WebView2 "WebView Stop"] +[!CommandMeasure WebView2 "WebView Restart"] + +; Navigation Commands +[!CommandMeasure WebView2 "Navigate Home"] +[!CommandMeasure WebView2 "Navigate Back"] +[!CommandMeasure WebView2 "Navigate Forward"] +[!CommandMeasure WebView2 "Navigate Reload"] +[!CommandMeasure WebView2 "Navigate Stop"] +[!CommandMeasure WebView2 "Navigate http://www.example.com"] + +; Open Commands +[!CommandMeasure WebView2 "Open DevTools"] +[!CommandMeasure WebView2 "Open TaskManager"] + +; Execute Commands +[!CommandMeasure WebView2 "Execute alert('Hello Rainmeter!')"] +[!CommandMeasure WebView2 "Execute path\to\file.js"] + +;Section Variables +[WebView2:CallJS('alert("Example script")')] + +;User Data Folder Path +C:\Users\User\AppData\Local\Temp\RainmeterWebView2\ + +``` +
+ +
+📂 User Data Folder + +### User Data Folder + +``` +📁 RainmeterWebView2\ + ├── 📁 EBWebView\ + │ └── 📁 [WebView2 Data]\ + ├── 📁 Extensions\ + │ └── Extensions.ini + │ └── 📁 MyExtension\ + │ └── 📁 MyOtherExtension\ + └── UserSettings.ini +``` + +When the plugin loads for the first time, a user data folder (UDP) is created. This folder contains all the data related to WebView2. + +*Path: `C:\Users\User\AppData\Local\Temp\RainmeterWebView2\`* + +If deleted, the folder will be re-created the next time the plugin is loaded. + +To delete all your WebView2 data, it is recommended to delete `RainmeterWebView2\EBWebView\` instead. This way you preserve your [User Settings File](#user-settings-file) and [Extensions](#extensions). + +> **💡 IMPORTANT:** +> - Exit Rainmeter before modifying anything in this folder, failing to do so will make WebView2 instances fail to start. +> - Restart Rainmeter if this happens. + + +### User Settings File + +There are certain settings that affect all WebView2 instances, for this reason they can't be exposed through the measure's options. + +Such settings can be found in a file called `UserSettings.ini`. + +*Path: `C:\Users\User\AppData\Local\Temp\RainmeterWebView2\UserSettings.ini`* + +
+Options + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionDescriptionDefault
Environment Options
ExtensionsAllows to use extensions.false
FluentOverlayScrollBarsEnable Fluent Overlay scrollbars.true
TrackingPreventionEnables Microsoft Edge tracking prevention.true
BrowserArgumentsAdditional command-line arguments passed to the WebView2 browser process.--allow-file-access-from-files
BrowserLocaleSets the browser UI locale. Use system to follow the OS language.
+Locales need to be in the format of BCP 47 Language Tags. e.g:
+en-US es-MX fr-FR
+A list of locales can be found here +
system
Controller Options
PrivateModeEnables private mode, also commonly called incognito mode.false
ScriptLocale +Locale used for JavaScript APIs such as date, time, and number formatting.
+Use system to follow the OS language.
+Locales need to be in the format of BCP 47 Language Tags. e.g:
+en-US es-MX fr-FR
+A list of locales can be found here +
system
Core Options
StatusBarShows or hides the browser status bar.true
PinchZoomEnables pinch-to-zoom gestures.true
SwipeNavigationEnables swipe gestures for back and forward navigation.true
SmartScreen +Enables SmartScreen protection. More info. +true
Profile Options
DownloadsFolderPathCustom folder path where downloaded files are saved. Empty uses the system default.
ColorSchemeControls the browser color scheme (light, dark, or system).system
PasswordAutoSaveAllows automatic saving of passwords.false
GeneralAutoFillEnables form autofill for non-password fields.true
+ +
+UserSettings.ini + +```ini +[Environment] +Extensions = false +FluentOverlayScrollBars = true +TrackingPrevention = true +BrowserLocale = system +BrowserArguments = --allow-file-access-from-files + +[Controller] +ScriptLocale = system +PrivateMode = false + +[Core] +StatusBar = true +PinchZoom = true +SwipeNavigation = true +SmartScreen = true + +[Profile] +DownloadsFolderPath = +ColorScheme = system +PasswordAutoSave = false +GeneralAutoFill = true + +``` + +
+ +
+ +### Extensions + +Using extensions on WebView2 is now possible! + +Unfortunately, this feature doesn't come without limitations. + +**Limitations** + +- Extensions' UI may not show up at all, or they may show up on the DevTools(F5) window. + +> ⚠️ Important: +> - Manipulating Extensions require Rainmeter to be exited. + +*Path: `C:\Users\User\AppData\Local\Temp\RainmeterWebView2\Extensions\`* + +
+Installing Extensions + +### Installing Extensions + +To install an extension: + +1. Exit Rainmeter +2. Go to `UserSettings.ini` and enable extensions -> `Extensions=true` +3. Drop an unpacked extension folder inside the `Extensions\` folder +4. Start Rainmeter +5. Done + +On Rainmeter, `WebView2: "Extension Name" extension installed.` will be logged. + +Once an extension is installed, a new ini file will be created at `Extensions\Extensions.ini`, where you can control your extensions. + +> ⚠️ Important: +> - Extensions must be **unpacked** folders. + +
+ +
+Toggling Extensions + +### Toggling Extensions + +To enable\disable an extension: + +1. Exit Rainmeter +2. Open `Extensions\Extensions.ini` +3. Find `[YourExtensionFolderName]` section +4. Set `Enabled=false` or `Enabled=false` +5. Save the file. +6. Launch Rainmeter +7. Done + +On Rainmeter, `WebView2: "Extension Name" extension enabled\disabled.` will be logged. + +
+ +
+Unninstalling Extensions + +### Unninstalling Extensions + +To uninstall an extension: + +1. Exit Rainmeter +2. Open `Extensions\Extensions.ini` +3. Find `[YourExtensionFolderName]` section +4. Set `Uninstall=true` +5. Save the file +6. Launch Rainmeter +7. Manually remove the extension's unpacked folder from `Extensions\` +8. Done + +On Rainmeter, `WebView2: "Extension Name" extension removed.` will be logged. + +> ⚠️ Important: +> * Uninstalling an extension will automatically delete its `[section]` from `Extensions.ini`, but will **not** remove its folder from the `Extensions\` folder. +> * If the folder is not manually removed after uninstalling the extension, it will be automatically re-installed the next time you launch Rainmeter. + +
+ +
+Extensions.ini + +*Path: `C:\Users\User\AppData\Local\Temp\RainmeterWebView2\Extensions\Extensions.ini`* + +```ini +[TheExtensionFolderName] +ID = theextensionid +Name = The Extension Name +Enabled = true +Uninstall = false +``` + +> ⚠️ Important: +> - You can only modify `Enabled` and `Unninstall`, other options are informative only. +> - Modifying or deleting `ID` will cause the extension to be reinstalled. +> - Modifying or deleting `Name` won't do anything, the name will be restored next time the plugin loads. + +
+ + +
+ +--- + +## 🔥 JavaScript Integration + +### Lifecycle Hooks + +Your JavaScript can respond to Rainmeter events: + +```javascript +// Called once when navigation starts +window.OnInitialize = function() { + console.log("🚀 WebView initialized!"); + RainmeterAPI.Bang('[!Log "🚀 WebView initialized!"]') +}; + +// Called on every Rainmeter update +window.OnUpdate = function() { + const now = new Date().toLocaleTimeString(); + updateSomething(now); +}; +``` + +### Call JavaScript from Rainmeter + +Use section variables to call any JavaScript function: + +```ini +[MeterTemperature] +Meter=String +Text=Current temp: [WebView2:CallJS('getTemperature')]°C +DynamicVariables=1 +``` + +```javascript +// In your HTML +window.getTemperature = function() { + return 72; +}; +``` +> ⚠️ **Note:** JavaScript execution is asynchronous, so there's a 1-update delay between JS return and Rainmeter display. This is normal! + +### Inject JS to Web Sites + +From inline one-liner strings: + +```ini +[WebView2] +Measure=Plugin +Plugin=WebView2 +URL=https://example.com/ +OnPageLoadFinishAction=[!CommandMeasure WebView2 "Execute alert('script executed'); console.log('hola!');"] +``` + +From files: + +```js +// @Resources\script.js + +alert('script executed from file'); +console.log('hola!'); +``` + +```ini +; skin.ini + +[WebView2] +Measure=Plugin +Plugin=WebView2 +URL=https://example.com/ +OnPageLoadFinishAction=[!CommandMeasure WebView2 "Execute #@#script.js"] +``` + +### Use app-region CSS Style + +Dragging elements with `app-region: drag;` set up will move the skin window. Right clicking these elements also opens the Skin Menu. + +```css +body { + app-region: drag; +} + +button { + app-region: no-drag; +} +``` + +When using `drag` on a div or any other container, you need to manually set `no-drag` on the interactable children of that container, otherwise they won't work properly. + +This CSS style is being used on the `YoutubePlayer` example skin. + +--- + +## 🌉 RainmeterAPI Bridge + +Access Rainmeter's full power from JavaScript: + +### Read Skin Options + +```javascript +// Read from your measure +const refreshRate = await RainmeterAPI.ReadInt('UpdateRate', 1000); +const siteName = await RainmeterAPI.ReadString('SiteName', 'Default'); + +// Read from other sections +const cpuUsage = await RainmeterAPI.ReadStringFromSection('MeasureCPU', 'Value', '0'); +``` + +### Execute Bangs + +```javascript +// Set variables +await RainmeterAPI.Bang('!SetVariable MyVar "Hello World"'); + +// Control skins +await RainmeterAPI.Bang('!ActivateConfig "MySkin" "Variant.ini"'); + +// Update meters +await RainmeterAPI.Bang('!UpdateMeter MeterName'); +await RainmeterAPI.Bang('!Redraw'); +``` + +### Get Skin Information + +```javascript +const skinName = await RainmeterAPI.SkinName; +const measureName = await RainmeterAPI.MeasureName; + +// Replace variables +const path = await RainmeterAPI.ReplaceVariables('#@#images/logo.png'); + +// Get variable values +const theme = await RainmeterAPI.GetVariable('CurrentTheme'); +``` + +### Logging + +```javascript +await RainmeterAPI.Log('Debug info', 'DEBUG'); +await RainmeterAPI.Log('Warning message', 'WARNING'); +await RainmeterAPI.Log('Error occurred', 'ERROR'); +``` + +### Complete API Reference + +
+📚 Click to see all available methods + +
+ +**Reading Options** +- `ReadString(option, defaultValue)` → `Promise` +- `ReadInt(option, defaultValue)` → `Promise` +- `ReadDouble(option, defaultValue)` → `Promise` +- `ReadFormula(option, defaultValue)` → `Promise` +- `ReadPath(option, defaultValue)` → `Promise` + +**Reading from Sections** - `ReadStringFromSection(section, option, defaultValue)` → `Promise` - `ReadIntFromSection(section, option, defaultValue)` → `Promise` - `ReadDoubleFromSection(section, option, defaultValue)` → `Promise` @@ -433,28 +1343,451 @@ await RainmeterAPI.Log('Error occurred', 'ERROR'); --- +## ❓ How to Make a Draggable Skin + +There are currently two ways to make WebView2 skins draggable. + +
+Using Clickthrough + +`Clickthrough` is a powerful option that allows mouse input to pass through the WebView2 window to the skin layer behind it, effectively making the window invisible to mouse interactions. + +A new value, `2`, was recently added. This makes `Clickthrough` toggleable by holding the `CTRL` key. + +**What do you need to do?** Nothing. + +By default, `Clickthrough=2`. To drag the skin, simply **hold `CTRL` and drag**. This works because you are interacting directly with the skin, not with the WebView2 window. +You can also open the Skin Menu with `CTRL + RMB`. +`Calendar.ini` uses this method. + +```ini +[WebView2] +Measure=Plugin +Plugin=WebView2 +X=0 +Y=0 +W=500 +H=500 +Clickthrough=2 +``` + +Alternatively, you can use `Clickthrough=1` if the skin is only displaying information or images. This removes the need to hold CTRL, but disables all interaction with the WebView2 window. +`Clock.ini` uses this method. + +```ini +[WebView2] +Measure=Plugin +Plugin=WebView2 +X=0 +Y=0 +W=500 +H=500 +Clickthrough=1 +``` + +
+ +
+Using app-region + +WebView2 supports the `app-region` CSS property to define draggable and non-draggable areas on a page, allowing to drag the skin without holding `CTRL`. +This is a more advanced way to do it, but it will behave much more skin-like than the `clickthrough` way. `YouTubePlayer.ini` uses this method. + +The property has two values: + +* `drag` +* `no-drag` + +--- + +*`app-region: drag`* + +* Dragging the element moves the window +* Right-click opens the Skin Menu +* All child elements inherit this behavior + +Use for non-interactive areas such as backgrounds or title bars. + +```css +.titlebar { + app-region: drag; +} +``` + +--- + +*`app-region: no-drag`* + +* Disables window dragging +* Enables normal mouse interaction +* Overrides inherited `drag` + +Required for buttons, inputs, sliders, and other interactive elements. + +```css +.button, +input { + app-region: no-drag; +} +``` + +--- + +*Common Pattern* + +```css +.window { + app-region: drag; +} + +.controls { + app-region: no-drag; +} +``` + +This keeps the window draggable while preserving UI interactivity. + +You can also apply `app-region` directly in HTML using inline styles. + +```html +
+ My Skin + +
+``` + +In this example, the title bar remains draggable while the button stays fully interactive. + +
+Draggable skin example + +Load this file using a default WebView2 measure: + +index.html +```html + + + + +

Draggable Skin Example

+

Drag the skin

+

you can drag over everything

+ +

hold CTRL and drag over the button

+ + +``` + +
+ +
+ +--- + +## ☄️ Setting Up a Virtual Host + +Some APIs and features do not function correctly when accessed via the `file:///` protocol. + Previously, this limitation required installing an external `http-server` to enable proper behavior. + WebView2 now provides a built-in **Virtual Host** feature, which allows a plugin to be served from a virtual `http` or `https` URL, effectively replicating the behavior of a local web server. + In practice, this means you can generate and use a custom virtual URL such as `https://my-skin-config-name/` or `http://my-skin-config-name/`. + +This feature provides the following configuration options: + +--- + + **`HostSecurity`** (default: `1`) + Specifies which protocol the virtual host will use. + `0` - Not secure (`http`) + `1` - Secure (`https`) + +--- + + **`HostOrigin`** (default: `1`) + Defines which config is used as the origin for the virtual host. + `0` - Current config + `1` - Root config + + This setting also determines how Local Storage is scoped: + - Using **Current config** restricts Local Storage access to the active skin (config) only. + - Using **Root config** allows Local Storage to be shared across all skins (configs) under the same root, which is useful for suites. + +--- + + **`HostPath`** (default: `""`) + Specifies the path to the folder containing your `file.html`. + Example: `HostPath = #@#` + + When this option is set, the plugin enables the Virtual Host feature and generates a virtual URL mapped to the specified folder. The resulting URL uses a protocol defined by `HostSecurity` and a host name derived from the `HostOrigin` setting. + +--- + +
+How it works: + +The plugin detects that a Virtual Host should be used when the `HostPath` option points to a valid folder. Once detected, it initializes the virtual host and generates a new base `URL` that can be used to access the mapped files. + +
+Example 1: Current Config as Origin (Isolated Storage) + +Assume the following code belongs to the `Illustro\Clock` skin (config): + +```ini +; HostSecurity: +; 0 = http (insecure context) +; 1 = https (secure context) +; Using HTTPS allows access to APIs that may be blocked by CORS. +HostSecurity=1 + +; HostOrigin: +; 0 = current config only +; 1 = root config +; Using current config isolates storage to this skin +HostOrigin=0 + +; Folder containing the HTML and assets +HostPath=#@# + +URL=index.html +``` + +In this case: + +* The protocol is set to `https` + +* The origin is derived from the current config: `Illustro\Clock` + +* The `index.html` file resides in the `@Resources` folder + +The generated base URL will be: + +``` +https://illustro-clock/ +``` + +You can navigate to the page explicitly: +```ini +URL=https://illustro-clock/index.html +``` + +Or implicitly: +```ini +URL=index.html +``` + +When using a Virtual Host, the `URL` option automatically resolves relative paths against the generated virtual host URL. + +Local Storage will be isolated to the `Illustro\Clock` skin, meaning it is scoped to the following origin: + +``` +https://illustro-clock/ +``` +
+ +
+Example 2: Root Config as Origin (Shared Storage) + +Assume the following code belongs to the Illustro\Clock skin (config): + +```ini +HostSecurity=0 +HostOrigin=1 +HostPath=#@#Clock\ + +URL=clock.html +``` + +In this case: + +* The protocol is set to `http` + +* The origin is derived from the root config: `Illustro` + +* The `clock.html` file resides in the `@Resources\Clock` folder + +The generated base URL will be: +``` +http://illustro/ +``` + +You can navigate to the page explicitly: + +```ini +URL=http://illustro/clock.html +``` + +Or implicitly: + +```ini +URL=clock.html +``` + +Local Storage will be shared across all skins (configs) that belong to the `Illustro` root config. In other words, Local Storage is scoped to: + +``` +http://illustro/ +``` + +
+ +
+Practical Example + +Assume the following skin structure: + +``` +📁 MyRootConfig\ + ├── 📁 @Resources\ + │ └── index.html + └── MyRootConfigIni.ini +``` + +
+MyRootConfigIni.ini + +```ini +[Rainmeter] +Update=1000 + +[WebView2] +Measure=Plugin +Plugin=WebView2 +HostPath=#@# +Url=Index.html +W=300 +H=300 + +[WebView2BG] +Meter=Image +W=300 +H=300 +SolidColor=0,0,0,255 +``` +
+ +
+Index.html + +```html + + + + + +

Hello, Rainmeter!

+

This is a very simple HTML page.

+

Hold CTRL to drag the skin

+

Press CTRL + RMB to open SkinMenu.

+ + + +``` +
+ +After loading the skin, check the `skins` tab on the `About` window. You'll see that the measure is returning a URL as its string value, which is `https://myrootconfig/Index.html`. + +By default, `HostSecurity=1`, therefore, as seen on the URL, it's using the `https` protocol. + +Also, `HostOrigin=1` by default, but in this example it doesn't matter given our skin's structure. This skin has only a root config, so that's the only host name we can use. Which is the `myrootconfig/` part on the URL. + +If we now open `DevTools` (press F12 inside the WebView window) and go to the `Aplication` tab, then go to `Storage` -> `Local storage` on the left side panel, we'll see that our `https://myrootconfig` origin is listed. + +Clicking on our origin will show all the key-value pairs that are stored. Obviously it is empty if you haven't saved anything yet. Check the storage for `YoutubePlayer` example skin instead. + + +--- + +Now try using this skin's structure and play with both `HostSecurity` and `HostOrigin`. + +``` +📁 MyRootConfig\ + ├── 📁 @Resources\ + │ └── index.html + ├── 📁 MyOtherConfig\ + │ └── MyOtherConfigIni.ini + └── MyRootConfigIni.ini +``` + +Try setting `HostOrigin=0` on both configs, you'll see the same `https://myrootconfig` origin on `DevTools` for both configs, which means they share local storage. + +If you then set `HostOrigin=1` on `MyOtherConfig`, you'll see its origin is now `https://myrootconfig-myotherconfig`, which means its local storage is not shared. + + +
+ +
+ +
+ + + +--- + ## 💡 Examples The plugin includes ready-to-use example skins: - - - - + +
+ 🕐 Clock
Animated liquid clock with smooth animations
+ 📅 Calendar
Interactive month view calendar
+ ⚙️ Config Reader
Read options from measures and sections
+ 🔧 Utilities
Demonstrate all API functions
+▶️ Youtube Player
+Youtube Player iFrame API example +
@@ -562,7 +1895,7 @@ document.addEventListener('DOMContentLoaded', () => { - ✅ Check URL path is correct - ✅ Verify HTML file exists - ✅ Look for errors in Rainmeter log -- ✅ Try: `[!CommandMeasure MeasureName "OpenDevTools"]` to debug +- ✅ Try: `[!CommandMeasure MeasureName "Open DevTools"]` to debug **Transparency tip:** The WebView has transparent background by default. Use `background: transparent;` in your CSS. diff --git a/Resources/Skins/WebView2/BangCommand/BangCommand.ini b/Resources/Skins/WebView2/BangCommand/BangCommand.ini index 033bd01..6116152 100644 Binary files a/Resources/Skins/WebView2/BangCommand/BangCommand.ini and b/Resources/Skins/WebView2/BangCommand/BangCommand.ini differ diff --git a/Resources/Skins/WebView2/Calendar/Calendar.ini b/Resources/Skins/WebView2/Calendar/Calendar.ini index 489531b..e670b04 100644 --- a/Resources/Skins/WebView2/Calendar/Calendar.ini +++ b/Resources/Skins/WebView2/Calendar/Calendar.ini @@ -2,10 +2,10 @@ ;Since this is a pure WebView2 skin, we don't need to update the skin. Update=-1 ; Add useful commands to the Skin Menu. -; We use ExecuteScript command to toggle locale. +; We use Execute command to toggle locale. ; Check implementation at @Resources\Calendar\script.js ContextTitle=Toggle Locale -ContextAction=[!CommandMeasure WebView2 "ExecuteScript toggleLocale();"] +ContextAction=[!CommandMeasure WebView2 "Execute toggleLocale();"] ContextTitle2=Open DevTools ContextAction2=[!CommandMeasure WebView2 "Open DevTools"] ContextTitle3= Open Task Manager diff --git a/Resources/Skins/WebView2/Clock/Clock.ini b/Resources/Skins/WebView2/Clock/Clock.ini index 2cb36e4..6e2c99d 100644 --- a/Resources/Skins/WebView2/Clock/Clock.ini +++ b/Resources/Skins/WebView2/Clock/Clock.ini @@ -2,12 +2,12 @@ ;Since this is a pure WebView2 skin, we don't need to update the skin. Update=-1 ; Add useful commands to the Skin Menu. -; We use ExecuteScript command to toggle format and locale. +; We use Execute command to toggle format and locale. ; Check implementation at @Resources\Clock\script.js ContextTitle= Toggle Format -ContextAction=[!CommandMeasure WebView2 "ExecuteScript toggleFormat();"] +ContextAction=[!CommandMeasure WebView2 "Execute toggleFormat();"] ContextTitle2=Toggle Locale -ContextAction2=[!CommandMeasure WebView2 "ExecuteScript toggleLocale();"] +ContextAction2=[!CommandMeasure WebView2 "Execute toggleLocale();"] ; Other useful commands: ContextTitle3=Open DevTools ContextAction3=[!CommandMeasure WebView2 "Open DevTools"] diff --git a/WebView2/Plugin.cpp b/WebView2/Plugin.cpp index 54c957b..ae4d1e6 100644 --- a/WebView2/Plugin.cpp +++ b/WebView2/Plugin.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #pragma comment(lib, "comctl32.lib") #pragma comment(lib, "ole32.lib") @@ -73,9 +73,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) // Measure constructor Measure::Measure() : rm(nullptr), skin(nullptr), skinWindow(nullptr), -measureName(nullptr), -width(800), height(600), x(0), y(0), -webMessageToken{} +measureName(nullptr), webMessageToken{} { // Initialize COM for this thread if not already done if (!g_comInitialized) @@ -145,14 +143,9 @@ void UpdateWindowBounds(Measure* measure) if (!measure || !measure->webViewController) return; - RECT bounds{ - measure->x, - measure->y, - measure->x + measure->width, - measure->y + measure->height - }; + measure->webViewArea = {measure->x, measure->y, measure->x + measure->width, measure->y + measure->height}; - measure->webViewController->put_Bounds(bounds); + measure->webViewController->put_Bounds(measure->webViewArea); measure->webViewController->NotifyParentWindowPositionChanged(); } @@ -220,9 +213,88 @@ LRESULT CALLBACK SkinSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lP if (!skinData || skinData->destroying) return DefSubclassProc(hWnd, uMsg, wParam, lParam); + switch (uMsg) { - case WM_APP_CTRL_CHANGED: + case WM_CONTEXTMENU: // Fix for SkinMenu opening when focused on WebView and the Menu key is pressed. + { + for (Measure* measure : skinData->measures) + { + if (measure && measure->initialized) + { + if (measure->isWebViewFocused) + return 0; + } + } + break; + } + case WM_SETCURSOR: // 1.Fix for app-region not opening SkinMenu. + { + const UINT hitTest = LOWORD(lParam); + const UINT mouseMsg = HIWORD(lParam); + + if (hitTest != HTCAPTION || mouseMsg != WM_RBUTTONDOWN) + break; + + // Mouse position + const DWORD pos = GetMessagePos(); + const POINT pt{ static_cast(LOWORD(pos)), static_cast(HIWORD(pos)) }; + + for (const Measure* measure : skinData->measures) + { + if (!measure || !measure->initialized) + continue; + + if (measure->clickthrough == 1 || measure->isClickthroughActive) + continue; + + // WebView area. + RECT rect{measure->webViewArea}; + MapWindowPoints(hWnd, nullptr, reinterpret_cast(&rect), 2); + + // Check if Click was inside the WebView area. + if (!PtInRect(&rect, pt)) + continue; + + // Click was inside the WebView area - Post custom message + PostMessage(measure->skinWindow, WM_APP_REGION_RMB, HTCAPTION, 0); + break; + } + break; + } + case WM_APP_REGION_RMB: // 2.Fix for app-region not opening SkinMenu. + { + for (const Measure* measure : skinData->measures) + { + if (!measure || !measure->initialized) + continue; + + if (measure->clickthrough == 1 || measure->isClickthroughActive) + continue; + + // Open SkinMenu + RmExecute(measure->skin, L"[!SkinMenu]"); + break; + } + break; + } + case WM_NCLBUTTONDOWN: // Clicking on the skin area removes focus from the WebView window. + { + HWND focused = GetFocus(); + + for (const Measure* measure : skinData->measures) + { + if (!measure || !measure->initialized) + continue; + + if (focused != measure->skinWindow) + SetFocus(nullptr); + + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + break; + } + case WM_APP_CTRL_CHANGED: // Clickthrough state control { bool isCtrlPressed = (bool)wParam; @@ -232,7 +304,7 @@ LRESULT CALLBACK SkinSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lP continue; measure->isCtrlPressed = isCtrlPressed; - + if (measure->clickthrough <= 1) continue; @@ -342,6 +414,158 @@ void RemoveKeyboardHook() } } +static std::wstring Utf8ToWstring(const char* data, int len) +{ + if (len <= 0) return std::wstring(); + + // First call to get required buffer size + int required = MultiByteToWideChar(CP_UTF8, 0, data, len, nullptr, 0); + if (required == 0) { + throw std::runtime_error("Utf8ToWstring: MultiByteToWideChar failed (size query)"); + } + + std::wstring out; + out.resize(required); + int res = MultiByteToWideChar(CP_UTF8, 0, data, len, &out[0], required); + if (res == 0) { + throw std::runtime_error("Utf8ToWstring: MultiByteToWideChar failed (conversion)"); + } + return out; +} + +std::wstring ReadScriptFile(const std::wstring& path) +{ + std::ifstream is(path, std::ios::binary); + if (!is) { + throw std::runtime_error("Failed to open file"); + } + + // Read all bytes + std::vector bytes((std::istreambuf_iterator(is)), std::istreambuf_iterator()); + size_t n = bytes.size(); + if (n == 0) return std::wstring(); + + const unsigned char* ub = reinterpret_cast(bytes.data()); + + // Detect BOMs + if (n >= 3 && ub[0] == 0xEFu && ub[1] == 0xBBu && ub[2] == 0xBFu) { + // UTF-8 with BOM -> skip BOM then convert + return Utf8ToWstring(reinterpret_cast(ub + 3), static_cast(n - 3)); + } + + if (n >= 2 && ub[0] == 0xFFu && ub[1] == 0xFEu) { + // UTF-16 LE with BOM + std::wstring out; + out.reserve((n - 2) / 2); + for (size_t i = 2; i + 1 < n; i += 2) { + wchar_t ch = static_cast(ub[i] | (ub[i + 1] << 8)); + out.push_back(ch); + } + return out; + } + + if (n >= 2 && ub[0] == 0xFEu && ub[1] == 0xFFu) { + // UTF-16 BE with BOM -> swap bytes + std::wstring out; + out.reserve((n - 2) / 2); + for (size_t i = 2; i + 1 < n; i += 2) { + wchar_t ch = static_cast((ub[i] << 8) | ub[i + 1]); + out.push_back(ch); + } + return out; + } + + // No BOM: assume UTF-8 and convert + return Utf8ToWstring(reinterpret_cast(ub), static_cast(n)); +} + +bool IsFilePathSyntax(LPCWSTR input) { + if (!input || input[0] == L'\0') return false; + + bool hasExtension = false; + bool hasSeparator = false; + bool hasIllegalChar = false; + + const wchar_t* lastDot = nullptr; + const wchar_t* p = input; + + while (*p) { + if (wcschr(L"<>:\"|?*();'", *p)) { + if (!(*p == L':' && p == input + 1)) { + return false; + } + } + + if (*p == L'\\' || *p == L'/') { + hasSeparator = true; + lastDot = nullptr; + } + else if (*p == L'.') { + lastDot = p; + } + p++; + } + + if (lastDot != nullptr && lastDot != input) { + if (!iswspace(*(lastDot + 1)) && *(lastDot + 1) != L'\0') { + hasExtension = true; + } + } + + bool hasSpace = (wcschr(input, L' ') != nullptr); + + if (hasSpace && !hasSeparator) { + return false; + } + + return hasSeparator || hasExtension; +} + +std::wstring NormalizePath(void* rm, LPCWSTR path) +{ + if (!path || !*path) + return {}; + + std::wstring value = path; + + // Relative path - absolute + if (value[0] != L'/' && (value.length() < 2 || value[1] != L':')) + { + if (LPCWSTR absolutePath = RmPathToAbsolute(rm, value.c_str())) + { + value = absolutePath; + } + } + + // Normalize slashes + for (wchar_t& ch : value) + { + if (ch == L'\\') ch = L'/'; + } + + // Enforce extension if required + auto dotPos = value.find_last_of(L'.'); + if (dotPos == std::wstring::npos) + { + RmLog(rm, LOG_ERROR, L"Execute: File extension is missing, use '.js'."); + return {}; + } + + std::wstring extension = value.substr(dotPos); + for (wchar_t& ch : extension) + ch = towlower(ch); + + if (_wcsicmp(extension.c_str(), L".js") != 0) + { + std::wstring msg = L"Execute: The file extension '"; + msg.append(extension); + msg += L"' is not supported. Use '.js' extension."; + RmLog(rm, LOG_ERROR, msg.c_str()); + return {}; + } + return value; +} + // Rainmeter Plugin Exports PLUGIN_EXPORT void Initialize(void** data, void* rm) { @@ -408,10 +632,12 @@ PLUGIN_EXPORT void Initialize(void** data, void* rm) wchar_t tempPath[MAX_PATH]; GetTempPathW(MAX_PATH, tempPath); measure->userDataFolder = std::wstring(tempPath) + L"RainmeterWebView2"; - measure->configPath = measure->userDataFolder + L"\\WebView2Settings.ini"; + measure->configPath = measure->userDataFolder + L"\\UserSettings.ini"; + measure->extensionsPath = measure->userDataFolder + L"\\Extensions\\Extensions.ini"; // Create the directory if it doesn't exist CreateDirectoryW(measure->userDataFolder.c_str(), nullptr); + CreateDirectoryW((measure->userDataFolder + L"\\Extensions").c_str(), nullptr); SkinSubclassData* skinData = nullptr; bool createdNew = false; @@ -581,9 +807,9 @@ PLUGIN_EXPORT void Reload(void* data, void* rm, double* /*maxValue*/) measure->onWebViewLoadAction = newOnWebViewLoadAction; measure->onWebViewFailAction = newOnWebViewFailAction; measure->onWebViewStopAction = newOnWebViewStopAction; + measure->onStateChangeAction = newOnStateChangeAction; measure->onUrlChangeAction = newOnUrlChangeAction; - measure->onPageLoadStartAction = newOnPageLoadStartAction; measure->onPageLoadingAction = newOnPageLoadingAction; measure->onPageDOMLoadAction = newOnPageDOMLoadAction; @@ -817,20 +1043,69 @@ PLUGIN_EXPORT void ExecuteBang(void* data, LPCWSTR args) { measure->webView6->OpenTaskManagerWindow(); } + else + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Unknown Open command"); + return; + } } - else if (_wcsicmp(action.c_str(), L"ExecuteScript") == 0) + else if (_wcsicmp(action.c_str(), L"Execute") == 0) { if (!param.empty()) { - measure->webView->ExecuteScript( - param.c_str(), - Callback( - [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + if (IsFilePathSyntax(param.c_str())) // Script file + { + try { + + std::wstring path = NormalizePath(measure->rm, param.c_str()); + + if (path.empty()) { - return S_OK; + return; } - ).Get() - ); + + std::wstring script = ReadScriptFile(path); + + if (script.empty()) + { + return; + } + + measure->webView->ExecuteScript( + script.c_str(), + Callback( + [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + } + catch (const std::exception& ex) { + try { + std::wstring msg = Utf8ToWstring( + ex.what(), + static_cast(std::strlen(ex.what())) + ); + RmLog(measure->rm, LOG_ERROR, msg.c_str()); + } + catch (...) { + RmLog(measure->rm, LOG_ERROR, L"Execute: Unknown error"); + } + } + } + else // Script string. + { + measure->webView->ExecuteScript( + param.c_str(), + Callback( + [](HRESULT errorCode, LPCWSTR resultObjectAsJson) -> HRESULT + { + return S_OK; + } + ).Get() + ); + } } } else @@ -959,6 +1234,6 @@ PLUGIN_EXPORT void Finalize(void* data) { RemoveKeyboardHook(); } - + delete measure; } \ No newline at end of file diff --git a/WebView2/Plugin.h b/WebView2/Plugin.h index 9850e06..82c06f0 100644 --- a/WebView2/Plugin.h +++ b/WebView2/Plugin.h @@ -21,17 +21,12 @@ using namespace Microsoft::WRL; extern wil::com_ptr g_typeLib; #define WM_APP_CTRL_CHANGED (WM_APP + 100) // Custom message for Ctrl key state change - -// Structure to hold frame information -struct Frames -{ - wil::com_ptr frame; - bool injected = false; - bool isDestroyed = false; -}; +#define WM_APP_REGION_RMB (WM_APP + 200) // Custom message for app-region RMB struct SkinSubclassData; +extern bool g_extensions_checked; + // Measure structure containing WebView2 state struct Measure { @@ -43,9 +38,9 @@ struct Measure SkinSubclassData* skinData = nullptr; wchar_t osLocale[LOCALE_NAME_MAX_LENGTH] = { 0 }; - std::wstring userDataFolder; std::wstring configPath; + std::wstring extensionsPath; std::wstring url; std::wstring currentUrl; std::wstring currentTitle; @@ -53,29 +48,30 @@ struct Measure std::wstring hostPath; std::wstring userAgent; - int width; - int height; - int x; - int y; + int width = 800; + int height = 600; + int x = 0; + int y = 0; int clickthrough = 1; double zoomFactor = 1.0; bool disabled = false; bool autoStart = true; bool visible = true; - bool initialized = false; - bool isFirstLoad = true; - bool isClickthroughActive = false; bool notifications = false; bool zoomControl = true; bool newWindow = false; - bool isViewSource = false; bool assistiveFeatures = true; bool hostSecurity = true; bool hostOrigin = true; + bool initialized = false; bool isCreationInProgress = false; + bool isClickthroughActive = false; bool isStopping = false; + bool isViewSource = false; + bool isFirstLoad = true; bool isCtrlPressed = false; + bool isWebViewFocused = false; std::wstring onWebViewLoadAction; std::wstring onWebViewFailAction; @@ -89,8 +85,10 @@ struct Measure std::wstring onPageLoadFinishAction; std::wstring onPageReloadAction; - CSimpleIniW ini; - bool iniDirty = false; + CSimpleIniW userSettingsFile; + CSimpleIniW extensionsFile; + bool userSettingsChanged = false; + bool extensionsChanged = false; wil::com_ptr webViewEnvironment; wil::com_ptr webViewController; @@ -98,10 +96,9 @@ struct Measure wil::com_ptr webView; wil::com_ptr webView3; wil::com_ptr webView6; - wil::com_ptr webViewProfile7; wil::com_ptr webViewSettings; wil::com_ptr webViewSettings2; - std::vector> Measure::webViewFrames; + RECT webViewArea; EventRegistrationToken webMessageToken; diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index 360d06d..ce882e4 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -3,6 +3,9 @@ #include "HostObjectRmAPI.h" #include "../API/RainmeterAPI.h" #include +#include + +bool g_extensions_checked = false; inline bool ParseBool(const wchar_t* value) { @@ -14,7 +17,8 @@ inline bool ParseBool(const wchar_t* value) _wcsicmp(value, L"on") == 0; } -inline bool GetIniBool(CSimpleIniW& ini, bool& dirty, const wchar_t* section, const wchar_t* key, bool def) + +inline bool GetIniBool(CSimpleIniW & ini, bool& dirty, const wchar_t* section, const wchar_t* key, bool def) { const wchar_t* value = ini.GetValue(section, key, nullptr); if (!value) @@ -26,7 +30,7 @@ inline bool GetIniBool(CSimpleIniW& ini, bool& dirty, const wchar_t* section, co return ParseBool(value); } -inline std::wstring GetIniString(CSimpleIniW& ini, bool& dirty, const wchar_t* section, const wchar_t* key, const wchar_t* def) +inline std::wstring GetIniString(CSimpleIniW& ini, bool& dirty, const wchar_t* section, const wchar_t* key, const wchar_t* def, bool forceDefault = false) { const wchar_t* value = ini.GetValue(section, key, nullptr); if (!value) @@ -35,9 +39,64 @@ inline std::wstring GetIniString(CSimpleIniW& ini, bool& dirty, const wchar_t* s dirty = true; return def; } + if (forceDefault && std::wcscmp(value, def) != 0) + { + ini.SetValue(section, key, def); + dirty = true; + return def; + } return value; } +std::vector GetExtensionsID(const std::wstring& input) +{ + std::vector result; + std::wstringstream ss(input); + std::wstring token; + + while (std::getline(ss, token, L',')) { + token.erase(0, token.find_first_not_of(L" \t")); + token.erase(token.find_last_not_of(L" \t") + 1); + + if (!token.empty()) { + result.push_back(token); + } + } + return result; +} + +static void EnableExtension( + ICoreWebView2BrowserExtension* extension, + BOOL enable) +{ + extension->Enable( + enable, + Callback( + [](HRESULT hr) -> HRESULT + { + if (FAILED(hr)) + ShowFailure(hr, L"Enable extension failed"); + return S_OK; + }).Get()); +} + +static void RemoveExtension(void* rm, + ICoreWebView2BrowserExtension* extension, + const std::wstring& name) +{ + extension->Remove( + Callback( + [](HRESULT hr) -> HRESULT + { + if (FAILED(hr)) + ShowFailure(hr, L"Uninstall extension failed"); + return S_OK; + }).Get()); + + RmLogF(rm, LOG_DEBUG, L"WebView2: \"%s\" extension removed.", name.c_str()); +} + + // Create WebView2 environment and controller void CreateWebView2(Measure* measure) { @@ -62,18 +121,18 @@ void CreateWebView2(Measure* measure) measure->isCreationInProgress = true; - // Load or create config.ini - measure->ini.SetUnicode(); - measure->ini.LoadFile(measure->configPath.c_str()); - measure->iniDirty = false; + // Load or create UserSettings.ini + measure->userSettingsFile.SetUnicode(); + measure->userSettingsFile.LoadFile(measure->configPath.c_str()); + measure->userSettingsChanged = false; - // Read environment options from config.ini - bool extensions = GetIniBool(measure->ini, measure->iniDirty, L"Environment", L"Extensions", false); // Extensions - bool fluentBars = GetIniBool(measure->ini, measure->iniDirty, L"Environment", L"FluentOverlayScrollBars", true); // Fluent Bars - bool trackingPrevention = GetIniBool(measure->ini, measure->iniDirty, L"Environment", L"TrackingPrevention", true); // Tracking Prevention (SmartScreen) - std::wstring language = GetIniString(measure->ini, measure->iniDirty, L"Environment", L"BrowserLocale", L"system"); // Language + // Read options from UserSettings.ini + bool fluentBars = GetIniBool(measure->userSettingsFile, measure->userSettingsChanged, L"Environment", L"FluentOverlayScrollBars", true); // Fluent Bars + bool trackingPrevention = GetIniBool(measure->userSettingsFile, measure->userSettingsChanged, L"Environment", L"TrackingPrevention", true); // Tracking Prevention (SmartScreen) + bool extensions = GetIniBool(measure->userSettingsFile, measure->userSettingsChanged, L"Environment", L"Extensions", false); // Extensions + std::wstring language = GetIniString(measure->userSettingsFile, measure->userSettingsChanged, L"Environment", L"BrowserLocale", L"system"); // Language // Available browser flags: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/webview-features-flags?tabs=win32cpp#available-webview2-browser-flags - std::wstring userBrowserArgs = GetIniString(measure->ini, measure->iniDirty, L"Environment", L"BrowserArguments", L"--allow-file-access-from-files"); // Browser Flags + std::wstring userBrowserArgs = GetIniString(measure->userSettingsFile, measure->userSettingsChanged, L"Environment", L"BrowserArguments", L"--allow-file-access-from-files"); // Browser Flags std::wstring browserArgs; browserArgs.append(L"--enable-features="); // Enable file access from file URLs browserArgs.append(userBrowserArgs); @@ -158,7 +217,6 @@ HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environme webViewEnvironment = env; - // Create WebView2 controller with options. auto webViewEnvironment10 = webViewEnvironment.try_query(); if (!webViewEnvironment10) @@ -184,9 +242,9 @@ HRESULT Measure::CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environme } CHECK_FAILURE(hr); - // Read environment options from config.ini - std::wstring scriptLocale = GetIniString(ini, iniDirty, L"Controller", L"ScriptLocale", L"system"); - bool privateMode = GetIniBool(ini, iniDirty, L"Controller", L"PrivateMode", false); + // Read options from UserSettings.ini + std::wstring scriptLocale = GetIniString(userSettingsFile, userSettingsChanged, L"Controller", L"ScriptLocale", L"system"); + bool privateMode = GetIniBool(userSettingsFile, userSettingsChanged, L"Controller", L"PrivateMode", false); // OPTIONS controllerOptions->put_ProfileName(L"rainmeter"); // Profile Name @@ -261,10 +319,9 @@ void RegisterFrames(Measure* measure, ICoreWebView2Frame* rawFrame, int level) wil::com_ptr frame = rawFrame; wil::com_ptr frame2 = frame.try_query(); - wil::com_ptr frame5 = frame.try_query(); // Only proceed if we have valid interfaces - if (!frame2 || !frame5) return; + if (!frame2) return; // Add host object wil::com_ptr hostObject = @@ -280,19 +337,10 @@ void RegisterFrames(Measure* measure, ICoreWebView2Frame* rawFrame, int level) CHECK_FAILURE(frame2->AddHostObjectToScriptWithOrigins(L"RainmeterAPI", &hostObjectVariant, 1, &origins)); VariantClear(&hostObjectVariant); - auto newFrameState = std::make_shared(); - newFrameState->frame = frame2; - newFrameState->injected = false; - newFrameState->isDestroyed = false; - - measure->webViewFrames.push_back(newFrameState); - - Frames* frameState = newFrameState.get(); - // Inject frame ancestor to nested frames to allow framing websites. (Requires virtual host or http-server). frame2->add_NavigationStarting( Microsoft::WRL::Callback( - [measure, origin, frameState](ICoreWebView2Frame* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT + [measure, origin](ICoreWebView2Frame* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { wil::com_ptr navigationStartArgs; if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&navigationStartArgs)))) @@ -304,63 +352,7 @@ void RegisterFrames(Measure* measure, ICoreWebView2Frame* rawFrame, int level) ).Get(), nullptr ); - frame2->add_ContentLoading( - Callback( - [measure, frameState](ICoreWebView2Frame* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT - { - return S_OK; - } - ).Get(), nullptr - ); - - frame2->add_DOMContentLoaded( - Callback( - [measure, frameState](ICoreWebView2Frame* sender, ICoreWebView2DOMContentLoadedEventArgs* args) -> HRESULT - { - return S_OK; - } - ).Get(), nullptr - ); - - frame2->add_NavigationCompleted( - Callback( - [measure, frameState](ICoreWebView2Frame* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT - { - return S_OK; - } - ).Get(), nullptr - ); - - frame2->add_Destroyed( - Callback( - [measure, level, frameState](ICoreWebView2Frame* sender, IUnknown* args)->HRESULT - { - if (measure->isStopping) - return S_OK; - // Remove frame - auto it = std::remove_if( - measure->webViewFrames.begin(), - measure->webViewFrames.end(), - [sender](const std::shared_ptr& f) - { - // Check equality against the COM pointer inside the struct - return f->frame.get() == sender; - } - ); - if (it != measure->webViewFrames.end()) - { - measure->webViewFrames.erase(it, measure->webViewFrames.end()); - } - - frameState->isDestroyed = true; - - return S_OK; - } - ).Get(), nullptr - ); - wil::com_ptr frame7 = frame.try_query(); - if (frame7) { CHECK_FAILURE(frame7->add_FrameCreated( @@ -397,22 +389,10 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller webViewController = controller; CHECK_FAILURE(webViewController->get_CoreWebView2(&webView)); - // Set bounds within the skin window - RECT bounds; - GetClientRect(skinWindow, &bounds); - bounds.left = x; - bounds.top = y; - if (width > 0) - { - bounds.right = x + width; - } - if (height > 0) - { - bounds.bottom = y + height; - } - + webViewArea = {x, y, x + width, y + height }; + // CONTROLLER OPTIONS - webViewController->put_Bounds(bounds); // Set initial bounds + webViewController->put_Bounds(webViewArea); // Set initial bounds webViewController->put_IsVisible(visible); // Set initial visibility webViewController->put_ZoomFactor(zoomFactor); // Set initial zoom factor @@ -432,6 +412,28 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller ).Get(), nullptr ); + // Set isWebViewFocused variable + webViewController->add_GotFocus( + Microsoft::WRL::Callback( + [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT + { + isWebViewFocused = true; + return S_OK; + } + ).Get(), nullptr + ); + + // Set isWebViewFocused variable + webViewController->add_LostFocus( + Microsoft::WRL::Callback( + [this](ICoreWebView2Controller* sender, IUnknown* args) -> HRESULT + { + isWebViewFocused = false; + return S_OK; + } + ).Get(), nullptr + ); + // Accelerator Keys webViewController->add_AcceleratorKeyPressed( Microsoft::WRL::Callback( @@ -498,11 +500,11 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller webViewSettings->put_AreDevToolsEnabled(TRUE); webViewSettings->put_IsZoomControlEnabled(zoomControl); - // Read environment options from config.ini - bool statusBar = GetIniBool(ini, iniDirty, L"Core", L"StatusBar", true); - bool pinchZoom = GetIniBool(ini, iniDirty, L"Core", L"PinchZoom", true); - bool swipeNavigation = GetIniBool(ini, iniDirty, L"Core", L"SwipeNavigation", true); - bool reputationChecking = GetIniBool(ini, iniDirty, L"Core", L"SmartScreen", true); + // Read options from UserSettings.ini + bool statusBar = GetIniBool(userSettingsFile, userSettingsChanged, L"Core", L"StatusBar", true); + bool pinchZoom = GetIniBool(userSettingsFile, userSettingsChanged, L"Core", L"PinchZoom", true); + bool swipeNavigation = GetIniBool(userSettingsFile, userSettingsChanged, L"Core", L"SwipeNavigation", true); + bool reputationChecking = GetIniBool(userSettingsFile, userSettingsChanged, L"Core", L"SmartScreen", true); webViewSettings->put_IsStatusBarEnabled(statusBar); @@ -549,11 +551,11 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller wil::com_ptr profile; CHECK_FAILURE(webView2_13->get_Profile(&profile)); - // Read environment options from config.ini - std::wstring downloadsFolder = GetIniString(ini, iniDirty, L"Profile", L"DownloadsFolderPath", L""); - std::wstring colorScheme = GetIniString(ini, iniDirty, L"Profile", L"ColorScheme", L"system"); - bool passAutoSave = GetIniBool(ini, iniDirty, L"Profile", L"PasswordAutoSave", false); - bool generalAutoFill = GetIniBool(ini, iniDirty, L"Profile", L"GeneralAutoFill", true); + // Read options from UserSettings.ini + std::wstring downloadsFolder = GetIniString(userSettingsFile, userSettingsChanged, L"Profile", L"DownloadsFolderPath", L""); + std::wstring colorScheme = GetIniString(userSettingsFile, userSettingsChanged, L"Profile", L"ColorScheme", L"system"); + bool passAutoSave = GetIniBool(userSettingsFile, userSettingsChanged, L"Profile", L"PasswordAutoSave", false); + bool generalAutoFill = GetIniBool(userSettingsFile, userSettingsChanged, L"Profile", L"GeneralAutoFill", true); profile->put_DefaultDownloadFolderPath(downloadsFolder.c_str()); // Downloads folder path @@ -570,17 +572,180 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller profile->put_PreferredColorScheme(COREWEBVIEW2_PREFERRED_COLOR_SCHEME_AUTO); } - auto profile6 = webViewSettings.try_query(); + auto profile6 = profile.try_query(); if (profile6) { profile6->put_IsPasswordAutosaveEnabled(passAutoSave); // Password AutoSave profile6->put_IsGeneralAutofillEnabled(generalAutoFill); // General AutoFill } - auto profile7 = webViewSettings.try_query(); - if (profile7) + // Extensions + auto profile7 = profile.try_query(); + const std::filesystem::path extensionsRoot = std::filesystem::path(userDataFolder) / L"Extensions"; + + if (profile7 && std::filesystem::exists(extensionsRoot) && !g_extensions_checked) { - webViewProfile7 = profile7; // For browser extensions (TODO) + g_extensions_checked = true; + + profile7->GetBrowserExtensions( + Callback( + [this, profile7, extensionsRoot] + (HRESULT error, ICoreWebView2BrowserExtensionList* extensions) -> HRESULT + { + if (FAILED(error) || !extensions) + { + ShowFailure(error, L"GetBrowserExtensions failed"); + return S_OK; + } + + // Load Extensions.ini + extensionsFile.SetUnicode(); + extensionsFile.LoadFile(extensionsPath.c_str()); + extensionsChanged = false; + + UINT extensionsCount = 0; + extensions->get_Count(&extensionsCount); + + auto addExtension = [&](const std::filesystem::path& path, const std::wstring& folderName) + { + profile7->AddBrowserExtension( + path.c_str(), + Callback( + [this, folderName] + (HRESULT error, ICoreWebView2BrowserExtension* extension) mutable -> HRESULT + { + if (FAILED(error)) + { + if (error == ERROR_FILE_NOT_FOUND) + RmLog(rm, LOG_ERROR, L"WebView2: Invalid extension path or manifest.json file is missing."); + else if (error == ERROR_NOT_SUPPORTED) + RmLog(rm, LOG_ERROR, L"WebView2: Extensions are disabled."); + + return S_OK; + } + + wil::unique_cotaskmem_string id; + wil::unique_cotaskmem_string name; + + extension->get_Id(&id); + extension->get_Name(&name); + + // Create the extension's section on Extensions.ini + extensionsFile.SetValue(folderName.c_str(), L"ID", id.get()); + extensionsFile.SetValue(folderName.c_str(), L"Name", name.get()); + extensionsFile.SetValue(folderName.c_str(), L"Enabled", L"true"); + extensionsFile.SetValue(folderName.c_str(), L"Uninstall", L"false"); + extensionsFile.SaveFile(extensionsPath.c_str()); + extensionsChanged = false; + + extension->Enable( + TRUE, + Callback( + [](HRESULT hr) -> HRESULT + { + if (FAILED(hr)) + ShowFailure(hr, L"Enable extension failed"); + return S_OK; + }).Get()); + + RmLogF(rm, LOG_NOTICE, L"WebView2: \"%s\" extension installed.", name.get()); + return S_OK; + }).Get()); + }; + + // Check extensions folders at Extensions\/ + for (const auto& entry : std::filesystem::directory_iterator(extensionsRoot)) + { + if (!entry.is_directory()) + continue; + + const auto& extensionPath = entry.path(); + const std::wstring folderName = extensionPath.filename().wstring(); + // Look for already saved ID on Extensions.ini for this extension + const std::wstring savedID = extensionsFile.GetValue(folderName.c_str(), L"ID", L""); + + if (savedID.empty()) // No ID found + { + addExtension(extensionPath, folderName); + continue; + } + + bool found = false; + + for (UINT i = 0; i < extensionsCount; ++i) + { + wil::com_ptr extension; + extensions->GetValueAtIndex(i, &extension); + + wil::unique_cotaskmem_string id; + extension->get_Id(&id); + + if (savedID != id.get()) // Check which already installed extension matches ID. + continue; + + found = true; + + wil::unique_cotaskmem_string name; + BOOL enabled = FALSE; + + extension->get_Name(&name); + extension->get_IsEnabled(&enabled); + + // Read options from Extensions.ini + const bool enable = GetIniBool(extensionsFile, extensionsChanged, folderName.c_str(), L"Enabled", true); + const bool remove = GetIniBool(extensionsFile, extensionsChanged, folderName.c_str(), L"Uninstall", false); + + if (remove) // Uninstall Extension + { + extension->Remove( + Callback( + [](HRESULT hr) -> HRESULT + { + if (FAILED(hr)) + ShowFailure(hr, L"Uninstall extension failed"); + return S_OK; + }).Get()); + + // Delete section from Extensions.ini + extensionsFile.Delete(folderName.c_str(), nullptr); + extensionsFile.SaveFile(extensionsPath.c_str()); + extensionsChanged = false; + + RmLogF(rm, LOG_NOTICE, L"WebView2: \"%s\" extension removed.", name.get()); + return S_OK; + } + + if (enabled != enable) // Toggle Extension + { + extension->Enable( + enable, + Callback( + [](HRESULT hr) -> HRESULT + { + if (FAILED(hr)) + ShowFailure(hr, L"Enable extension failed"); + return S_OK; + }).Get()); + + RmLogF(rm, LOG_NOTICE, L"WebView2: \"%s\" extension %s.", name.get(), enable ? L"enabled" : L"disabled"); + } + + break; + } + + if (!found) // Extension not yet installed. + addExtension(extensionPath, folderName); + } + + if (extensionsChanged) + { + // Save Extensions.ini + extensionsFile.SaveFile(extensionsPath.c_str()); + extensionsChanged = false; + } + + return S_OK; + }).Get()); } } @@ -616,16 +781,18 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller Microsoft::WRL::Callback( [this, variant](ICoreWebView2* sender, ICoreWebView2FrameCreatedEventArgs* args) -> HRESULT { - wil::com_ptr frame; - args->get_Frame(&frame); - - BOOL isDestroyed; - frame->IsDestroyed(&isDestroyed); + if (!isStopping) + { + wil::com_ptr frame; + args->get_Frame(&frame); - if (isDestroyed) return S_OK; + BOOL isDestroyed; + frame->IsDestroyed(&isDestroyed); - RegisterFrames(this, frame.get(), 1); + if (isDestroyed) return S_OK; + RegisterFrames(this, frame.get(), 1); + } return S_OK; } ).Get(), nullptr @@ -1121,10 +1288,10 @@ HRESULT Measure::CreateControllerHandler(HRESULT result, ICoreWebView2Controller ).Get(), nullptr ); - if (iniDirty) + if (userSettingsChanged) { - ini.SaveFile(configPath.c_str()); - iniDirty = false; + userSettingsFile.SaveFile(configPath.c_str()); + userSettingsChanged = false; } initialized = true; @@ -1228,8 +1395,6 @@ void StopWebView2(Measure* measure) measure->isCreationInProgress = false; - measure->webViewFrames.clear(); - // Stop navigation if (measure->webView) {