wip: basic theming support

This commit is contained in:
2025-10-28 21:23:06 +08:00
parent 9c96714c8f
commit 4409986687
13 changed files with 1457 additions and 5 deletions

49
app.py
View File

@ -2,12 +2,13 @@ import sys
from pathlib import Path
import structlog
from PySide6.QtCore import QCoreApplication, QObject, Qt, QUrl
from PySide6.QtCore import QCoreApplication, QEvent, QObject, Qt, QUrl
from PySide6.QtGui import QGuiApplication, QIcon
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle
from ui.resources import resources_rc # noqa: F401
from ui.theme import ThemeManager
from ui.utils import url # noqa: F401
from ui.viewmodels import overview # noqa: F401
@ -18,6 +19,34 @@ DEFAULT_FONTS = ["微软雅黑", "Microsoft YaHei UI", "Microsoft YaHei", "Segoe
logger: structlog.stdlib.BoundLogger = structlog.get_logger()
class ThemeChangeEventFilter(QObject):
logger: structlog.stdlib.BoundLogger = structlog.get_logger(
tag="ThemeChangeEventFilter",
)
def __init__(self, *, themeManager: ThemeManager):
super().__init__(None)
self.themeManager = themeManager
self.scheme = self.themeManager.getCurrentScheme()
def doSomething(self) -> None:
scheme = self.themeManager.getCurrentScheme()
if scheme == self.scheme:
self.logger.debug("Ignored same scheme event (%r)", scheme)
return
self.scheme = scheme
self.themeManager.updateTheme()
self.logger.debug("something done")
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if event.type() == QEvent.Type.ThemeChange:
self.doSomething()
return False
def main() -> None:
app = QGuiApplication(sys.argv)
app.setFont(DEFAULT_FONTS)
@ -25,10 +54,20 @@ def main() -> None:
app.setApplicationDisplayName("Arcaea Offline")
app.setWindowIcon(QIcon(":/images/icon.png"))
QQuickStyle.setStyle("Fusion")
engine = QQmlApplicationEngine()
themeManager = ThemeManager(parent=app)
def onThemeManagerThemeChanged():
logger.debug("App palette changed")
app.setPalette(themeManager.qPalette) # pyright: ignore[reportArgumentType]
engine.rootContext().setContextProperty("appTheme", themeManager.qmlExposer)
onThemeManagerThemeChanged()
themeManager.themeChanged.connect(onThemeManagerThemeChanged)
QQuickStyle.setStyle("Fusion")
def onEngineObjectCreated(obj: QObject | None, objUrl: QUrl) -> None:
if obj is None:
logger.critical("rootObject is None! Exiting!")
@ -41,6 +80,10 @@ def main() -> None:
engine.load("ui/qmls/App.qml")
rootObject = engine.rootObjects()[0]
ef = ThemeChangeEventFilter(themeManager=themeManager)
rootObject.installEventFilter(ef)
sys.exit(app.exec())

View File

@ -16,6 +16,7 @@ dependencies = [
"exif~=1.6.0",
"PySide6==6.10.0",
"Pillow~=10.1.0",
"materialyoucolor~=2.0.10",
]
classifiers = [
"Development Status :: 3 - Alpha",

View File

@ -10,6 +10,45 @@ ApplicationWindow {
width: 800
height: 600
SystemPalette {
id: systemPaletteActive
colorGroup: SystemPalette.Active
}
SystemPalette {
id: systemPaletteDisabled
colorGroup: SystemPalette.Disabled
}
palette {
accent: systemPaletteActive.accent
alternateBase: systemPaletteActive.alternateBase
base: systemPaletteActive.base
button: systemPaletteActive.button
buttonText: systemPaletteActive.buttonText
dark: systemPaletteActive.dark
highlight: systemPaletteActive.highlight
highlightedText: systemPaletteActive.highlightedText
light: systemPaletteActive.light
mid: systemPaletteActive.mid
midlight: systemPaletteActive.midlight
placeholderText: systemPaletteActive.placeholderText
shadow: systemPaletteActive.shadow
text: systemPaletteActive.text
window: systemPaletteActive.window
windowText: systemPaletteActive.windowText
disabled {
button: systemPaletteDisabled.button
buttonText: systemPaletteDisabled.buttonText
}
inactive {
button: systemPaletteDisabled.button
buttonText: systemPaletteDisabled.buttonText
}
}
StackLayout {
id: stackLayout
anchors.fill: parent

View File

@ -27,8 +27,7 @@ ColumnLayout {
function displayBool(value): string {
if (value === undefined)
return '-';
// TODO: color success & error
return value ? `<font color="lightgreen">Yes</font>` : `<font color="lightpink">No</font>`;
return value ? `<font color="${appTheme.success}">Yes</font>` : `<font color="${appTheme.error}">No</font>`;
}
component LabelLabel: Label {

View File

@ -19,5 +19,8 @@
<file>lang/zh_CN.qm</file>
<file>lang/en_US.qm</file>
<file>themes/default.json</file>
<file>themes/tempest.json</file>
</qresource>
</RCC>

View File

@ -0,0 +1,427 @@
{
"//url": "http://material-foundation.github.io/material-theme-builder/?primary=%234E486C&custom%3ASuccess=%2300C555&colorMatch=true",
"name": "default",
"description": "TYPE: CUSTOM\nMaterial Theme Builder export",
"seed": "#4E486C",
"coreColors": {
"primary": "#4E486C"
},
"extendedColors": [
{
"name": "Success",
"color": "#00C555",
"description": "",
"harmonized": true
}
],
"schemes": {
"light": {
"primary": "#373154",
"surfaceTint": "#605A7F",
"onPrimary": "#FFFFFF",
"primaryContainer": "#4E486C",
"onPrimaryContainer": "#C0B8E3",
"secondary": "#605C6C",
"onSecondary": "#FFFFFF",
"secondaryContainer": "#E3DDF0",
"onSecondaryContainer": "#646071",
"tertiary": "#4F2B40",
"onTertiary": "#FFFFFF",
"tertiaryContainer": "#684157",
"onTertiaryContainer": "#E3B0CA",
"error": "#BA1A1A",
"onError": "#FFFFFF",
"errorContainer": "#FFDAD6",
"onErrorContainer": "#93000A",
"background": "#FDF8FC",
"onBackground": "#1C1B1E",
"surface": "#FDF8FC",
"onSurface": "#1C1B1E",
"surfaceVariant": "#E6E1EA",
"onSurfaceVariant": "#48464D",
"outline": "#79767E",
"outlineVariant": "#C9C5CE",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#313033",
"inverseOnSurface": "#F4EFF3",
"inversePrimary": "#C9C1EC",
"primaryFixed": "#E6DEFF",
"onPrimaryFixed": "#1C1738",
"primaryFixedDim": "#C9C1EC",
"onPrimaryFixedVariant": "#484266",
"secondaryFixed": "#E6E0F3",
"onSecondaryFixed": "#1C1A27",
"secondaryFixedDim": "#C9C4D7",
"onSecondaryFixedVariant": "#484554",
"tertiaryFixed": "#FFD8EA",
"onTertiaryFixed": "#301024",
"tertiaryFixedDim": "#ECB8D2",
"onTertiaryFixedVariant": "#613B51",
"surfaceDim": "#DDD9DC",
"surfaceBright": "#FDF8FC",
"surfaceContainerLowest": "#FFFFFF",
"surfaceContainerLow": "#F7F2F6",
"surfaceContainer": "#F1ECF0",
"surfaceContainerHigh": "#EBE7EA",
"surfaceContainerHighest": "#E6E1E5"
},
"light-medium-contrast": {
"primary": "#373154",
"surfaceTint": "#605A7F",
"onPrimary": "#FFFFFF",
"primaryContainer": "#4E486C",
"onPrimaryContainer": "#EDE7FF",
"secondary": "#373443",
"onSecondary": "#FFFFFF",
"secondaryContainer": "#6F6B7B",
"onSecondaryContainer": "#FFFFFF",
"tertiary": "#4E2B40",
"onTertiary": "#FFFFFF",
"tertiaryContainer": "#684157",
"onTertiaryContainer": "#FFE4EF",
"error": "#740006",
"onError": "#FFFFFF",
"errorContainer": "#CF2C27",
"onErrorContainer": "#FFFFFF",
"background": "#FDF8FC",
"onBackground": "#1C1B1E",
"surface": "#FDF8FC",
"onSurface": "#121113",
"surfaceVariant": "#E6E1EA",
"onSurfaceVariant": "#37353D",
"outline": "#545159",
"outlineVariant": "#6F6C74",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#313033",
"inverseOnSurface": "#F4EFF3",
"inversePrimary": "#C9C1EC",
"primaryFixed": "#6F688E",
"onPrimaryFixed": "#FFFFFF",
"primaryFixedDim": "#565075",
"onPrimaryFixedVariant": "#FFFFFF",
"secondaryFixed": "#6F6B7B",
"onSecondaryFixed": "#FFFFFF",
"secondaryFixedDim": "#565363",
"onSecondaryFixedVariant": "#FFFFFF",
"tertiaryFixed": "#8B6078",
"onTertiaryFixed": "#FFFFFF",
"tertiaryFixedDim": "#71495F",
"onTertiaryFixedVariant": "#FFFFFF",
"surfaceDim": "#C9C5C9",
"surfaceBright": "#FDF8FC",
"surfaceContainerLowest": "#FFFFFF",
"surfaceContainerLow": "#F7F2F6",
"surfaceContainer": "#EBE7EA",
"surfaceContainerHigh": "#E0DCDF",
"surfaceContainerHighest": "#D4D0D4"
},
"light-high-contrast": {
"primary": "#2D2749",
"surfaceTint": "#605A7F",
"onPrimary": "#FFFFFF",
"primaryContainer": "#4A4468",
"onPrimaryContainer": "#FFFFFF",
"secondary": "#2D2A39",
"onSecondary": "#FFFFFF",
"secondaryContainer": "#4A4757",
"onSecondaryContainer": "#FFFFFF",
"tertiary": "#432135",
"onTertiary": "#FFFFFF",
"tertiaryContainer": "#643D53",
"onTertiaryContainer": "#FFFFFF",
"error": "#600004",
"onError": "#FFFFFF",
"errorContainer": "#98000A",
"onErrorContainer": "#FFFFFF",
"background": "#FDF8FC",
"onBackground": "#1C1B1E",
"surface": "#FDF8FC",
"onSurface": "#000000",
"surfaceVariant": "#E6E1EA",
"onSurfaceVariant": "#000000",
"outline": "#2D2B32",
"outlineVariant": "#4A4850",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#313033",
"inverseOnSurface": "#FFFFFF",
"inversePrimary": "#C9C1EC",
"primaryFixed": "#4A4468",
"onPrimaryFixed": "#FFFFFF",
"primaryFixedDim": "#342E50",
"onPrimaryFixedVariant": "#FFFFFF",
"secondaryFixed": "#4A4757",
"onSecondaryFixed": "#FFFFFF",
"secondaryFixedDim": "#34313F",
"onSecondaryFixedVariant": "#FFFFFF",
"tertiaryFixed": "#643D53",
"onTertiaryFixed": "#FFFFFF",
"tertiaryFixedDim": "#4A273C",
"onTertiaryFixedVariant": "#FFFFFF",
"surfaceDim": "#BBB8BB",
"surfaceBright": "#FDF8FC",
"surfaceContainerLowest": "#FFFFFF",
"surfaceContainerLow": "#F4EFF3",
"surfaceContainer": "#E6E1E5",
"surfaceContainerHigh": "#D7D3D7",
"surfaceContainerHighest": "#C9C5C9"
},
"dark": {
"primary": "#C9C1EC",
"surfaceTint": "#C9C1EC",
"onPrimary": "#312C4E",
"primaryContainer": "#4E486C",
"onPrimaryContainer": "#C0B8E3",
"secondary": "#C9C4D7",
"onSecondary": "#312E3D",
"secondaryContainer": "#4A4757",
"onSecondaryContainer": "#BBB6C8",
"tertiary": "#ECB8D2",
"onTertiary": "#48253A",
"tertiaryContainer": "#684157",
"onTertiaryContainer": "#E3B0CA",
"error": "#FFB4AB",
"onError": "#690005",
"errorContainer": "#93000A",
"onErrorContainer": "#FFDAD6",
"background": "#141315",
"onBackground": "#E6E1E5",
"surface": "#141315",
"onSurface": "#E6E1E5",
"surfaceVariant": "#48464D",
"onSurfaceVariant": "#C9C5CE",
"outline": "#938F98",
"outlineVariant": "#48464D",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#E6E1E5",
"inverseOnSurface": "#313033",
"inversePrimary": "#605A7F",
"primaryFixed": "#E6DEFF",
"onPrimaryFixed": "#1C1738",
"primaryFixedDim": "#C9C1EC",
"onPrimaryFixedVariant": "#484266",
"secondaryFixed": "#E6E0F3",
"onSecondaryFixed": "#1C1A27",
"secondaryFixedDim": "#C9C4D7",
"onSecondaryFixedVariant": "#484554",
"tertiaryFixed": "#FFD8EA",
"onTertiaryFixed": "#301024",
"tertiaryFixedDim": "#ECB8D2",
"onTertiaryFixedVariant": "#613B51",
"surfaceDim": "#141315",
"surfaceBright": "#3A393B",
"surfaceContainerLowest": "#0F0E10",
"surfaceContainerLow": "#1C1B1E",
"surfaceContainer": "#201F22",
"surfaceContainerHigh": "#2B292C",
"surfaceContainerHighest": "#363437"
},
"dark-medium-contrast": {
"primary": "#E0D7FF",
"surfaceTint": "#C9C1EC",
"onPrimary": "#262142",
"primaryContainer": "#938CB4",
"onPrimaryContainer": "#000000",
"secondary": "#DFD9ED",
"onSecondary": "#262432",
"secondaryContainer": "#938EA0",
"onSecondaryContainer": "#000000",
"tertiary": "#FFCFE7",
"onTertiary": "#3C1A2E",
"tertiaryContainer": "#B2839C",
"onTertiaryContainer": "#000000",
"error": "#FFD2CC",
"onError": "#540003",
"errorContainer": "#FF5449",
"onErrorContainer": "#000000",
"background": "#141315",
"onBackground": "#E6E1E5",
"surface": "#141315",
"onSurface": "#FFFFFF",
"surfaceVariant": "#48464D",
"onSurfaceVariant": "#DFDAE4",
"outline": "#B4B0BA",
"outlineVariant": "#928F98",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#E6E1E5",
"inverseOnSurface": "#2B292C",
"inversePrimary": "#494367",
"primaryFixed": "#E6DEFF",
"onPrimaryFixed": "#120C2D",
"primaryFixedDim": "#C9C1EC",
"onPrimaryFixedVariant": "#373254",
"secondaryFixed": "#E6E0F3",
"onSecondaryFixed": "#120F1D",
"secondaryFixedDim": "#C9C4D7",
"onSecondaryFixedVariant": "#373443",
"tertiaryFixed": "#FFD8EA",
"onTertiaryFixed": "#230619",
"tertiaryFixedDim": "#ECB8D2",
"onTertiaryFixedVariant": "#4E2B40",
"surfaceDim": "#141315",
"surfaceBright": "#454447",
"surfaceContainerLowest": "#080709",
"surfaceContainerLow": "#1E1D20",
"surfaceContainer": "#28272A",
"surfaceContainerHigh": "#333235",
"surfaceContainerHighest": "#3F3D40"
},
"dark-high-contrast": {
"primary": "#F3EDFF",
"surfaceTint": "#C9C1EC",
"onPrimary": "#000000",
"primaryContainer": "#C6BDE8",
"onPrimaryContainer": "#0B0627",
"secondary": "#F3EDFF",
"onSecondary": "#000000",
"secondaryContainer": "#C5C0D3",
"onSecondaryContainer": "#0C0916",
"tertiary": "#FFEBF3",
"onTertiary": "#000000",
"tertiaryContainer": "#E7B4CE",
"onTertiaryContainer": "#1C0213",
"error": "#FFECE9",
"onError": "#000000",
"errorContainer": "#FFAEA4",
"onErrorContainer": "#220001",
"background": "#141315",
"onBackground": "#E6E1E5",
"surface": "#141315",
"onSurface": "#FFFFFF",
"surfaceVariant": "#48464D",
"onSurfaceVariant": "#FFFFFF",
"outline": "#F3EEF8",
"outlineVariant": "#C5C1CA",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#E6E1E5",
"inverseOnSurface": "#000000",
"inversePrimary": "#494367",
"primaryFixed": "#E6DEFF",
"onPrimaryFixed": "#000000",
"primaryFixedDim": "#C9C1EC",
"onPrimaryFixedVariant": "#120C2D",
"secondaryFixed": "#E6E0F3",
"onSecondaryFixed": "#000000",
"secondaryFixedDim": "#C9C4D7",
"onSecondaryFixedVariant": "#120F1D",
"tertiaryFixed": "#FFD8EA",
"onTertiaryFixed": "#000000",
"tertiaryFixedDim": "#ECB8D2",
"onTertiaryFixedVariant": "#230619",
"surfaceDim": "#141315",
"surfaceBright": "#514F52",
"surfaceContainerLowest": "#000000",
"surfaceContainerLow": "#201F22",
"surfaceContainer": "#313033",
"surfaceContainerHigh": "#3C3B3E",
"surfaceContainerHighest": "#484649"
}
},
"palettes": {
"primary": {
"0": "#000000",
"5": "#110B2C",
"10": "#1C1738",
"15": "#272142",
"20": "#312C4E",
"25": "#3C375A",
"30": "#484266",
"35": "#544E72",
"40": "#605A7F",
"50": "#797299",
"60": "#938CB4",
"70": "#AEA6CF",
"80": "#C9C1EC",
"90": "#E6DEFF",
"95": "#F4EEFF",
"98": "#FDF8FF",
"99": "#FFFBFF",
"100": "#FFFFFF"
},
"secondary": {
"0": "#000000",
"5": "#111018",
"10": "#1C1A23",
"15": "#26252D",
"20": "#312F38",
"25": "#3C3A43",
"30": "#48454F",
"35": "#54515B",
"40": "#605D67",
"50": "#797580",
"60": "#938F9A",
"70": "#AEA9B5",
"80": "#C9C4D0",
"90": "#E6E0EC",
"95": "#F4EEFB",
"98": "#FDF8FF",
"99": "#FFFBFF",
"100": "#FFFFFF"
},
"tertiary": {
"0": "#000000",
"5": "#1B0C13",
"10": "#27171E",
"15": "#322128",
"20": "#3E2B33",
"25": "#49363E",
"30": "#564149",
"35": "#624D55",
"40": "#6F5861",
"50": "#897179",
"60": "#A38A93",
"70": "#BFA4AD",
"80": "#DCBFC9",
"90": "#F9DBE5",
"95": "#FFECF1",
"98": "#FFF8F8",
"99": "#FFFBFF",
"100": "#FFFFFF"
},
"neutral": {
"0": "#000000",
"5": "#111112",
"10": "#1C1B1D",
"15": "#262527",
"20": "#313032",
"25": "#3C3B3D",
"30": "#484648",
"35": "#545254",
"40": "#605E60",
"50": "#797678",
"60": "#939092",
"70": "#ADAAAC",
"80": "#C9C5C7",
"90": "#E5E1E3",
"95": "#F4EFF1",
"98": "#FDF8FA",
"99": "#FFFBFF",
"100": "#FFFFFF"
},
"neutral-variant": {
"0": "#000000",
"5": "#111014",
"10": "#1C1B1F",
"15": "#262529",
"20": "#313034",
"25": "#3C3B3F",
"30": "#48464A",
"35": "#545256",
"40": "#605D62",
"50": "#79767B",
"60": "#939094",
"70": "#AEAAAF",
"80": "#C9C5CA",
"90": "#E6E1E6",
"95": "#F4EFF4",
"98": "#FDF8FD",
"99": "#FFFBFF",
"100": "#FFFFFF"
}
}
}

View File

@ -0,0 +1,427 @@
{
"//url": "http://material-foundation.github.io/material-theme-builder/?primary=%23186D98&custom%3ASuccess=%2300C555&colorMatch=true",
"name": "tempest",
"description": "TYPE: CUSTOM\nMaterial Theme Builder export",
"seed": "#186D98",
"coreColors": {
"primary": "#186D98"
},
"extendedColors": [
{
"name": "Success",
"color": "#00C555",
"description": "",
"harmonized": true
}
],
"schemes": {
"light": {
"primary": "#005479",
"surfaceTint": "#03658F",
"onPrimary": "#FFFFFF",
"primaryContainer": "#186D98",
"onPrimaryContainer": "#CEE9FF",
"secondary": "#496173",
"onSecondary": "#FFFFFF",
"secondaryContainer": "#C9E3F8",
"onSecondaryContainer": "#4D6678",
"tertiary": "#683D7A",
"onTertiary": "#FFFFFF",
"tertiaryContainer": "#825594",
"onTertiaryContainer": "#F9DCFF",
"error": "#BA1A1A",
"onError": "#FFFFFF",
"errorContainer": "#FFDAD6",
"onErrorContainer": "#93000A",
"background": "#F7F9FD",
"onBackground": "#191C1F",
"surface": "#F7F9FD",
"onSurface": "#191C1F",
"surfaceVariant": "#DCE3EB",
"onSurfaceVariant": "#40484E",
"outline": "#70787F",
"outlineVariant": "#C0C7CF",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#2D3134",
"inverseOnSurface": "#EFF1F5",
"inversePrimary": "#88CEFE",
"primaryFixed": "#C8E6FF",
"onPrimaryFixed": "#001E2E",
"primaryFixedDim": "#88CEFE",
"onPrimaryFixedVariant": "#004C6D",
"secondaryFixed": "#CCE6FB",
"onSecondaryFixed": "#021E2D",
"secondaryFixedDim": "#B0CADF",
"onSecondaryFixedVariant": "#314A5B",
"tertiaryFixed": "#F8D8FF",
"onTertiaryFixed": "#300443",
"tertiaryFixedDim": "#E8B4FA",
"onTertiaryFixedVariant": "#603572",
"surfaceDim": "#D8DADE",
"surfaceBright": "#F7F9FD",
"surfaceContainerLowest": "#FFFFFF",
"surfaceContainerLow": "#F2F4F7",
"surfaceContainer": "#ECEEF2",
"surfaceContainerHigh": "#E6E8EC",
"surfaceContainerHighest": "#E0E2E6"
},
"light-medium-contrast": {
"primary": "#003A55",
"surfaceTint": "#03658F",
"onPrimary": "#FFFFFF",
"primaryContainer": "#186D98",
"onPrimaryContainer": "#FFFFFF",
"secondary": "#203949",
"onSecondary": "#FFFFFF",
"secondaryContainer": "#577082",
"onSecondaryContainer": "#FFFFFF",
"tertiary": "#4E2460",
"onTertiary": "#FFFFFF",
"tertiaryContainer": "#825594",
"onTertiaryContainer": "#FFFFFF",
"error": "#740006",
"onError": "#FFFFFF",
"errorContainer": "#CF2C27",
"onErrorContainer": "#FFFFFF",
"background": "#F7F9FD",
"onBackground": "#191C1F",
"surface": "#F7F9FD",
"onSurface": "#0E1214",
"surfaceVariant": "#DCE3EB",
"onSurfaceVariant": "#2F373D",
"outline": "#4C535A",
"outlineVariant": "#666E75",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#2D3134",
"inverseOnSurface": "#EFF1F5",
"inversePrimary": "#88CEFE",
"primaryFixed": "#23749F",
"onPrimaryFixed": "#FFFFFF",
"primaryFixedDim": "#005B82",
"onPrimaryFixedVariant": "#FFFFFF",
"secondaryFixed": "#577082",
"onSecondaryFixed": "#FFFFFF",
"secondaryFixedDim": "#3F5869",
"onSecondaryFixedVariant": "#FFFFFF",
"tertiaryFixed": "#895C9B",
"onTertiaryFixed": "#FFFFFF",
"tertiaryFixedDim": "#6F4381",
"onTertiaryFixedVariant": "#FFFFFF",
"surfaceDim": "#C4C7CA",
"surfaceBright": "#F7F9FD",
"surfaceContainerLowest": "#FFFFFF",
"surfaceContainerLow": "#F2F4F7",
"surfaceContainer": "#E6E8EC",
"surfaceContainerHigh": "#DBDDE1",
"surfaceContainerHighest": "#CFD2D5"
},
"light-high-contrast": {
"primary": "#003046",
"surfaceTint": "#03658F",
"onPrimary": "#FFFFFF",
"primaryContainer": "#004E71",
"onPrimaryContainer": "#FFFFFF",
"secondary": "#152F3F",
"onSecondary": "#FFFFFF",
"secondaryContainer": "#344C5D",
"onSecondaryContainer": "#FFFFFF",
"tertiary": "#421955",
"onTertiary": "#FFFFFF",
"tertiaryContainer": "#623874",
"onTertiaryContainer": "#FFFFFF",
"error": "#600004",
"onError": "#FFFFFF",
"errorContainer": "#98000A",
"onErrorContainer": "#FFFFFF",
"background": "#F7F9FD",
"onBackground": "#191C1F",
"surface": "#F7F9FD",
"onSurface": "#000000",
"surfaceVariant": "#DCE3EB",
"onSurfaceVariant": "#000000",
"outline": "#252D33",
"outlineVariant": "#424A51",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#2D3134",
"inverseOnSurface": "#FFFFFF",
"inversePrimary": "#88CEFE",
"primaryFixed": "#004E71",
"onPrimaryFixed": "#FFFFFF",
"primaryFixedDim": "#003650",
"onPrimaryFixedVariant": "#FFFFFF",
"secondaryFixed": "#344C5D",
"onSecondaryFixed": "#FFFFFF",
"secondaryFixedDim": "#1C3546",
"onSecondaryFixedVariant": "#FFFFFF",
"tertiaryFixed": "#623874",
"onTertiaryFixed": "#FFFFFF",
"tertiaryFixedDim": "#4A205C",
"onTertiaryFixedVariant": "#FFFFFF",
"surfaceDim": "#B6B9BD",
"surfaceBright": "#F7F9FD",
"surfaceContainerLowest": "#FFFFFF",
"surfaceContainerLow": "#EFF1F5",
"surfaceContainer": "#E0E2E6",
"surfaceContainerHigh": "#D2D4D8",
"surfaceContainerHighest": "#C4C7CA"
},
"dark": {
"primary": "#88CEFE",
"surfaceTint": "#88CEFE",
"onPrimary": "#00344D",
"primaryContainer": "#186D98",
"onPrimaryContainer": "#CEE9FF",
"secondary": "#B0CADF",
"onSecondary": "#1A3343",
"secondaryContainer": "#314A5B",
"onSecondaryContainer": "#9FB8CD",
"tertiary": "#E8B4FA",
"onTertiary": "#471E59",
"tertiaryContainer": "#825594",
"onTertiaryContainer": "#F9DCFF",
"error": "#FFB4AB",
"onError": "#690005",
"errorContainer": "#93000A",
"onErrorContainer": "#FFDAD6",
"background": "#101417",
"onBackground": "#E0E2E6",
"surface": "#101417",
"onSurface": "#E0E2E6",
"surfaceVariant": "#40484E",
"onSurfaceVariant": "#C0C7CF",
"outline": "#8A9299",
"outlineVariant": "#40484E",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#E0E2E6",
"inverseOnSurface": "#2D3134",
"inversePrimary": "#03658F",
"primaryFixed": "#C8E6FF",
"onPrimaryFixed": "#001E2E",
"primaryFixedDim": "#88CEFE",
"onPrimaryFixedVariant": "#004C6D",
"secondaryFixed": "#CCE6FB",
"onSecondaryFixed": "#021E2D",
"secondaryFixedDim": "#B0CADF",
"onSecondaryFixedVariant": "#314A5B",
"tertiaryFixed": "#F8D8FF",
"onTertiaryFixed": "#300443",
"tertiaryFixedDim": "#E8B4FA",
"onTertiaryFixedVariant": "#603572",
"surfaceDim": "#101417",
"surfaceBright": "#363A3D",
"surfaceContainerLowest": "#0B0F11",
"surfaceContainerLow": "#191C1F",
"surfaceContainer": "#1D2023",
"surfaceContainerHigh": "#272A2D",
"surfaceContainerHighest": "#323538"
},
"dark-medium-contrast": {
"primary": "#BBE1FF",
"surfaceTint": "#88CEFE",
"onPrimary": "#00293D",
"primaryContainer": "#5098C5",
"onPrimaryContainer": "#000000",
"secondary": "#C6E0F5",
"onSecondary": "#0D2838",
"secondaryContainer": "#7B94A7",
"onSecondaryContainer": "#000000",
"tertiary": "#F5D0FF",
"onTertiary": "#3B114E",
"tertiaryContainer": "#AF7FC1",
"onTertiaryContainer": "#000000",
"error": "#FFD2CC",
"onError": "#540003",
"errorContainer": "#FF5449",
"onErrorContainer": "#000000",
"background": "#101417",
"onBackground": "#E0E2E6",
"surface": "#101417",
"onSurface": "#FFFFFF",
"surfaceVariant": "#40484E",
"onSurfaceVariant": "#D6DDE5",
"outline": "#ABB3BB",
"outlineVariant": "#899199",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#E0E2E6",
"inverseOnSurface": "#272A2D",
"inversePrimary": "#004D6F",
"primaryFixed": "#C8E6FF",
"onPrimaryFixed": "#00131F",
"primaryFixedDim": "#88CEFE",
"onPrimaryFixedVariant": "#003A55",
"secondaryFixed": "#CCE6FB",
"onSecondaryFixed": "#00131F",
"secondaryFixedDim": "#B0CADF",
"onSecondaryFixedVariant": "#203949",
"tertiaryFixed": "#F8D8FF",
"onTertiaryFixed": "#220032",
"tertiaryFixedDim": "#E8B4FA",
"onTertiaryFixedVariant": "#4E2460",
"surfaceDim": "#101417",
"surfaceBright": "#424548",
"surfaceContainerLowest": "#05080A",
"surfaceContainerLow": "#1B1E21",
"surfaceContainer": "#25282B",
"surfaceContainerHigh": "#303336",
"surfaceContainerHighest": "#3B3E41"
},
"dark-high-contrast": {
"primary": "#E3F2FF",
"surfaceTint": "#88CEFE",
"onPrimary": "#000000",
"primaryContainer": "#84CAFA",
"onPrimaryContainer": "#000D17",
"secondary": "#E3F2FF",
"onSecondary": "#000000",
"secondaryContainer": "#ACC6DB",
"onSecondaryContainer": "#000D17",
"tertiary": "#FDEAFF",
"onTertiary": "#000000",
"tertiaryContainer": "#E4B0F6",
"onTertiaryContainer": "#190026",
"error": "#FFECE9",
"onError": "#000000",
"errorContainer": "#FFAEA4",
"onErrorContainer": "#220001",
"background": "#101417",
"onBackground": "#E0E2E6",
"surface": "#101417",
"onSurface": "#FFFFFF",
"surfaceVariant": "#40484E",
"onSurfaceVariant": "#FFFFFF",
"outline": "#E9F1F9",
"outlineVariant": "#BCC3CB",
"shadow": "#000000",
"scrim": "#000000",
"inverseSurface": "#E0E2E6",
"inverseOnSurface": "#000000",
"inversePrimary": "#004D6F",
"primaryFixed": "#C8E6FF",
"onPrimaryFixed": "#000000",
"primaryFixedDim": "#88CEFE",
"onPrimaryFixedVariant": "#00131F",
"secondaryFixed": "#CCE6FB",
"onSecondaryFixed": "#000000",
"secondaryFixedDim": "#B0CADF",
"onSecondaryFixedVariant": "#00131F",
"tertiaryFixed": "#F8D8FF",
"onTertiaryFixed": "#000000",
"tertiaryFixedDim": "#E8B4FA",
"onTertiaryFixedVariant": "#220032",
"surfaceDim": "#101417",
"surfaceBright": "#4D5054",
"surfaceContainerLowest": "#000000",
"surfaceContainerLow": "#1D2023",
"surfaceContainer": "#2D3134",
"surfaceContainerHigh": "#383C3F",
"surfaceContainerHighest": "#44474A"
}
},
"palettes": {
"primary": {
"0": "#000000",
"5": "#00131F",
"10": "#001E2E",
"15": "#00293D",
"20": "#00344D",
"25": "#00405D",
"30": "#004C6D",
"35": "#00587E",
"40": "#03658F",
"50": "#317EAA",
"60": "#5098C5",
"70": "#6CB3E1",
"80": "#88CEFE",
"90": "#C8E6FF",
"95": "#E5F2FF",
"98": "#F6FAFF",
"99": "#FBFCFF",
"100": "#FFFFFF"
},
"secondary": {
"0": "#000000",
"5": "#05121B",
"10": "#0F1D26",
"15": "#1A2731",
"20": "#25323C",
"25": "#303D47",
"30": "#3B4853",
"35": "#46545F",
"40": "#52606B",
"50": "#6B7984",
"60": "#84929E",
"70": "#9FADB9",
"80": "#BAC8D5",
"90": "#D6E4F2",
"95": "#E5F2FF",
"98": "#F6FAFF",
"99": "#FBFCFF",
"100": "#FFFFFF"
},
"tertiary": {
"0": "#000000",
"5": "#140D25",
"10": "#1E1730",
"15": "#29223B",
"20": "#342C46",
"25": "#3F3752",
"30": "#4B425E",
"35": "#564E6A",
"40": "#635A76",
"50": "#7C7290",
"60": "#968CAB",
"70": "#B1A6C6",
"80": "#CDC1E2",
"90": "#E9DDFF",
"95": "#F6EDFF",
"98": "#FEF7FF",
"99": "#FFFBFF",
"100": "#FFFFFF"
},
"neutral": {
"0": "#000000",
"5": "#0F1113",
"10": "#1A1C1E",
"15": "#242628",
"20": "#2F3132",
"25": "#3A3B3D",
"30": "#454749",
"35": "#515254",
"40": "#5D5E60",
"50": "#767779",
"60": "#909193",
"70": "#AAABAD",
"80": "#C6C6C8",
"90": "#E2E2E4",
"95": "#F1F0F3",
"98": "#F9F9FB",
"99": "#FCFCFE",
"100": "#FFFFFF"
},
"neutral-variant": {
"0": "#000000",
"5": "#0C1215",
"10": "#171C20",
"15": "#21262B",
"20": "#2C3135",
"25": "#373C40",
"30": "#42474C",
"35": "#4E5358",
"40": "#5A5F64",
"50": "#73787D",
"60": "#8C9196",
"70": "#A7ACB1",
"80": "#C2C7CC",
"90": "#DEE3E8",
"95": "#EDF1F7",
"98": "#F6FAFF",
"99": "#FBFCFF",
"100": "#FFFFFF"
}
}
}

3
ui/theme/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .manager import ThemeManager
__all__ = ["ThemeManager"]

160
ui/theme/manager.py Normal file
View File

@ -0,0 +1,160 @@
import dataclasses
import json
from typing import overload
import structlog
from PySide6.QtCore import Property, QObject, QResource, Qt, Signal
from PySide6.QtGui import QColor, QGuiApplication, QPalette
from .material3 import Material3DynamicThemeImpl, Material3ThemeImpl
from .qml import ThemeQmlExposer
from .shared import ThemeImpl, ThemeInfo, _TCustomPalette, _TScheme
QML_IMPORT_NAME = "internal.ui.theme"
QML_IMPORT_MAJOR_VERSION = 1
QML_IMPORT_MINOR_VERSION = 0
_THEME_CACHES: dict[ThemeInfo, ThemeImpl] = {}
logger: structlog.stdlib.BoundLogger = structlog.get_logger()
class ThemeManager(QObject):
_void = Signal()
themeChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._qPalette = QPalette()
self._customPalette: _TCustomPalette = {
"primary": QColor.fromString("#616161"),
"success": QColor.fromString("#616161"),
"error": QColor.fromString("#616161"),
}
self._lastThemeInfo = ThemeInfo(
series="material3",
name="default",
scheme=self.getCurrentScheme(),
)
self._qmlExposer = ThemeQmlExposer(themeImpl=ThemeImpl())
self._cacheMaterial3Theme(themeName="default", scheme="light")
self._cacheMaterial3Theme(themeName="default", scheme="dark")
self._cacheMaterial3Theme(themeName="tempest", scheme="light")
self._cacheMaterial3Theme(themeName="tempest", scheme="dark")
self._cacheMaterial3DynamicTheme(themeName="default", scheme="light")
self._cacheMaterial3DynamicTheme(themeName="default", scheme="dark")
self._cacheMaterial3DynamicTheme(themeName="tempest", scheme="light")
self._cacheMaterial3DynamicTheme(themeName="tempest", scheme="dark")
self.setTheme("material3-dynamic", "default")
def getCurrentScheme(self) -> _TScheme:
qApp: QGuiApplication = QGuiApplication.instance() # pyright: ignore[reportAssignmentType]
return (
"dark"
if qApp.styleHints().colorScheme() == Qt.ColorScheme.Dark
else "light"
)
def getMaterial3Theme(self, themeName: str, scheme: _TScheme) -> Material3ThemeImpl:
themeDataResource = QResource(f":/themes/{themeName}.json")
if not themeDataResource.isValid():
raise ValueError(f"Material3 theme {themeName!r} not found")
themeData = json.loads(
themeDataResource.uncompressedData().data().decode("utf-8")
)
return Material3ThemeImpl(themeData=themeData, scheme=scheme)
def getMaterial3DynamicTheme(
self, themeName: str, scheme: _TScheme
) -> Material3DynamicThemeImpl:
themeDataResource = QResource(f":/themes/{themeName}.json")
if not themeDataResource.isValid():
raise ValueError(f"Material3 theme {themeName!r} not found")
themeData = json.loads(
themeDataResource.uncompressedData().data().decode("utf-8")
)
return Material3DynamicThemeImpl(
sourceColorHex=themeData["seed"],
scheme=scheme,
name=themeName,
)
def _cacheTheme(self, *, themeImpl: ThemeImpl):
_THEME_CACHES[themeImpl.info] = themeImpl
logger.debug("Theme %r cached", themeImpl.info)
def _getCachedTheme(self, *, themeInfo: ThemeInfo):
cachedTheme = _THEME_CACHES.get(themeInfo)
if cachedTheme is None:
raise KeyError(f"Theme {themeInfo!r} not cached")
return cachedTheme
def _cacheMaterial3Theme(self, *, themeName: str, scheme: _TScheme):
self._cacheTheme(
themeImpl=self.getMaterial3Theme(themeName=themeName, scheme=scheme),
)
def _cacheMaterial3DynamicTheme(self, *, themeName: str, scheme: _TScheme):
self._cacheTheme(
themeImpl=self.getMaterial3DynamicTheme(themeName=themeName, scheme=scheme)
)
@overload
def setTheme(self, *, themeInfo: ThemeInfo): ...
@overload
def setTheme(
self, themeSeries: str, themeName: str, scheme: _TScheme | None = None, /
): ...
def setTheme(self, *args, **kwargs):
if "themeInfo" in kwargs:
themeInfo = kwargs["themeInfo"]
elif 2 <= len(args) <= 3:
themeSeries = args[0]
themeName = args[1]
schemeArg = args[2] if len(args) > 2 else None
scheme = schemeArg or self.getCurrentScheme()
themeInfo = ThemeInfo(series=themeSeries, name=themeName, scheme=scheme)
else:
raise TypeError("Invalid setTheme() call")
logger.debug("Preparing to set theme %r", themeInfo)
cachedTheme = self._getCachedTheme(themeInfo=themeInfo)
self._qPalette = cachedTheme.qPalette
self._customPalette = cachedTheme.customPalette
self._lastThemeInfo = themeInfo
self._qmlExposer.themeImpl = cachedTheme
self.themeChanged.emit()
def updateTheme(self, scheme: _TScheme | None = None):
themeInfo = dataclasses.replace(self._lastThemeInfo) # make a copy
scheme = scheme or self.getCurrentScheme()
themeInfo.scheme = scheme
self.setTheme(themeInfo=themeInfo)
@Property(QPalette, notify=themeChanged)
def qPalette(self) -> QPalette:
return self._qPalette
@Property(dict, notify=themeChanged)
def customPalette(self) -> _TCustomPalette:
return self._customPalette
@Property(ThemeQmlExposer, notify=themeChanged)
def qmlExposer(self) -> ThemeQmlExposer:
return self._qmlExposer

245
ui/theme/material3.py Normal file
View File

@ -0,0 +1,245 @@
from typing import TypedDict
from materialyoucolor.blend import Blend
from materialyoucolor.dynamiccolor.contrast_curve import ContrastCurve
from materialyoucolor.dynamiccolor.dynamic_color import DynamicColor, FromPaletteOptions
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
from materialyoucolor.hct import Hct
from materialyoucolor.palettes.tonal_palette import TonalPalette
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
from PySide6.QtGui import QColor, QPalette
from .shared import ThemeImpl, ThemeInfo, _TCustomPalette, _TScheme
class _M3ThemeDataExtendedColorItem(TypedDict):
name: str
color: str
description: str
harmonized: bool
_M3ThemeDataSchemes = TypedDict(
"_M3ThemeDataSchemes",
{
"light": dict[str, str],
"light-medium-contrast": dict[str, str],
"light-high-contrast": dict[str, str],
"dark": dict[str, str],
"dark-medium-contrast": dict[str, str],
"dark-high-contrast": dict[str, str],
},
)
_M3ThemeDataPalettes = TypedDict(
"_M3ThemeDataPalettes",
{
"primary": dict[str, str],
"secondary": dict[str, str],
"tertiary": dict[str, str],
"neutral": dict[str, str],
"neutral-variant": dict[str, str],
},
)
class _M3ThemeData(TypedDict):
name: str
description: str
seed: str
coreColors: dict[str, str]
extendedColors: list[_M3ThemeDataExtendedColorItem]
schemes: _M3ThemeDataSchemes
palettes: _M3ThemeDataPalettes
def _hexToHct(hexColor: str) -> Hct:
pureHexPart = hexColor[1:] if hexColor.startswith("#") else hexColor
return Hct.from_int(int(f"0xff{pureHexPart}", 16))
def _hctToQColor(hct: Hct) -> QColor:
return QColor.fromRgba(hct.to_int())
class Material3ThemeImpl(ThemeImpl):
COLOR_ROLE_MAPPING: dict[QPalette.ColorRole, str] = {
QPalette.ColorRole.Window: "surface",
QPalette.ColorRole.WindowText: "onSurface",
QPalette.ColorRole.Base: "surfaceContainer",
QPalette.ColorRole.AlternateBase: "surfaceContainerHighest",
QPalette.ColorRole.ToolTipBase: "secondaryContainer",
QPalette.ColorRole.ToolTipText: "onSecondaryContainer",
QPalette.ColorRole.PlaceholderText: "inverseSurface",
QPalette.ColorRole.Text: "onSurface",
QPalette.ColorRole.Button: "primaryContainer",
QPalette.ColorRole.ButtonText: "onPrimaryContainer",
QPalette.ColorRole.BrightText: "onSecondary",
QPalette.ColorRole.Light: "surfaceContainerLowest",
QPalette.ColorRole.Midlight: "surfaceContainerLow",
QPalette.ColorRole.Dark: "inverseSurface",
QPalette.ColorRole.Mid: "surfaceContainer",
QPalette.ColorRole.Shadow: "shadow",
QPalette.ColorRole.Highlight: "primary",
QPalette.ColorRole.Accent: "primary",
QPalette.ColorRole.HighlightedText: "onPrimary",
QPalette.ColorRole.Link: "tertiary",
QPalette.ColorRole.LinkVisited: "tertiaryContainer",
}
def __init__(self, *, themeData: _M3ThemeData, scheme: _TScheme):
self.themeData = themeData
self.scheme: _TScheme = scheme
if self.themeData["schemes"].get(scheme) is None:
raise ValueError(f"Invalid scheme: {scheme}")
def _findExtendedColor(
self, colorName: str
) -> _M3ThemeDataExtendedColorItem | None:
return next(
(it for it in self.themeData["extendedColors"] if it["name"] == colorName),
None,
)
@property
def info(self):
return ThemeInfo(
series="material3",
name=self.themeData["name"],
scheme=self.scheme,
)
@property
def qPalette(self) -> QPalette:
qPalette = QPalette()
for role, name in self.COLOR_ROLE_MAPPING.items():
color = QColor.fromString(self.themeData["schemes"][self.scheme][name])
qPalette.setColor(role, color)
return qPalette
@property
def customPalette(self) -> _TCustomPalette:
primaryHct = _hexToHct(self.themeData["schemes"][self.scheme]["primary"])
successColorItem = self._findExtendedColor("Success")
if successColorItem is None:
raise Exception("Success color not found")
successHct = _hexToHct(successColorItem["color"])
successHarmonizedHct = Hct.from_int(
Blend.harmonize(successHct.to_int(), primaryHct.to_int())
)
return {
"primary": _hctToQColor(primaryHct),
"success": _hctToQColor(successHarmonizedHct),
"error": QColor.fromString(self.themeData["schemes"][self.scheme]["error"]),
}
class Material3DynamicThemeImpl(ThemeImpl):
ACTIVE_COLOR_ROLE_MAPPING: dict[QPalette.ColorRole, DynamicColor] = {
QPalette.ColorRole.Window: MaterialDynamicColors.surface,
QPalette.ColorRole.WindowText: MaterialDynamicColors.onSurface,
QPalette.ColorRole.Base: MaterialDynamicColors.surfaceContainer,
QPalette.ColorRole.AlternateBase: MaterialDynamicColors.surfaceContainerHighest,
QPalette.ColorRole.ToolTipBase: MaterialDynamicColors.secondaryContainer,
QPalette.ColorRole.ToolTipText: MaterialDynamicColors.onSecondaryContainer,
QPalette.ColorRole.PlaceholderText: MaterialDynamicColors.inverseSurface,
QPalette.ColorRole.Text: MaterialDynamicColors.onSurface,
QPalette.ColorRole.Button: MaterialDynamicColors.primaryContainer,
QPalette.ColorRole.ButtonText: MaterialDynamicColors.onPrimaryContainer,
QPalette.ColorRole.BrightText: MaterialDynamicColors.onSecondary,
QPalette.ColorRole.Light: MaterialDynamicColors.surfaceContainerLowest,
QPalette.ColorRole.Midlight: MaterialDynamicColors.surfaceContainerLow,
QPalette.ColorRole.Dark: MaterialDynamicColors.inverseSurface,
QPalette.ColorRole.Mid: MaterialDynamicColors.surfaceContainer,
QPalette.ColorRole.Shadow: MaterialDynamicColors.shadow,
QPalette.ColorRole.Highlight: MaterialDynamicColors.primary,
QPalette.ColorRole.Accent: MaterialDynamicColors.primary,
QPalette.ColorRole.HighlightedText: MaterialDynamicColors.onPrimary,
QPalette.ColorRole.Link: MaterialDynamicColors.tertiary,
QPalette.ColorRole.LinkVisited: MaterialDynamicColors.tertiaryContainer,
}
EXTENDED_COLORS = {
"success": "#00c555",
}
def __init__(self, sourceColorHex: str, scheme: _TScheme, *, name: str):
self.material3Scheme = SchemeTonalSpot(
_hexToHct(sourceColorHex),
is_dark=scheme == "dark",
contrast_level=0.0,
)
self.name = name
@property
def info(self):
return ThemeInfo(
series="material3-dynamic",
name=self.name,
scheme="dark" if self.material3Scheme.is_dark else "light",
)
@property
def qPalette(self) -> QPalette:
qPalette = QPalette()
for role, dynamicColor in self.ACTIVE_COLOR_ROLE_MAPPING.items():
hct = dynamicColor.get_hct(self.material3Scheme)
qColor = QColor.fromRgba(hct.to_int())
qPalette.setColor(QPalette.ColorGroup.Active, role, qColor)
# TODO: disabled palette seems to work only after a theme reload, needs further investigation
if role in [QPalette.ColorRole.Button, QPalette.ColorRole.ButtonText]:
disabledHct = Hct.from_hct(hct.hue, 1.0, hct.tone)
disabledQColor = QColor.fromRgba(disabledHct.to_int())
qPalette.setColor(QPalette.ColorGroup.Disabled, role, disabledQColor)
return qPalette
@property
def customPalette(self) -> _TCustomPalette:
primaryHct = MaterialDynamicColors.primary.get_hct(self.material3Scheme)
errorHct = MaterialDynamicColors.error.get_hct(self.material3Scheme)
extendedPalettes: dict[str, DynamicColor] = {}
for colorName, colorHex in self.EXTENDED_COLORS.items():
colorHct = _hexToHct(colorHex)
colorHarmonized = Blend.harmonize(colorHct.to_int(), primaryHct.to_int())
colorTonalPalette = TonalPalette.from_int(colorHarmonized)
colorSurfacePaletteOptions = DynamicColor.from_palette(
FromPaletteOptions(
name=f"{colorName}_container",
palette=lambda s: colorTonalPalette,
tone=lambda s: 30 if s.is_dark else 90,
is_background=True,
background=lambda s: MaterialDynamicColors.highestSurface(s),
contrast_curve=ContrastCurve(1, 1, 3, 4.5),
)
)
extendedPalettes[colorName] = DynamicColor.from_palette(
FromPaletteOptions(
name=colorName, # pyright: ignore[reportArgumentType]
palette=lambda s: colorTonalPalette,
tone=lambda s: 80 if s.is_dark else 40, # pyright: ignore[reportArgumentType]
is_background=False, # pyright: ignore[reportArgumentType]
background=lambda s: MaterialDynamicColors.highestSurface(s),
contrast_curve=ContrastCurve(3, 4.5, 7, 7),
)
)
return {
"primary": _hctToQColor(primaryHct),
"success": _hctToQColor(
extendedPalettes["success"].get_hct(self.material3Scheme)
),
"error": _hctToQColor(errorHct),
}

37
ui/theme/qml.py Normal file
View File

@ -0,0 +1,37 @@
from PySide6.QtCore import Property, QObject, Signal
from PySide6.QtGui import QColor
from .shared import ThemeImpl
QML_IMPORT_NAME = "internal.ui.theme"
QML_IMPORT_MAJOR_VERSION = 1
QML_IMPORT_MINOR_VERSION = 0
class ThemeQmlExposer(QObject):
themeChanged = Signal()
def __init__(self, *, themeImpl: ThemeImpl, parent: QObject | None = None):
super().__init__(parent)
self._themeImpl = themeImpl
@property
def themeImpl(self) -> ThemeImpl:
return self._themeImpl
@themeImpl.setter
def themeImpl(self, themeImpl: ThemeImpl):
self._themeImpl = themeImpl
self.themeChanged.emit()
@Property(QColor, notify=themeChanged)
def primary(self):
return self._themeImpl.customPalette["primary"]
@Property(QColor, notify=themeChanged)
def success(self):
return self._themeImpl.customPalette["success"]
@Property(QColor, notify=themeChanged)
def error(self):
return self._themeImpl.customPalette["error"]

43
ui/theme/shared.py Normal file
View File

@ -0,0 +1,43 @@
from dataclasses import dataclass
from typing import Literal, TypedDict
from PySide6.QtGui import QColor, QPalette
class _TCustomPalette(TypedDict):
primary: QColor
success: QColor
error: QColor
_TScheme = Literal["light", "dark"]
@dataclass
class ThemeInfo:
series: str
name: str
scheme: _TScheme
def __hash__(self) -> int:
return hash((self.series, self.name, self.scheme))
class ThemeImpl:
DEFAULT_CUSTOM_PALETTE = {
"primary": QColor.fromString("#616161"),
"success": QColor.fromString("#616161"),
"error": QColor.fromString("#616161"),
}
@property
def info(self) -> ThemeInfo:
return ThemeInfo(series="placeholder", name="placeholder", scheme="dark")
@property
def qPalette(self) -> QPalette:
return QPalette()
@property
def customPalette(self) -> _TCustomPalette:
return self.DEFAULT_CUSTOM_PALETTE # pyright: ignore[reportReturnType]

25
uv.lock generated
View File

@ -51,6 +51,7 @@ dependencies = [
{ name = "arcaea-offline-ocr" },
{ name = "colorama" },
{ name = "exif" },
{ name = "materialyoucolor" },
{ name = "pillow" },
{ name = "pyside6" },
{ name = "rich" },
@ -72,6 +73,7 @@ requires-dist = [
{ name = "colorama", specifier = "~=0.4.6" },
{ name = "exif", specifier = "~=1.6.0" },
{ name = "imageio", marker = "extra == 'dev'" },
{ name = "materialyoucolor", specifier = "~=2.0.10" },
{ name = "nuitka", marker = "extra == 'dev'", specifier = "~=2.7.6" },
{ name = "pillow", specifier = "~=10.1.0" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" },
@ -293,6 +295,29 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "materialyoucolor"
version = "2.0.10"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/83/b3/8835bacf50ea32bf000f9118f60da4c8e5febe80ab5929be18ce99a4baea/materialyoucolor-2.0.10.tar.gz", hash = "sha256:31b4d407b9a4fd4b54b30559b0d0313f6d6da1f9d19b75546ed65da52671ea76", size = 250103, upload-time = "2025-01-09T07:56:43.05Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/38/52/36cf8bbb3989f31328ca2a06819b17c021052bcd97a7e703eadace1f225f/materialyoucolor-2.0.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4de08ee488369cf0e055ae17ac43c62684f618781939feaf0ad11bc32572a4b6", size = 316148, upload-time = "2025-01-09T09:02:52.364Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ea/96/4f3919588aa341f8152ad01de784924ac26059077b095069f2ca83525053/materialyoucolor-2.0.10-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad22b0fb6980e0995d5b8437cad3411f628e1ea88c223c94a20b59d5dbcbacb2", size = 206497, upload-time = "2025-01-09T08:34:31.692Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/91/05303aa40da51a71c4e828781a0a34b5ba8c88a9a0f2eec245c1257fe82e/materialyoucolor-2.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:997787ec4224f3e2d9c90e99d82db746f5581670564a22a0f182ad377e704761", size = 150420, upload-time = "2025-01-09T09:02:55.842Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/04/eb/78d067df3948bd6a347677c10d35d89b6092916f78e0b98c51d47fd080f5/materialyoucolor-2.0.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e9a4dd1e444a64ea54982be06314ce4611f3c38fdfae2c33cca7d106f6383288", size = 319432, upload-time = "2025-01-09T09:02:51.999Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/b1/04424f2bd811fe7e803d8a21cc28e8b4f9dea8b07cbc948318d4b4de3dd8/materialyoucolor-2.0.10-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:144fdd9dca272fee60a1779ce35acf24830cd3bc33ff20982ecba4936cb5bf91", size = 208262, upload-time = "2025-01-09T08:34:34.198Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/82/43/926ae82c04e5a5d02523f66e48af8021c0fdd8e01463a51e934137f7a5a0/materialyoucolor-2.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:f126196a051673707297e86b8d0fbd90805a552dc3ac79e1d90442cc3fc877cb", size = 151620, upload-time = "2025-01-09T09:02:52.028Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/69/e8/847974020cde0e09124d7fb51bf41aa92cc3a9cad83f83911e5d67c1d665/materialyoucolor-2.0.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0fc0b43a684536270177b71bf509b247dab4ff816e49d1ac1b124fc1a32e082e", size = 318810, upload-time = "2025-01-09T09:02:52.905Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fa/67/2f3e70d707f86be3c541327693db5105622aa6aa1079d48ff90876d2e650/materialyoucolor-2.0.10-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fe5bbc63124c3ea2f5560ea4a5250af72fce9ab55d29925e6bac7c7ec65329d", size = 208373, upload-time = "2025-01-09T08:34:35.656Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/f2/1119e97e741b33717e26c494b4aa3c5ca57d770df4fa42707b698bfb7588/materialyoucolor-2.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:aedc8596fd7583fd9e23f3e6d4f7b9defd7d5ef6e7889967814b9900c1e3d5b3", size = 151716, upload-time = "2025-01-09T09:02:57.1Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/49/c4517231ec21bdd91a301cb40fdf86c632a95b10d86bc3278bd5d619480c/materialyoucolor-2.0.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:71de9c581c8227c3010c61108a00af9a007c96f99358fdb0628626d0ade817d9", size = 318890, upload-time = "2025-01-09T09:02:52.276Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4d/62/807bea1db749dc45debc2afa51ea4b27f5a219fbe73f6458c2bae2a15427/materialyoucolor-2.0.10-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:978748eca7725d8b569cdecfd9f86a8e34bf946182f79a48b059c98a10f1a0de", size = 208339, upload-time = "2025-01-09T08:34:38.213Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/01/05075b959b228c5c99c80703b7c63532ac6ee49abdf8054077e70908d6e3/materialyoucolor-2.0.10-cp313-cp313-win_amd64.whl", hash = "sha256:2ffc8963a2f10d8acb6776e60e97f0e7929d3392eb282cf2bc63b8e56cd930b3", size = 151751, upload-time = "2025-01-09T09:02:53.299Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/01/56/29e59a5bcc92f0dc0a1de728e711edfcd971a6ef269a97cf461642fc339d/materialyoucolor-2.0.10-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e4827307b1c78897f97f7989237ef688a5c21663a63d327d7cec8e47f107e509", size = 316289, upload-time = "2025-01-09T09:02:53.101Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3c/be/039f46e565b77ce088902c19352c073e16c34a4a8743c95fcad1a537d81f/materialyoucolor-2.0.10-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8cebae884c6106584e1792b910d9588778353255a1a8156cb634e21f1b8670e", size = 206468, upload-time = "2025-01-09T07:56:41.508Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6b/b9/d8946d2d9901ca7bd088ea8efcdc975c73f51766b234fc3a83b821988b2b/materialyoucolor-2.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:4c5141fdd973ab386ebd0242072a7faab28d19cc083ee0d14f5b70c31ad75d60", size = 150332, upload-time = "2025-01-09T09:02:56.525Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"