forked from NG-SD-Plugins/PowerTools
Merge branch 'dev'
This commit is contained in:
commit
ab4e34756c
3 changed files with 219 additions and 24 deletions
|
@ -14,6 +14,7 @@ You will need that installed for this plugin to work.
|
||||||
- Set some GPU power parameters (fastPPT & slowPPT)
|
- Set some GPU power parameters (fastPPT & slowPPT)
|
||||||
- Set the fan RPM (unsupported on SteamOS beta)
|
- Set the fan RPM (unsupported on SteamOS beta)
|
||||||
- Display supplementary battery info
|
- Display supplementary battery info
|
||||||
|
- Keep settings between restarts (stored in `~/.config/powertools.json`)
|
||||||
|
|
||||||
## Cool, but that's too much work
|
## Cool, but that's too much work
|
||||||
|
|
||||||
|
|
208
main.py
208
main.py
|
@ -1,14 +1,40 @@
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
|
||||||
VERSION = "0.4.2"
|
VERSION = "0.5.0"
|
||||||
|
SETTINGS_LOCATION = "~/.config/powertools.json"
|
||||||
|
LOG_LOCATION = "/tmp/powertools.log"
|
||||||
|
FANTASTIC_INSTALL_DIR = "~/homebrew/plugins/Fantastic"
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
filename = LOG_LOCATION,
|
||||||
|
format = '%(asctime)s %(levelname)s %(message)s',
|
||||||
|
filemode = 'w',
|
||||||
|
force = True)
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logging.info(f"PowerTools v{VERSION} https://github.com/NGnius/PowerTools")
|
||||||
|
startup_time = time.time()
|
||||||
|
|
||||||
class CPU:
|
class CPU:
|
||||||
SCALING_FREQUENCIES = [1700000, 2400000, 2800000]
|
SCALING_FREQUENCIES = [1700000, 2400000, 2800000]
|
||||||
|
|
||||||
def __init__(self, number):
|
def __init__(self, number, settings=None):
|
||||||
self.number = number
|
self.number = number
|
||||||
|
|
||||||
|
if settings is not None:
|
||||||
|
self.set_max_boost(settings["max_boost"])
|
||||||
|
if settings["online"]:
|
||||||
|
self.enable()
|
||||||
|
else:
|
||||||
|
self.disable()
|
||||||
|
# TODO governor
|
||||||
|
|
||||||
if(self.status()):
|
if(self.status()):
|
||||||
self.max_boost = self._get_max_boost()
|
self.max_boost = self._get_max_boost()
|
||||||
else:
|
else:
|
||||||
|
@ -46,6 +72,16 @@ class CPU:
|
||||||
filepath = cpu_online_path(self.number)
|
filepath = cpu_online_path(self.number)
|
||||||
return read_from_sys(filepath) == "1"
|
return read_from_sys(filepath) == "1"
|
||||||
|
|
||||||
|
def governor(self) -> str:
|
||||||
|
return self._read_scaling_governor()
|
||||||
|
|
||||||
|
def settings(self) -> dict:
|
||||||
|
return {
|
||||||
|
"online": self.status(),
|
||||||
|
"max_boost": self.max_boost,
|
||||||
|
"governor": self.governor(),
|
||||||
|
}
|
||||||
|
|
||||||
def _read_scaling_governor(self) -> str:
|
def _read_scaling_governor(self) -> str:
|
||||||
filepath = cpu_governor_scaling_path(self.number)
|
filepath = cpu_governor_scaling_path(self.number)
|
||||||
return read_from_sys(filepath, amount=-1).strip()
|
return read_from_sys(filepath, amount=-1).strip()
|
||||||
|
@ -82,6 +118,8 @@ class Plugin:
|
||||||
FAN_SPEEDS = [0, 1000, 2000, 3000, 4000, 5000, 6000]
|
FAN_SPEEDS = [0, 1000, 2000, 3000, 4000, 5000, 6000]
|
||||||
|
|
||||||
auto_fan = True
|
auto_fan = True
|
||||||
|
persistent = True
|
||||||
|
modified_settings = False
|
||||||
|
|
||||||
async def get_version(self) -> str:
|
async def get_version(self) -> str:
|
||||||
return VERSION
|
return VERSION
|
||||||
|
@ -90,6 +128,7 @@ class Plugin:
|
||||||
|
|
||||||
# call from main_view.html with setCPUs(count, smt)
|
# call from main_view.html with setCPUs(count, smt)
|
||||||
async def set_cpus(self, count, smt=True):
|
async def set_cpus(self, count, smt=True):
|
||||||
|
self.modified_settings = True
|
||||||
cpu_count = len(self.cpus)
|
cpu_count = len(self.cpus)
|
||||||
self.smt = smt
|
self.smt = smt
|
||||||
# print("Setting CPUs")
|
# print("Setting CPUs")
|
||||||
|
@ -121,13 +160,15 @@ class Plugin:
|
||||||
return self.smt
|
return self.smt
|
||||||
|
|
||||||
async def set_boost(self, enabled: bool) -> bool:
|
async def set_boost(self, enabled: bool) -> bool:
|
||||||
write_to_sys("/sys/devices/system/cpu/cpufreq/boost", int(enabled))
|
self.modified_settings = True
|
||||||
|
write_cpu_boost(enabled)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_boost(self) -> bool:
|
async def get_boost(self) -> bool:
|
||||||
return read_from_sys("/sys/devices/system/cpu/cpufreq/boost") == "1"
|
return read_cpu_boost()
|
||||||
|
|
||||||
async def set_max_boost(self, index):
|
async def set_max_boost(self, index):
|
||||||
|
self.modified_settings = True
|
||||||
if index < 0 or index >= len(CPU.SCALING_FREQUENCIES):
|
if index < 0 or index >= len(CPU.SCALING_FREQUENCIES):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -144,30 +185,32 @@ class Plugin:
|
||||||
# GPU stuff
|
# GPU stuff
|
||||||
|
|
||||||
async def set_gpu_power(self, value: int, power_number: int) -> bool:
|
async def set_gpu_power(self, value: int, power_number: int) -> bool:
|
||||||
write_to_sys(gpu_power_path(power_number), value)
|
self.modified_settings = True
|
||||||
|
write_gpu_ppt(power_number, value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_gpu_power(self, power_number: int) -> int:
|
async def get_gpu_power(self, power_number: int) -> int:
|
||||||
return int(read_from_sys(gpu_power_path(power_number), amount=-1).strip())
|
return read_gpu_ppt(power_number)
|
||||||
|
|
||||||
# Fan stuff
|
# Fan stuff
|
||||||
|
|
||||||
async def set_fan_tick(self, tick: int):
|
async def set_fan_tick(self, tick: int):
|
||||||
|
self.modified_settings = True
|
||||||
if tick >= len(self.FAN_SPEEDS):
|
if tick >= len(self.FAN_SPEEDS):
|
||||||
# automatic mode
|
# automatic mode
|
||||||
self.auto_fan = True
|
self.auto_fan = True
|
||||||
write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 0)
|
write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 0)
|
||||||
write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", 4099) # 4099 is default
|
write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", 4099) # 4099 is default
|
||||||
#subprocess.run(["systemctl", "start", "jupiter-fan-control.service"])
|
#subprocess.Popen("systemctl start jupiter-fan-control.service", stdout=subprocess.PIPE, shell=True).wait()
|
||||||
else:
|
else:
|
||||||
# manual voltage
|
# manual voltage
|
||||||
self.auto_fan = False
|
self.auto_fan = False
|
||||||
write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1)
|
write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1)
|
||||||
write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", self.FAN_SPEEDS[tick])
|
write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", self.FAN_SPEEDS[tick])
|
||||||
#subprocess.run(["systemctl", "stop", "jupiter-fan-control.service"])
|
#subprocess.Popen("systemctl stop jupiter-fan-control.service", stdout=subprocess.PIPE, shell=True).wait()
|
||||||
|
|
||||||
async def get_fan_tick(self) -> int:
|
async def get_fan_tick(self) -> int:
|
||||||
fan_target = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_target", amount=-1).strip())
|
fan_target = read_fan_target()
|
||||||
fan_input = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_input", amount=-1).strip())
|
fan_input = int(read_from_sys("/sys/class/hwmon/hwmon5/fan1_input", amount=-1).strip())
|
||||||
fan_target_v = float(fan_target) / 1000
|
fan_target_v = float(fan_target) / 1000
|
||||||
fan_input_v = float(fan_input) / 1000
|
fan_input_v = float(fan_input) / 1000
|
||||||
|
@ -176,17 +219,17 @@ class Plugin:
|
||||||
elif fan_target == 4099 or (int(round(fan_target_v)) != int(round(fan_input_v)) and fan_target not in self.FAN_SPEEDS):
|
elif fan_target == 4099 or (int(round(fan_target_v)) != int(round(fan_input_v)) and fan_target not in self.FAN_SPEEDS):
|
||||||
# cannot read /sys/class/hwmon/hwmon5/recalculate, so guess based on available fan info
|
# cannot read /sys/class/hwmon/hwmon5/recalculate, so guess based on available fan info
|
||||||
# NOTE: the fan takes time to ramp up, so fan_target will never approximately equal fan_input
|
# NOTE: the fan takes time to ramp up, so fan_target will never approximately equal fan_input
|
||||||
# when fan_target was changed recently (hence set voltage caching)
|
# when fan_target was changed recently (hence set RPM caching)
|
||||||
return len(self.FAN_SPEEDS)
|
return len(self.FAN_SPEEDS)
|
||||||
else:
|
else:
|
||||||
# quantize voltage to nearest tick (price is right rules; closest without going over)
|
# quantize RPM to nearest tick (price is right rules; closest without going over)
|
||||||
for i in range(len(self.FAN_SPEEDS)-1):
|
for i in range(len(self.FAN_SPEEDS)-1):
|
||||||
if fan_target <= self.FAN_SPEEDS[i]:
|
if fan_target <= self.FAN_SPEEDS[i]:
|
||||||
return i
|
return i
|
||||||
return len(self.FAN_SPEEDS)-1 # any higher value is considered as highest manual setting
|
return len(self.FAN_SPEEDS)-1 # any higher value is considered as highest manual setting
|
||||||
|
|
||||||
async def fantastic_installed(self) -> bool:
|
async def fantastic_installed(self) -> bool:
|
||||||
return os.path.exists("/home/deck/homebrew/plugins/Fantastic")
|
return os.path.exists(FANTASTIC_INSTALL_DIR)
|
||||||
|
|
||||||
# Battery stuff
|
# Battery stuff
|
||||||
|
|
||||||
|
@ -201,22 +244,107 @@ class Plugin:
|
||||||
|
|
||||||
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
|
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
|
||||||
async def _main(self):
|
async def _main(self):
|
||||||
pass
|
# startup: load & apply settings
|
||||||
|
if os.path.exists(SETTINGS_LOCATION):
|
||||||
|
settings = read_json(SETTINGS_LOCATION)
|
||||||
|
logging.debug(f"Loaded settings from {SETTINGS_LOCATION}: {settings}")
|
||||||
|
else:
|
||||||
|
settings = None
|
||||||
|
logging.debug(f"Settings {SETTINGS_LOCATION} does not exist, skipped")
|
||||||
|
if settings is None or settings["persistent"] == False:
|
||||||
|
logging.debug("Ignoring settings from file")
|
||||||
|
self.persistent = False
|
||||||
|
self.cpus = []
|
||||||
|
|
||||||
|
for cpu_number in range(0, Plugin.CPU_COUNT):
|
||||||
|
self.cpus.append(CPU(cpu_number))
|
||||||
|
|
||||||
|
# If any core has two threads, smt is True
|
||||||
|
self.smt = self.cpus[1].status()
|
||||||
|
if(not self.smt):
|
||||||
|
for cpu_number in range(2, len(self.cpus), 2):
|
||||||
|
if(self.cpus[cpu_number].status()):
|
||||||
|
self.smt = True
|
||||||
|
break
|
||||||
|
logging.info(f"SMT state is guessed to be {self.smt}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# apply settings
|
||||||
|
logging.debug("Restoring settings from file")
|
||||||
|
self.persistent = True
|
||||||
|
# CPU
|
||||||
|
self.cpus = []
|
||||||
|
|
||||||
|
for cpu_number in range(0, Plugin.CPU_COUNT):
|
||||||
|
self.cpus.append(CPU(cpu_number, settings=settings["cpu"]["threads"][cpu_number]))
|
||||||
|
self.smt = settings["cpu"]["smt"]
|
||||||
|
write_cpu_boost(settings["cpu"]["boost"])
|
||||||
|
# GPU
|
||||||
|
write_gpu_ppt(1, settings["gpu"]["slowppt"])
|
||||||
|
write_gpu_ppt(2, settings["gpu"]["fastppt"])
|
||||||
|
# Fan
|
||||||
|
if not (os.path.exists(FANTASTIC_INSTALL_DIR) or settings["fan"]["auto"]):
|
||||||
|
write_to_sys("/sys/class/hwmon/hwmon5/recalculate", 1)
|
||||||
|
write_to_sys("/sys/class/hwmon/hwmon5/fan1_target", settings["fan"]["target"])
|
||||||
|
self.dirty = False
|
||||||
|
logging.info("Handled saved settings, back-end startup complete")
|
||||||
|
# work loop
|
||||||
|
while True:
|
||||||
|
if self.modified_settings and self.persistent:
|
||||||
|
self.save_settings(self)
|
||||||
|
self.modified_settings = False
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# called from main_view::onViewReady
|
# called from main_view::onViewReady
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
self.cpus = []
|
delta = time.time() - startup_time
|
||||||
|
logging.info(f"Front-end initialised {delta}s after startup")
|
||||||
|
|
||||||
for cpu_number in range(0, Plugin.CPU_COUNT):
|
# persistence
|
||||||
self.cpus.append(CPU(cpu_number))
|
|
||||||
|
async def get_persistent(self) -> bool:
|
||||||
|
return self.persistent
|
||||||
|
|
||||||
|
async def set_persistent(self, enabled: bool):
|
||||||
|
logging.debug(f"Persistence is now: {enabled}")
|
||||||
|
self.persistent = enabled
|
||||||
|
self.save_settings(self)
|
||||||
|
|
||||||
|
def current_settings(self) -> dict:
|
||||||
|
settings = dict()
|
||||||
|
settings["cpu"] = self.current_cpu_settings(self)
|
||||||
|
settings["gpu"] = self.current_gpu_settings(self)
|
||||||
|
settings["fan"] = self.current_fan_settings(self)
|
||||||
|
settings["persistent"] = self.persistent
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def current_cpu_settings(self) -> dict:
|
||||||
|
settings = dict()
|
||||||
|
cpu_settings = []
|
||||||
|
for cpu in self.cpus:
|
||||||
|
cpu_settings.append(cpu.settings())
|
||||||
|
settings["threads"] = cpu_settings
|
||||||
|
settings["smt"] = self.smt
|
||||||
|
settings["boost"] = read_cpu_boost()
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def current_gpu_settings(self) -> dict:
|
||||||
|
settings = dict()
|
||||||
|
settings["slowppt"] = read_gpu_ppt(1)
|
||||||
|
settings["fastppt"] = read_gpu_ppt(2)
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def current_fan_settings(self) -> dict:
|
||||||
|
settings = dict()
|
||||||
|
settings["target"] = read_fan_target()
|
||||||
|
settings["auto"] = self.auto_fan
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def save_settings(self):
|
||||||
|
settings = self.current_settings(self)
|
||||||
|
logging.info(f"Saving settings to file: {settings}")
|
||||||
|
write_json(SETTINGS_LOCATION, settings)
|
||||||
|
|
||||||
# If any core has two threads, smt is True
|
|
||||||
self.smt = self.cpus[1].status()
|
|
||||||
if(not self.smt):
|
|
||||||
for cpu_number in range(2, len(self.cpus), 2):
|
|
||||||
if(self.cpus[cpu_number].status()):
|
|
||||||
self.smt = True
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
# these are stateless (well, the state is not saved internally) functions, so there's no need for these to be called like a class method
|
# these are stateless (well, the state is not saved internally) functions, so there's no need for these to be called like a class method
|
||||||
|
@ -233,10 +361,42 @@ def cpu_governor_scaling_path(cpu_number: int) -> str:
|
||||||
def gpu_power_path(power_number: int) -> str:
|
def gpu_power_path(power_number: int) -> str:
|
||||||
return f"/sys/class/hwmon/hwmon4/power{power_number}_cap"
|
return f"/sys/class/hwmon/hwmon4/power{power_number}_cap"
|
||||||
|
|
||||||
|
def read_cpu_boost() -> bool:
|
||||||
|
return read_from_sys("/sys/devices/system/cpu/cpufreq/boost") == "1"
|
||||||
|
|
||||||
|
def write_cpu_boost(enable: bool):
|
||||||
|
write_to_sys("/sys/devices/system/cpu/cpufreq/boost", int(enable))
|
||||||
|
|
||||||
|
def read_gpu_ppt(power_number: int) -> int:
|
||||||
|
return read_sys_int(gpu_power_path(power_number))
|
||||||
|
|
||||||
|
def write_gpu_ppt(power_number:int, value: int):
|
||||||
|
write_to_sys(gpu_power_path(power_number), value)
|
||||||
|
|
||||||
|
def read_fan_target() -> int:
|
||||||
|
return read_sys_int("/sys/class/hwmon/hwmon5/fan1_target")
|
||||||
|
|
||||||
def write_to_sys(path, value: int):
|
def write_to_sys(path, value: int):
|
||||||
with open(path, mode="w") as f:
|
with open(path, mode="w") as f:
|
||||||
f.write(str(value))
|
f.write(str(value))
|
||||||
|
logging.debug(f"Wrote `{value}` to {path}")
|
||||||
|
|
||||||
def read_from_sys(path, amount=1):
|
def read_from_sys(path, amount=1):
|
||||||
with open(path, mode="r") as f:
|
with open(path, mode="r") as f:
|
||||||
return f.read(amount)
|
value = f.read(amount)
|
||||||
|
logging.debug(f"Read `{value}` from {path}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def read_sys_int(path) -> int:
|
||||||
|
return int(read_from_sys(path, amount=-1).strip())
|
||||||
|
|
||||||
|
def write_json(path, data):
|
||||||
|
with open(path, mode="w") as f:
|
||||||
|
json.dump(data, f) # I always guess which is which param and I hate it
|
||||||
|
|
||||||
|
def read_json(path):
|
||||||
|
with open(path, mode="r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
os_release = read_from_sys("/etc/os-release", amount=-1).strip()
|
||||||
|
logging.info(f"/etc/os-release\n{os_release}")
|
||||||
|
|
|
@ -74,6 +74,14 @@
|
||||||
return call_plugin_method("get_charge_design", {});
|
return call_plugin_method("get_charge_design", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPersistent(value) {
|
||||||
|
return call_plugin_method("set_persistent", {"enabled": value});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersistent() {
|
||||||
|
return call_plugin_method("get_persistent", {});
|
||||||
|
}
|
||||||
|
|
||||||
// other logic
|
// other logic
|
||||||
|
|
||||||
async function onReady() {
|
async function onReady() {
|
||||||
|
@ -95,6 +103,7 @@
|
||||||
selectNotch("fanNotch", await getFanTick(), 8);
|
selectNotch("fanNotch", await getFanTick(), 8);
|
||||||
}
|
}
|
||||||
await updateBatteryStats();
|
await updateBatteryStats();
|
||||||
|
setToggleState(document.getElementById("persistToggle"), await getPersistent());
|
||||||
// this is unimportant; always do it last
|
// this is unimportant; always do it last
|
||||||
await updateVersion();
|
await updateVersion();
|
||||||
window.setInterval(function() {updateBatteryStats().then(_ => {})}, 5000);
|
window.setInterval(function() {updateBatteryStats().then(_ => {})}, 5000);
|
||||||
|
@ -233,6 +242,13 @@
|
||||||
batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(0).toString() + "%)";
|
batCapacityFull.innerText = (7.7 * chargeFull / 1000000).toFixed(2).toString() + " Wh (" + (100 * chargeFull / chargeDesign).toFixed(0).toString() + "%)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function togglePersist() {
|
||||||
|
let toggle = document.getElementById("persistToggle");
|
||||||
|
let isActive = getToggleState(toggle);
|
||||||
|
await setPersistent(!isActive);
|
||||||
|
setToggleState(toggle, !isActive);
|
||||||
|
}
|
||||||
|
|
||||||
let versionCount = -1;
|
let versionCount = -1;
|
||||||
async function updateVersion() {
|
async function updateVersion() {
|
||||||
let version = await getVersion();
|
let version = await getVersion();
|
||||||
|
@ -535,6 +551,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="quickaccesscontrols_PanelSection_2C0g0" style="padding:0px 4px;">
|
||||||
|
<div class="quickaccesscontrols_PanelSectionRow_2VQ88">
|
||||||
|
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_WithDescription_3bMIS gamepaddialog_ExtraPaddingOnChildrenBelow_5UO-_ gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;">
|
||||||
|
<div class="gamepaddialog_FieldLabelRow_H9WOq">
|
||||||
|
<div class="gamepaddialog_FieldLabel_3b0U-">
|
||||||
|
Persistent
|
||||||
|
</div>
|
||||||
|
<div class="gamepaddialog_FieldChildren_14_HB">
|
||||||
|
<div id="persistToggle" tabindex="0" class="gamepaddialog_Toggle_24G4g Focusable" onclick="togglePersist()">
|
||||||
|
<div class="gamepaddialog_ToggleRail_2JtC3"></div>
|
||||||
|
<div class="gamepaddialog_ToggleSwitch_3__OD"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gamepaddialog_FieldDescription_2OJfk">Restores settings after a reboot</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="quickaccesscontrols_PanelSection_2C0g0" style="padding:0px 4px;">
|
<div class="quickaccesscontrols_PanelSection_2C0g0" style="padding:0px 4px;">
|
||||||
<div class="quickaccesscontrols_PanelSectionRow_2VQ88" onclick="updateVersion()">
|
<div class="quickaccesscontrols_PanelSectionRow_2VQ88" onclick="updateVersion()">
|
||||||
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparator_1lUZx gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;">
|
<div class="gamepaddialog_Field_S-_La gamepaddialog_WithFirstRow_qFXi6 gamepaddialog_VerticalAlignCenter_3XNvA gamepaddialog_InlineWrapShiftsChildrenBelow_pHUb6 gamepaddialog_WithBottomSeparator_1lUZx gamepaddialog_StandardPadding_XRBFu gamepaddialog_HighlightOnFocus_wE4V6 Panel Focusable" style="--indent-level:0;">
|
||||||
|
@ -548,5 +581,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue