Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Plugin settings rendering #200

Merged
merged 41 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
639390d
feat: Slightly Better Russian Translation (#187)
dotFelixan Dec 16, 2024
4ce6b5d
fix: Use correct classes for Field (#189)
ricewind012 Dec 17, 2024
a29c7df
fix: Fix plugin JS being removed from webkit when a new theme is sele…
shdwmtr Dec 19, 2024
1de0352
chore(release): bump version to v2.16.0 [skip ci]
invalid-email-address Dec 19, 2024
3e7d93e
chore(release): 2.16.0 [skip ci]
semantic-release-bot Dec 19, 2024
d25461d
fix: Properly handle theme config with invalid props.
shdwmtr Dec 19, 2024
8817735
fix: Catch installer socket being used instead of crashing.
shdwmtr Dec 19, 2024
07dc66c
fix: Fix unix pathing
shdwmtr Dec 20, 2024
9b7e1e5
fix: Fix crashing on new installs
shdwmtr Dec 20, 2024
4b085a2
chore(release): bump version to v2.16.1 [skip ci]
invalid-email-address Dec 20, 2024
0bc733d
chore(release): 2.16.1 [skip ci]
semantic-release-bot Dec 20, 2024
bfafcb7
fix: Fix installer crashes (again)
shdwmtr Dec 21, 2024
cae007a
chore(release): bump version to v2.16.2 [skip ci]
invalid-email-address Dec 21, 2024
eb2a567
chore(release): 2.16.2 [skip ci]
semantic-release-bot Dec 21, 2024
fb4053d
fix: Fix installer script
shdwmtr Dec 21, 2024
8990389
Merge branch 'main' of https://github.com/shdwmtr/millennium
shdwmtr Dec 21, 2024
71dabc6
fix: Add proxy support for web requests within Steam.
shdwmtr Dec 25, 2024
e2cd49d
fix: Fix webkit and CSP related issues. closes #197, #196, #194
shdwmtr Dec 27, 2024
2c91a95
fix: Fix webkit issues, Close #194, #196
shdwmtr Dec 28, 2024
2573215
fix: Fix webkit patcher edge cases on redirects
shdwmtr Dec 28, 2024
af602c3
feat: Add system accent color to webkit
shdwmtr Dec 28, 2024
65708de
chore: Rename pipe
shdwmtr Dec 28, 2024
d0ff106
chore: Remove debug logs
shdwmtr Dec 28, 2024
43b280a
chore: Fix linux
shdwmtr Dec 28, 2024
b9a6d0d
fix: Fix webkit patcher skipping non-secure requests.
shdwmtr Dec 29, 2024
861c845
Fix: prevent artifact builder from bumping the version
shdwmtr Dec 29, 2024
284a2a2
chore(release): bump version to v2.17.0 [skip ci]
invalid-email-address Dec 29, 2024
1f94b71
chore(release): 2.17.0 [skip ci]
semantic-release-bot Dec 29, 2024
4614cc8
fix: Fix workshop pages not rendering HTML.
shdwmtr Dec 29, 2024
6608125
chore(release): bump version to v2.17.1 [skip ci]
invalid-email-address Dec 29, 2024
2501c85
chore(release): 2.17.1 [skip ci]
semantic-release-bot Dec 29, 2024
7d0fbcf
fix: Fix overlay browser not loading on pages without a response phrase.
shdwmtr Dec 29, 2024
1ca25a5
chore(release): bump version to v2.17.2 [skip ci]
invalid-email-address Dec 29, 2024
900b7eb
chore(release): 2.17.2 [skip ci]
semantic-release-bot Dec 29, 2024
24de43b
feat: Add embedded updater
shdwmtr Dec 31, 2024
558a779
chore: CMake cleanup
shdwmtr Dec 31, 2024
aa4f054
fix: allow frontend function to send booleans & numbers to backend (#…
tddebart Dec 31, 2024
e138118
feat(ipc): add support for fetching frontend settings from webkit con…
tddebart Jan 2, 2025
9054c95
chore: integrate new settings framework in example plugin
tddebart Jan 2, 2025
5423f61
feat: render plugin settings modal based on plugin settings object
tddebart Jan 4, 2025
44ed4a4
fix: settings not receiving in other context when using tabs
tddebart Jan 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/workflows/artifact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# dry run to get the next version
- name: Bump Version
id: read_version
run: . scripts\ci\win32\version.ps1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Set up cache for Python source
uses: actions/cache@v3
id: build-cache
Expand Down Expand Up @@ -177,13 +170,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# dry run to get the next version
- name: Bump Version
id: read_version
run: bash scripts/ci/posix/version.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Install dependencies
run: |
sudo dpkg --add-architecture i386
Expand Down
8 changes: 3 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ endif()
add_subdirectory(cli)

find_package(CURL REQUIRED) # used for web requests.
find_package(unofficial-minizip CONFIG REQUIRED) # used for extracting zip files

include_directories(${LIBGIT2_INCLUDE_DIRS})

if(WIN32)
Expand Down Expand Up @@ -110,7 +108,7 @@ set(SOURCE_FILES
)

if (MSVC)
set(SOURCE_FILES "${SOURCE_FILES} version.rc") # conpile version information on msvc
set(SOURCE_FILES "${SOURCE_FILES} version.rc") # compile version information on msvc
endif()

if (WIN32)
Expand Down Expand Up @@ -156,10 +154,10 @@ if (WINDRES)
target_link_libraries(Millennium ${CMAKE_BINARY_DIR}/version.o)
endif()

target_link_libraries(Millennium CURL::libcurl unofficial::minizip::minizip)
target_link_libraries(Millennium CURL::libcurl)

if(WIN32)
target_link_libraries(Millennium Ws2_32.lib wsock32 Iphlpapi)
target_link_libraries(Millennium Ws2_32.lib wsock32 Iphlpapi winhttp)

if (GITHUB_ACTION_BUILD)
target_link_libraries(Millennium "D:/a/Millennium/Millennium/Python-3.11.8/PCbuild/win32/python311.lib")
Expand Down
5 changes: 5 additions & 0 deletions assets/core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def get_load_config():
"useInterface": True if millennium.get('Settings', 'useInterface', fallback='') == "yes" else False,
})

def _webkit_accent_color():
config = cfg.get_config()

return Colors.get_accent_color(config["accentColor"])

def update_plugin_status(plugin_name: str, enabled: bool):
Millennium.change_plugin_status(plugin_name, enabled)

Expand Down
29 changes: 18 additions & 11 deletions assets/core/util/theme_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,25 @@ async def handler(self, websocket):
logger.log("Client disconnected")

async def start_server(self):
# Start WebSocket server
self.server = await websockets.serve(self.handler, self.host, self.port)
logger.log(f"Server started on ws://{self.host}:{self.port}")

# Run server until stop event is set
await self.stop_event.wait()

# Close server gracefully
self.server.close()
await self.server.wait_closed()
logger.log("Server stopped")
try:
# Start WebSocket server
self.server = await websockets.serve(self.handler, self.host, self.port)
logger.log(f"Server started on ws://{self.host}:{self.port}")

# Run server until stop event is set
await self.stop_event.wait()

# Close server gracefully
self.server.close()
await self.server.wait_closed()
logger.log("Server stopped")

except OSError as e:
if e.errno == 98: # Errno 98 is typically "Address already in use"
logger.error(f"Port {self.port} is already in use. Please stop the process using it or choose a different port.")
else:
logger.error(f"An unexpected error occurred: {e}")

def _run_loop(self):
# Create and run the event loop in the thread
self.loop = asyncio.new_event_loop()
Expand Down
4 changes: 2 additions & 2 deletions assets/core/util/webkit_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def add_conditional_data(path: str, data: dict):
parsed_patches = parse_conditional_patches(data)

for patch in parsed_patches:
if patch['fileType'] == 'TargetCss':
if patch['fileType'] == 'TargetCss' and patch['targetPath'] is not None and patch['matchString'] is not None:
add_browser_css(os.path.join(path, patch['targetPath']), patch['matchString'])
elif patch['fileType'] == 'TargetJs':
elif patch['fileType'] == 'TargetJs' and patch['targetPath'] is not None and patch['matchString'] is not None:
add_browser_js(os.path.join(path, patch['targetPath']), patch['matchString'])
7 changes: 7 additions & 0 deletions assets/src/classes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { findClassModule } from "@steambrew/client"

export const devClasses = findClassModule(m => m.richPresenceLabel && m.blocked) as any
export const fieldClasses: any = findClassModule(
(m) =>
m.FieldLabel &&
!m.GyroButtonPickerDialog &&
!m.ControllerOutline &&
!m.AwaitingEmailConfIcon,
);
export const pagedSettingsClasses = findClassModule(m => m.PagedSettingsDialog_PageList) as any
export const settingsClasses = findClassModule(m => m.SettingsTitleBar && m.SettingsDialogButton) as any
export const notificationClasses = findClassModule(m => m.GroupMessageTitle && !m.ShortTemplate && !m.TwoLine && !m.FriendIndicator && !m.AchievementIcon) as any;
7 changes: 4 additions & 3 deletions assets/src/components/CreatePopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ export interface RenderProps {

export class CreatePopup extends CreatePopupBase {

constructor(component: ReactNode, strPopupName: string, options: any) {
constructor(component: ReactNode, strPopupName: string, options: any, componentParams: any = {}) {
super(strPopupName, options)
this.component = component
this.componentParams = componentParams
}

Show() {
Expand All @@ -35,7 +36,7 @@ export class CreatePopup extends CreatePopupBase {
hideMax={false}
hideActions={false}
/>
<this.component/>
<this.component {...this.componentParams}/>

</div>
</>
Expand Down Expand Up @@ -66,4 +67,4 @@ export class CreatePopup extends CreatePopupBase {
this.m_popup.SteamClient?.Window?.ResizeTo(450, height + 48, true)
this.m_popup.SteamClient.Window.ShowWindow()
}
}
}
31 changes: 12 additions & 19 deletions assets/src/custom_components/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import React, { ReactNode } from "react";
import { Classes, classMap, findClassModule } from "@steambrew/client";
import { fieldClasses } from "../classes";

const containerClasses = [
Classes.Field,
Classes.WithFirstRow,
Classes.VerticalAlignCenter,
Classes.WithDescription,
Classes.WithBottomSeparatorStandard,
Classes.ChildrenWidthFixed,
Classes.ExtraPaddingOnChildrenBelow,
Classes.StandardPadding,
Classes.HighlightOnFocus,
fieldClasses.Field,
fieldClasses.WithFirstRow,
fieldClasses.VerticalAlignCenter,
fieldClasses.WithDescription,
fieldClasses.WithBottomSeparatorStandard,
fieldClasses.ChildrenWidthFixed,
fieldClasses.ExtraPaddingOnChildrenBelow,
fieldClasses.StandardPadding,
fieldClasses.HighlightOnFocus,
"Panel",
].join(" ");
const fieldClasses: any = findClassModule(
(m) =>
m.FieldLabel &&
!m.GyroButtonPickerDialog &&
!m.ControllerOutline &&
!m.AwaitingEmailConfIcon,
);

interface FieldProps {
children: ReactNode;
Expand All @@ -41,8 +34,8 @@ export const Field: React.FC<FieldProps> = ({
<div className={containerClasses}>
<div className={fieldClasses.FieldLabelRow}>
<div className={fieldClasses.FieldLabel}>{label}</div>
<div className={classMap.FieldChildrenWithIcon}>{children}</div>
<div className={fieldClasses.FieldChildrenWithIcon}>{children}</div>
</div>
<div className={classMap.FieldDescription}>{description}</div>
<div className={fieldClasses.FieldDescription}>{description}</div>
</div>
);
58 changes: 58 additions & 0 deletions assets/src/custom_components/PluginField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ComponentProps } from './PluginSettings';
import React, { ChangeEventHandler, ReactElement, useState } from 'react';
import { Field, SettingType, TextField, Toggle } from '@steambrew/client';

export const PluginField: React.FC<ComponentProps> = ({settingsModule, propertyKey, metadata}) => {
const [value, setValue] = useState(settingsModule[propertyKey]);

const RenderToggle: React.FC<ComponentProps> = ({settingsModule, propertyKey}) => {
const onCheckChange = (enabled: boolean) => {
settingsModule[propertyKey] = enabled;
setValue(enabled);
};

return <Toggle value={value} onChange={onCheckChange}/>;
};

const RenderTextField: React.FC<ComponentProps> = ({settingsModule, propertyKey, metadata}) => {
const onValueChange: ChangeEventHandler<HTMLInputElement> = (e) => {
let newValue: string | number = e.target.value;
if (metadata.type === SettingType.NumberInput) {
newValue = parseInt(e.target.value);
}

if (newValue !== value) {
settingsModule[propertyKey] = newValue;
setValue(newValue);
}
};

return <TextField
value={value}
onChange={onValueChange}
// Focus on click otherwise we can't focus the textfield because of how showModal works
onClick={(e) => (e.target as HTMLInputElement).focus()}
/>;
};


let component: ReactElement;
switch (metadata.type) {
case SettingType.Toggle:
component = <RenderToggle settingsModule={settingsModule} propertyKey={propertyKey} metadata={metadata}/>;
break;
case SettingType.NumberInput:
case SettingType.TextInput:
component = <RenderTextField settingsModule={settingsModule} propertyKey={propertyKey} metadata={metadata}/>;
break;
default:
component = <span style={{color: 'crimson'}}>Unsupported setting type "{metadata.type}"</span>;
break;
}

return (
<Field label={metadata.name} description={metadata.description}>
{component}
</Field>
);
};
89 changes: 89 additions & 0 deletions assets/src/custom_components/PluginSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { PluginComponent } from '../types';
import { DialogBody, MillenniumModuleSettings, MillenniumSettingTabs, ModalPosition, SettingMetadata, SidebarNavigation, SidebarNavigationPage } from '@steambrew/client';
import { PluginField } from './PluginField';
import { settingsClasses } from '../classes';

type PluginSettingsProps = {
plugin: PluginComponent;
}

export type ComponentProps = {
settingsModule: MillenniumModuleSettings;
propertyKey: string;
metadata: SettingMetadata;
}

export default class RenderPluginSettings extends React.Component<PluginSettingsProps> {
plugin: PluginComponent;
pluginSettings: MillenniumModuleSettings | MillenniumSettingTabs;

constructor(props: PluginSettingsProps) {
super(props);
this.plugin = props.plugin;
this.pluginSettings = window.PLUGIN_LIST[this.plugin.data.name].settings;
}

RenderPage: React.FC<{ settingsModule: MillenniumModuleSettings }> = ({settingsModule}) => {
return (
<DialogBody>
{
Object.entries(settingsModule.metadata).map(([key, value]) => (
<PluginField settingsModule={settingsModule} propertyKey={key} metadata={value}/>
))
}
</DialogBody>
);
};

render() {
let pages: SidebarNavigationPage[] = [];

if (this.pluginSettings instanceof MillenniumSettingTabs) {
for (let metadataKey in this.pluginSettings.metadata) {
const metadata = this.pluginSettings.metadata[metadataKey];
const settingsModule = this.pluginSettings[metadataKey];
if (!(settingsModule instanceof MillenniumModuleSettings)) {
throw new Error(`Expected MillenniumModuleSettings but received ${typeof settingsModule} in MillenniumSettingTabs object`);
}

//TODO: find a way to fix the bug where toggle values are kept between tabs
// using a route on the page seems to fix it but fully breaks the library
// probably need to do hook into the react router like decky does
pages.push({
...metadata,
content: <this.RenderPage settingsModule={settingsModule}/>,
});
}
} else if (this.pluginSettings instanceof MillenniumModuleSettings) {
pages.push({
title: `${this.plugin.data.common_name} settings`,
content: <this.RenderPage settingsModule={this.pluginSettings}/>,
});
}

const className = `${settingsClasses.SettingsModal} ${settingsClasses.DesktopPopup}`;
const title = <>{this.plugin.data.common_name}<br/>settings</>;
const hasMultiplePages = this.pluginSettings instanceof MillenniumSettingTabs;

return (
<ModalPosition>
{!hasMultiplePages && (
<style>{`
.PageListColumn {
display: none !important;
}
.DialogContentTransition {
max-width: unset !important;
}
`}</style>
)}

{/* @ts-ignore: className hasn't been added to DFL yet */}
<SidebarNavigation className={className} pages={pages} title={title}>

</SidebarNavigation>
</ModalPosition>
);
}
}
Loading