Implement dark mode theme (#744)

Co-authored-by: DevilXD <4180725+DevilXD@users.noreply.github.com>
This commit is contained in:
kWAYandDevilXD authored and GitHub committed 2025-08-27 17:19:32 +02:00
1 parent f990437731
commit 2737936739
23 files changed
+265 -16

No files matched your search

+10 -4
View File
@@ -3,12 +3,12 @@ name: Build
on:
push:
branches:
- 'master'
- "master"
pull_request:
workflow_dispatch:
env:
PYTHON_VERSION: '3.10'
PYTHON_VERSION: "3.10"
jobs:
validate:
@@ -267,8 +267,14 @@ jobs:
with:
path: artifacts
- name: Delete the existing pre-release
run: gh release delete dev-build --cleanup-tag --yes --repo $GITHUB_REPOSITORY
- name: Delete the existing pre-release (if present)
run: |
TAG=dev-build
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release delete "$TAG" --cleanup-tag --yes --repo "$GITHUB_REPOSITORY"
else
echo "No $TAG release; skipping delete"
fi
env:
GITHUB_TOKEN: ${{github.token}}
+1
View File
@@ -134,6 +134,7 @@ placed past the period character at the end.
-->
@guihkx - For the CI script, CI maintenance, and everything related to Linux builds.
@kWAYTV - For the implementation of the dark mode theme.
@Bamboozul - For the entirety of the Arabic (العربية) translation.
@Suz1e - For the entirety of the Chinese (简体中文) translation and revisions.
+229 -11
View File
@@ -264,6 +264,10 @@ class PaddedListbox(tk.Listbox):
# because 'config = configure' makes mypy complain
self.configure(*args, **kwargs)
def configure_theme(self, *, bg: str, fg: str, sel_bg: str, sel_fg: str):
# Apply basic colors for dark/light mode
super().config(bg=bg, fg=fg, selectbackground=sel_bg, selectforeground=sel_fg)
class MouseOverLabel(ttk.Label):
def __init__(self, *args, alt_text: str = '', reverse: bool = False, **kwargs) -> None:
@@ -819,6 +823,16 @@ class ConsoleOutput:
self._text.see("end") # scroll to the newly added line
self._text.config(state="disabled")
def configure_theme(self, *, bg: str, fg: str, sel_bg: str, sel_fg: str):
# Apply colors to the Tk Text widget used for console output
self._text.config(
bg=bg,
fg=fg,
insertbackground=fg,
selectbackground=sel_bg,
selectforeground=sel_fg,
)
class _Buttons(TypedDict):
frame: ttk.Frame
@@ -1291,6 +1305,10 @@ class InventoryOverview:
self._campaigns: dict[DropsCampaign, CampaignDisplay] = {}
self._drops: dict[str, ttk.Label] = {}
def configure_theme(self, *, bg: str):
# Canvas background needs manual control
self._canvas.configure(bg=bg)
def _update_visibility(self, campaign: DropsCampaign):
# True if the campaign is supposed to show, False makes it hidden.
frame = self._campaigns[campaign]["frame"]
@@ -1528,6 +1546,7 @@ class _SettingsVars(TypedDict):
tray: IntVar
proxy: StringVar
autostart: IntVar
dark_mode: IntVar
language: StringVar
priority_mode: StringVar
tray_notifications: IntVar
@@ -1550,7 +1569,7 @@ class SettingsPanel:
}
def __init__(self, manager: GUIManager, master: ttk.Widget):
self._twitch = manager._twitch
self._manager = manager
self._settings: Settings = manager._twitch.settings
priority_mode = self._settings.priority_mode
if priority_mode not in self.PRIORITY_MODES:
@@ -1561,6 +1580,7 @@ class SettingsPanel:
"language": StringVar(master, _.current),
"proxy": StringVar(master, str(self._settings.proxy)),
"tray": IntVar(master, self._settings.autostart_tray),
"dark_mode": IntVar(master, int(self._settings.dark_mode)),
"priority_mode": StringVar(master, self.PRIORITY_MODES[priority_mode]),
"tray_notifications": IntVar(master, self._settings.tray_notifications),
}
@@ -1616,6 +1636,14 @@ class SettingsPanel:
variable=self._vars["tray_notifications"],
command=self.update_notifications,
).grid(column=1, row=irow, sticky="w")
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "dark_mode")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
checkboxes_frame,
variable=self._vars["dark_mode"],
command=self.update_dark_mode,
).grid(column=1, row=irow, sticky="w")
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "priority_mode")
).grid(column=0, row=(irow := irow + 1), sticky="e")
@@ -1719,7 +1747,7 @@ class SettingsPanel:
ttk.Button(
reload_frame,
text=_("gui", "settings", "reload"),
command=self._twitch.state_change(State.INVENTORY_FETCH),
command=self._manager._twitch.state_change(State.INVENTORY_FETCH),
).grid(column=1, row=0)
self._vars["autostart"].set(self._query_autostart())
@@ -1728,6 +1756,11 @@ class SettingsPanel:
self._priority_list.selection_clear(0, "end")
self._exclude_list.selection_clear(0, "end")
def update_dark_mode(self) -> None:
self._settings.dark_mode = bool(self._vars["dark_mode"].get())
self._settings.alter()
self._manager.apply_theme(self._settings.dark_mode)
def update_notifications(self) -> None:
self._settings.tray_notifications = bool(self._vars["tray_notifications"].get())
@@ -2025,7 +2058,6 @@ class GUIManager:
# style adjustements
self._style = style = ttk.Style(root)
default_font = nametofont("TkDefaultFont")
# theme
theme = ''
# theme = style.theme_names()[6]
@@ -2057,17 +2089,23 @@ class GUIManager:
style.configure("green.TLabel", foreground="green")
style.configure("yellow.TLabel", foreground="goldenrod")
style.configure("red.TLabel", foreground="red")
# fonts
default_font = nametofont("TkDefaultFont")
self._fonts: dict[str, Font] = {
"default": default_font,
"large": default_font.copy(),
"monospaced": default_font.copy(),
"underlined": default_font.copy(),
}
self._fonts["large"].config(size=10)
self._fonts["underlined"].config(underline=True)
self._fonts["monospaced"].config(family="Courier New", size=10)
# label style with a monospace font
monospaced_font = Font(root, family="Courier New", size=10)
style.configure("MS.TLabel", font=monospaced_font)
style.configure("MS.TLabel", font=self._fonts["monospaced"])
# button style with a larger font
large_font = default_font.copy()
large_font.config(size=10)
style.configure("Large.TButton", font=large_font)
style.configure("Large.TButton", font=self._fonts["large"])
# label style that mimics links
link_font = default_font.copy()
link_font.config(underline=True)
style.configure("Link.TLabel", font=link_font, foreground="blue")
style.configure("Link.TLabel", font=self._fonts["underlined"], foreground="blue")
# end of style changes
root_frame = ttk.Frame(root, padding=8)
@@ -2133,6 +2171,12 @@ class GUIManager:
# use old-style window closing protocol for non-windows platforms
root.protocol("WM_DELETE_WINDOW", self.close)
root.protocol("WM_DESTROY_WINDOW", self.close)
# Save current theme and apply palette after widgets are created
try:
self._orig_theme_name = self._style.theme_use()
except Exception:
self._orig_theme_name = ''
self.apply_theme(self._twitch.settings.dark_mode)
# stay hidden in tray if needed, otherwise show the window when everything's ready
if self._twitch.settings.tray:
# NOTE: this starts the tray icon thread
@@ -2277,6 +2321,179 @@ class GUIManager:
# print to our custom output
self.output.print(message)
def apply_theme(self, dark: bool) -> None:
"""
Apply dark/light palette to ttk styles and Tk widgets in a minimal, non-invasive way.
"""
# Palette
if dark:
# Switch to a configurable ttk theme for better color control
if self._style.theme_use() != "clam":
self._style.theme_use("clam")
bg = "#1e1e1e"
fg = "#e6e6e6"
sel_bg = "#094771"
sel_fg = "#ffffff"
link = "#4ea3ff"
surface = "#252525"
header = "#2a2a2a"
fieldbg = "#2b2b2b"
border = "#3c3c3c"
muted = "#b3b3b3"
accent = "#0d99ff"
else:
# Restore original theme if we changed it
if getattr(self, "_orig_theme_name", '') and self._style.theme_use() == "clam":
self._style.theme_use(self._orig_theme_name)
# Use platform defaults but ensure toggling back is readable
bg = "#f0f0f0"
fg = "#000000"
sel_bg = "#cce5ff"
sel_fg = "#000000"
link = "blue"
surface = "#ffffff"
header = "#eeeeee"
fieldbg = "#ffffff"
border = "#cccccc"
muted = "#404040"
accent = "#0a84ff"
s = self._style
# Base containers and labels
s.configure("TFrame", background=bg, foreground=fg)
s.configure("TLabel", background=bg, foreground=fg)
s.configure("TLabelframe", background=bg, foreground=fg)
s.configure("TLabelframe.Label", background=bg, foreground=fg)
s.configure("MS.TLabel", background=bg, foreground=fg)
s.configure("green.TLabel", background=bg)
s.configure("yellow.TLabel", background=bg)
s.configure("red.TLabel", background=bg)
s.configure("Link.TLabel", font=self._fonts["underlined"], background=bg, foreground=link)
# Buttons and checks
s.configure("TButton", background=surface, foreground=fg, bordercolor=border)
s.configure("Large.TButton", background=surface, foreground=fg, bordercolor=border)
s.map(
"TButton",
background=[("active", header), ("pressed", border)],
foreground=[("disabled", muted)],
)
s.configure(
"TCheckbutton",
background=bg,
foreground=fg,
focuscolor=bg,
bordercolor=border,
)
s.map(
"TCheckbutton",
# Remove hover visuals by mapping active/pressed to the base background
background=[
("active", bg),
("pressed", bg),
],
foreground=[("disabled", muted)],
indicatorcolor=[
("selected", accent if dark else fg),
("!selected", border),
],
)
# Notebook
s.configure("TNotebook", background=bg, bordercolor=border)
s.configure("TNotebook.Tab", background=surface, foreground=fg, bordercolor=border)
s.map(
"TNotebook.Tab",
background=[("selected", header), ("active", header)],
foreground=[("disabled", muted)],
)
# Entries/Combos
s.configure(
"TEntry", fieldbackground=fieldbg, background=fieldbg, foreground=fg, insertcolor=fg
)
s.configure(
"TCombobox", fieldbackground=fieldbg, background=fieldbg, foreground=fg, arrowcolor=fg
)
# Ensure readability for readonly comboboxes (Language, Priority mode)
s.map(
"TCombobox",
foreground=[("readonly", fg), ("disabled", muted)],
fieldbackground=[("readonly", fieldbg)],
background=[("readonly", fieldbg)],
arrowcolor=[("readonly", fg)],
)
s.map("TEntry", foreground=[("disabled", muted)])
# Treeview
s.configure(
"Treeview",
background=surface,
fieldbackground=surface,
foreground=fg,
bordercolor=border,
)
s.map(
"Treeview",
background=[("selected", sel_bg)],
foreground=[("selected", sel_fg)],
)
s.configure("Treeview.Heading", background=header, foreground=fg, bordercolor=border)
# Progressbar
s.configure("TProgressbar", background=accent, troughcolor=surface)
# Scrollbars
s.configure(
"Vertical.TScrollbar",
background=surface,
troughcolor=bg,
arrowcolor=fg,
bordercolor=border,
)
s.configure(
"Horizontal.TScrollbar",
background=surface,
troughcolor=bg,
arrowcolor=fg,
bordercolor=border,
)
# Pure Tk widgets
# Console text
self.output.configure_theme(bg=surface, fg=fg, sel_bg=sel_bg, sel_fg=sel_fg)
# Listboxes
self.settings._priority_list.configure_theme(
bg=surface, fg=fg, sel_bg=sel_bg, sel_fg=sel_fg
)
self.settings._exclude_list.configure_theme(
bg=surface, fg=fg, sel_bg=sel_bg, sel_fg=sel_fg
)
# Inventory canvas
self.inv.configure_theme(bg=bg)
# Tk option database for selection/popup list readability (affects Tk-backed widgets)
# Global selection colors and listbox defaults (covers Combobox dropdown)
self._root.option_add("*selectBackground", sel_bg)
self._root.option_add("*selectForeground", sel_fg)
# Combobox dropdown list (Tk Listbox)
for key in (
"*TCombobox*Listbox.background",
"*TCombobox*Listbox.Background",
"*Listbox.background",
):
self._root.option_add(key, surface)
for key in (
"*TCombobox*Listbox.foreground",
"*TCombobox*Listbox.Foreground",
"*Listbox.foreground",
):
self._root.option_add(key, fg)
for key in (
"*TCombobox*Listbox.selectBackground",
"*Listbox.selectBackground",
):
self._root.option_add(key, sel_bg)
for key in (
"*TCombobox*Listbox.selectForeground",
"*Listbox.selectForeground",
):
self._root.option_add(key, sel_fg)
###################
# GUI MANAGER END #
@@ -2403,6 +2620,7 @@ if __name__ == "__main__":
tray=False,
priority=[],
proxy=URL(),
dark_mode=False,
alter=lambda: None,
language="English",
autostart_tray=False,
+1
View File
@@ -135,6 +135,7 @@
"tray": "Automatisk start i systembakken: ",
"tray_notifications": "Bakkemeddelelser: ",
"priority_only": "Kun prioritet: ",
"dark_mode": "Mørk tilstand: ",
"proxy": "Proxy (kræver genstart):"
},
"game_name": "Spilnavn",
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Autostart: ",
"tray": "Autostart ins System Tray: ",
"priority_only": "Nur Priorität: ",
"dark_mode": "Dunkler Modus: ",
"proxy": "Proxy (Erfordert Neustart):"
},
"game_name": "Spiel",
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Ejecutar al iniciar el sistema: ",
"tray": "Ejecutar en la bandeja del sistema: ",
"tray_notifications": "Mostrar notificaciones: ",
"dark_mode": "Modo oscuro: ",
"priority_mode": "Modo de prioridad: ",
"proxy": "Proxy (requiere reinicio):"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Démarrage automatique :",
"tray": "Démarrage automatique dans la barre des tâches :",
"tray_notifications": "Notifications dans la barre des tâches :",
"dark_mode": "Mode sombre : ",
"priority_mode": "Mode de priorité :",
"proxy": "Proxy (nécessite un redémarrage) :"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Mulai secara otomatis :",
"tray": "Mulai otomatis di bilah tugas :",
"tray_notifications": "Pemberitahuan bilah tugas :",
"dark_mode": "Mode gelap: ",
"priority_only": "Prioritas saja :",
"proxy": "Proksi (harus dimulai ulang) :"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Avvio automatico: ",
"tray": "Avvio automatico nella barra delle applicazioni: ",
"tray_notifications": "Notifiche: ",
"dark_mode": "Modalità scura: ",
"priority_only": "Solo priorità: ",
"proxy": "Proxy (richiede il riavvio):"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Autostart: ",
"tray": "Automatisch starten in het taakbalk: ",
"priority_only": "Alleen prioriteit: ",
"dark_mode": "Donkere modus: ",
"proxy": "Proxy (Vereist opnieuw opstarten):"
},
"game_name": "Spel",
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Autostart: ",
"tray": "Autostart z zasobnika: ",
"tray_notifications": "Powiadomienia z zasobnika: ",
"dark_mode": "Tryb ciemny: ",
"priority_mode": "Tryb priorytetu: ",
"proxy": "Proxy (wymaga restartu):"
},
+2
View File
@@ -133,6 +133,8 @@
"name": "Geral",
"autostart": "Auto-início: ",
"tray": "Auto-iniciar na área de notificação: ",
"tray_notifications": "Notificações: ",
"dark_mode": "Modo escuro: ",
"priority_only": "Apenas com prioridade: ",
"proxy": "Proxy (requer o reinício do app):"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Pornire automată :",
"tray": "Pornire automată în bara de activități :",
"tray_notifications": "Notificări în bara de activități :",
"dark_mode": "Mod întunecat :",
"priority_only": "Doar cu prioritate :",
"proxy": "Proxy (necesită repornire) :"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Otomatik başlatma: ",
"tray": "Sistem tepsisine otomatik başlatma: ",
"priority_only": "Yalnızca öncelik: ",
"dark_mode": "Karanlık mod: ",
"proxy": "Proxy (Yeniden başlatma gerektirir):"
},
"game_name": "Oyun",
+1
View File
@@ -127,6 +127,7 @@
"autostart": "Automatické spuštění: ",
"tray": "Automaticky spusti minimalizovaně: ",
"priority_only": "Pouze prioritní: ",
"dark_mode": "Tmavý režim: ",
"proxy": "Proxy:"
},
"game_name": "Název Hry",
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Автозапуск: ",
"tray": "Автозапуск свёрнутым: ",
"tray_notifications": "Всплывающие уведомления: ",
"dark_mode": "Тёмный режим: ",
"priority_mode": "Режим приоритета: ",
"proxy": "Прокси (Требуется перезапуск):"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "Автозапуск: ",
"tray": "Автозапуск у треї: ",
"tray_notifications": "Сповіщення: ",
"dark_mode": "Темний режим: ",
"priority_mode": "Режим пріоритету: ",
"proxy": "Проксі (потребує перезапуску):"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "تشغيل تلقائي ",
"tray": "تشغيل تلقائي في الدرج ",
"tray_notifications": "إشعارات الدرج ",
"dark_mode": "الوضع الداكن: ",
"priority_only": "الأولوية فقط ",
"proxy": "بروكسي (يتطلب إعادة التشغيل) "
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "自動起動:",
"tray": "トレイに自動起動:",
"tray_notifications": "トレイ通知:",
"dark_mode": "ダークモード:",
"priority_only": "優先のみ:",
"proxy": "プロキシ(再起動が必要):"
},
+1
View File
@@ -134,6 +134,7 @@
"autostart": "开机自启: ",
"tray": "开机自启并最小化: ",
"tray_notifications": "启用托盘通知: ",
"dark_mode": "深色模式: ",
"priority_mode": "掉宝模式: ",
"proxy": "代理:"
},
+2 -1
View File
@@ -134,6 +134,7 @@
"autostart": "開機時自動啟動:",
"tray": "開機時以最小化自動啟動:",
"tray_notifications": "啟用系統列通知:",
"dark_mode": "深色模式:",
"priority_mode": "掉寶模式:",
"proxy": "代理伺服器(需要重新啟動):"
},
@@ -158,6 +159,6 @@
"how_it_works_text": "每隔一段時間,應用程式就會向 Twitch 請求當前觀看頻道的原始串流數據的 URL。然後提取此串流的元數據——這足以推進掉寶進度。請注意,這完全繞過了下載實際的串流視頻和音頻的需求。為了使頻道的狀態(上線或離線)的即時更新,應用程式建立了一個 WebSocket 連接,該連接會接收有關串流上下線的事件或當前觀眾數量的更新。",
"getting_started": "事前準備",
"getting_started_text": "1. 透過應用程式登入。\n2. 確保您的 Twitch 帳號已經和您所有感興趣的遊戲帳號已經連接。\n 3. 如果您想要獲取所有感興趣的掉寶,請不要勾選「僅參與優先掉寶遊戲」,然後按「重新載入」。\n4. 如果您想優先獲取特定遊戲,請使用「優先掉寶遊戲」列表設置您想獲取的遊戲的優先順序。應用程式將嘗試先獲取列表頂端的遊戲,然後依序獲取之後的遊戲。\n5. 勾選「僅參與優先掉寶遊戲」以避免獲取不在優先列表中的遊戲。當然,你也可以選擇不勾選——這取決於您。\n6. 使用「排除掉寶遊戲」列表設置永不獲取的遊戲。\n7. 更改任一列表的內容,或更改「僅參與優先掉寶遊戲」勾選狀態,需要手動按「重新載入」才能使更改生效。"
}
}
}
}
+3
View File
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
class SettingsFile(TypedDict):
proxy: URL
language: str
dark_mode: bool
exclude: set[str]
priority: list[str]
autostart_tray: bool
@@ -26,6 +27,7 @@ default_settings: SettingsFile = {
"proxy": URL(),
"priority": [],
"exclude": set(),
"dark_mode": False,
"autostart_tray": False,
"connection_quality": 1,
"language": DEFAULT_LANG,
@@ -46,6 +48,7 @@ class Settings:
# from settings file
proxy: URL
language: str
dark_mode: bool
exclude: set[str]
priority: list[str]
autostart_tray: bool
+2
View File
@@ -161,6 +161,7 @@ class GUISettingsGeneral(TypedDict):
autostart: str
tray: str
tray_notifications: str
dark_mode: str
priority_mode: str
proxy: str
@@ -361,6 +362,7 @@ default_translation: Translation = {
"autostart": "Autostart: ",
"tray": "Autostart into tray: ",
"tray_notifications": "Tray notifications: ",
"dark_mode": "Dark mode: ",
"priority_mode": "Priority mode: ",
"proxy": "Proxy (requires restart):",
},