Add the advanced settings section;

Implement the checkboxes for badge and emote mining toggle, and the extra available drops check toggle
This commit is contained in:
DevilXD committed 2025-11-16 10:08:47 +01:00
1 parent 165f9eccb3
commit 5f54c26f4a
6 files changed
+112 -44

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@ class Stream:
self.channel: Channel = channel self.channel: Channel = channel
self.broadcast_id = int(id) self.broadcast_id = int(id)
self.viewers: int = viewers self.viewers: int = viewers
self.drops_enabled: bool = True self.drops_enabled: bool = not channel._twitch.settings.available_drops_check
self.game: Game | None = Game(game) if game else None self.game: Game | None = Game(game) if game else None
self.title: str = title self.title: str = title
self._stream_url: URLType | None = None self._stream_url: URLType | None = None
+63 -15
View File
@@ -1553,6 +1553,8 @@ class _SettingsVars(TypedDict):
language: StringVar language: StringVar
priority_mode: StringVar priority_mode: StringVar
tray_notifications: IntVar tray_notifications: IntVar
enable_badges_emotes: IntVar
available_drops_check: IntVar
class SettingsPanel: class SettingsPanel:
@@ -1586,6 +1588,12 @@ class SettingsPanel:
"dark_mode": IntVar(master, int(self._settings.dark_mode)), "dark_mode": IntVar(master, int(self._settings.dark_mode)),
"priority_mode": StringVar(master, self.PRIORITY_MODES[priority_mode]), "priority_mode": StringVar(master, self.PRIORITY_MODES[priority_mode]),
"tray_notifications": IntVar(master, self._settings.tray_notifications), "tray_notifications": IntVar(master, self._settings.tray_notifications),
"enable_badges_emotes": IntVar(
master, int(self._settings.enable_badges_emotes)
),
"available_drops_check": IntVar(
master, int(self._settings.available_drops_check)
),
} }
self._game_names: set[str] = set() self._game_names: set[str] = set()
master.rowconfigure(0, weight=1) master.rowconfigure(0, weight=1)
@@ -1593,6 +1601,7 @@ class SettingsPanel:
# use a frame to center the content within the tab # use a frame to center the content within the tab
center_frame = ttk.Frame(master) center_frame = ttk.Frame(master)
center_frame.grid(column=0, row=0) center_frame.grid(column=0, row=0)
# General section # General section
general_frame = ttk.LabelFrame( general_frame = ttk.LabelFrame(
center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "general", "name") center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "general", "name")
@@ -1602,11 +1611,11 @@ class SettingsPanel:
# NOTE: this can be adjusted or removed later on if more options were to be added # NOTE: this can be adjusted or removed later on if more options were to be added
general_frame.rowconfigure(0, weight=1) general_frame.rowconfigure(0, weight=1)
general_frame.columnconfigure(0, weight=1) general_frame.columnconfigure(0, weight=1)
center_frame2 = ttk.Frame(general_frame) general_center = ttk.Frame(general_frame)
center_frame2.grid(column=0, row=0) general_center.grid(column=0, row=0)
# language frame # language frame
language_frame = ttk.Frame(center_frame2) language_frame = ttk.Frame(general_center)
language_frame.grid(column=0, row=0) language_frame.grid(column=0, row=0)
ttk.Label(language_frame, text="Language 🌐 (requires restart): ").grid(column=0, row=0) ttk.Label(language_frame, text="Language 🌐 (requires restart): ").grid(column=0, row=0)
SelectCombobox( SelectCombobox(
@@ -1617,7 +1626,7 @@ class SettingsPanel:
).grid(column=1, row=0) ).grid(column=1, row=0)
# checkboxes frame # checkboxes frame
checkboxes_frame = ttk.Frame(center_frame2) checkboxes_frame = ttk.Frame(general_center)
checkboxes_frame.grid(column=0, row=1) checkboxes_frame.grid(column=0, row=1)
ttk.Label( ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "autostart") checkboxes_frame, text=_("gui", "settings", "general", "autostart")
@@ -1637,7 +1646,9 @@ class SettingsPanel:
ttk.Checkbutton( ttk.Checkbutton(
checkboxes_frame, checkboxes_frame,
variable=self._vars["tray_notifications"], variable=self._vars["tray_notifications"],
command=self.update_notifications, command=lambda: setattr(
self._settings, "tray_notifications", bool(self._vars["tray_notifications"].get())
),
).grid(column=1, row=irow, sticky="w") ).grid(column=1, row=irow, sticky="w")
ttk.Label( ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "dark_mode") checkboxes_frame, text=_("gui", "settings", "general", "dark_mode")
@@ -1658,7 +1669,7 @@ class SettingsPanel:
).grid(column=1, row=irow, sticky="w") ).grid(column=1, row=irow, sticky="w")
# proxy frame # proxy frame
proxy_frame = ttk.Frame(center_frame2) proxy_frame = ttk.Frame(general_center)
proxy_frame.grid(column=0, row=2) proxy_frame.grid(column=0, row=2)
ttk.Label(proxy_frame, text=_("gui", "settings", "general", "proxy")).grid(column=0, row=0) ttk.Label(proxy_frame, text=_("gui", "settings", "general", "proxy")).grid(column=0, row=0)
self._proxy = PlaceholderEntry( self._proxy = PlaceholderEntry(
@@ -1671,11 +1682,48 @@ class SettingsPanel:
) )
self._proxy.config(validatecommand=partial(proxy_validate, self._proxy, self._settings)) self._proxy.config(validatecommand=partial(proxy_validate, self._proxy, self._settings))
self._proxy.grid(column=0, row=1) self._proxy.grid(column=0, row=1)
# Advanced section
advanced_frame = ttk.LabelFrame(
center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "advanced", "name")
)
advanced_frame.grid(column=0, row=1, sticky="nsew")
advanced_frame.columnconfigure(0, weight=1)
advanced_frame.rowconfigure(0, weight=1)
advanced_center = ttk.Frame(advanced_frame)
advanced_center.grid(column=0, row=0)
# Toggles for badges and emotes, and available drops check
ttk.Label(
advanced_center, text=_("gui", "settings", "advanced", "enable_badges_emotes")
).grid(column=0, row=(irow := 0), sticky="e")
ttk.Checkbutton(
advanced_center,
variable=self._vars["enable_badges_emotes"],
command=lambda: setattr(
self._settings,
"enable_badges_emotes",
bool(self._vars["enable_badges_emotes"].get()),
),
).grid(column=1, row=irow, sticky="w")
ttk.Label(
advanced_center, text=_("gui", "settings", "advanced", "available_drops_check")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
advanced_center,
variable=self._vars["available_drops_check"],
command=lambda: setattr(
self._settings,
"available_drops_check",
bool(self._vars["available_drops_check"].get()),
),
).grid(column=1, row=irow, sticky="w")
# Priority section # Priority section
priority_frame = ttk.LabelFrame( priority_frame = ttk.LabelFrame(
center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "priority") center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "priority")
) )
priority_frame.grid(column=1, row=0, sticky="nsew") priority_frame.grid(column=1, row=0, rowspan=2, sticky="nsew")
self._priority_entry = PlaceholderCombobox( self._priority_entry = PlaceholderCombobox(
priority_frame, placeholder=_("gui", "settings", "game_name"), width=30 priority_frame, placeholder=_("gui", "settings", "game_name"), width=30
) )
@@ -1686,7 +1734,7 @@ class SettingsPanel:
).grid(column=1, row=0) ).grid(column=1, row=0)
self._priority_list = PaddedListbox( self._priority_list = PaddedListbox(
priority_frame, priority_frame,
height=10, height=12,
padding=(1, 0), padding=(1, 0),
activestyle="none", activestyle="none",
selectmode="single", selectmode="single",
@@ -1715,11 +1763,12 @@ class SettingsPanel:
priority_frame, text="❌", command=self.priority_delete, width=3, style="Large.TButton" priority_frame, text="❌", command=self.priority_delete, width=3, style="Large.TButton"
).grid(column=1, row=3, sticky="ns") ).grid(column=1, row=3, sticky="ns")
priority_frame.rowconfigure(3, weight=1) priority_frame.rowconfigure(3, weight=1)
# Exclude section # Exclude section
exclude_frame = ttk.LabelFrame( exclude_frame = ttk.LabelFrame(
center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "exclude") center_frame, padding=(4, 0, 4, 4), text=_("gui", "settings", "exclude")
) )
exclude_frame.grid(column=2, row=0, sticky="nsew") exclude_frame.grid(column=2, row=0, rowspan=2, sticky="nsew")
self._exclude_entry = PlaceholderCombobox( self._exclude_entry = PlaceholderCombobox(
exclude_frame, placeholder=_("gui", "settings", "game_name"), width=26 exclude_frame, placeholder=_("gui", "settings", "game_name"), width=26
) )
@@ -1729,7 +1778,7 @@ class SettingsPanel:
).grid(column=1, row=0) ).grid(column=1, row=0)
self._exclude_list = PaddedListbox( self._exclude_list = PaddedListbox(
exclude_frame, exclude_frame,
height=10, height=12,
padding=(1, 0), padding=(1, 0),
activestyle="none", activestyle="none",
selectmode="single", selectmode="single",
@@ -1743,9 +1792,10 @@ class SettingsPanel:
ttk.Button( ttk.Button(
exclude_frame, text="❌", command=self.exclude_delete, width=3, style="Large.TButton" exclude_frame, text="❌", command=self.exclude_delete, width=3, style="Large.TButton"
).grid(column=0, row=2, columnspan=2, sticky="ew") ).grid(column=0, row=2, columnspan=2, sticky="ew")
# Reload button # Reload button
reload_frame = ttk.Frame(center_frame) reload_frame = ttk.Frame(center_frame)
reload_frame.grid(column=0, row=1, columnspan=3, pady=4) reload_frame.grid(column=0, row=2, columnspan=3, pady=4)
ttk.Label(reload_frame, text=_("gui", "settings", "reload_text")).grid(column=0, row=0) ttk.Label(reload_frame, text=_("gui", "settings", "reload_text")).grid(column=0, row=0)
ttk.Button( ttk.Button(
reload_frame, reload_frame,
@@ -1761,12 +1811,8 @@ class SettingsPanel:
def update_dark_mode(self) -> None: def update_dark_mode(self) -> None:
self._settings.dark_mode = bool(self._vars["dark_mode"].get()) self._settings.dark_mode = bool(self._vars["dark_mode"].get())
self._settings.alter()
self._manager.apply_theme(self._settings.dark_mode) self._manager.apply_theme(self._settings.dark_mode)
def update_notifications(self) -> None:
self._settings.tray_notifications = bool(self._vars["tray_notifications"].get())
def _get_self_path(self) -> str: def _get_self_path(self) -> str:
# NOTE: we need double quotes in case the path contains spaces # NOTE: we need double quotes in case the path contains spaces
return f'"{SELF_PATH.resolve()!s}"' return f'"{SELF_PATH.resolve()!s}"'
@@ -2629,6 +2675,8 @@ if __name__ == "__main__":
autostart_tray=False, autostart_tray=False,
exclude={"Lit Game"}, exclude={"Lit Game"},
tray_notifications=True, tray_notifications=True,
enable_badges_emotes=False,
available_drops_check=False,
logging_level=LOGGING_LEVELS[0], logging_level=LOGGING_LEVELS[0],
priority_mode=PriorityMode.PRIORITY_ONLY, priority_mode=PriorityMode.PRIORITY_ONLY,
) )
+3 -1
View File
@@ -395,7 +395,9 @@ class DropsCampaign:
@property @property
def eligible(self) -> bool: def eligible(self) -> bool:
return self.linked or self.has_badge_or_emote return self.linked or (
self._twitch.settings.enable_badges_emotes and self.has_badge_or_emote
)
@cached_property @cached_property
def has_badge_or_emote(self) -> bool: def has_badge_or_emote(self) -> bool:
+6
View File
@@ -20,6 +20,8 @@ class SettingsFile(TypedDict):
autostart_tray: bool autostart_tray: bool
connection_quality: int connection_quality: int
tray_notifications: bool tray_notifications: bool
enable_badges_emotes: bool
available_drops_check: bool
priority_mode: PriorityMode priority_mode: PriorityMode
@@ -32,6 +34,8 @@ default_settings: SettingsFile = {
"connection_quality": 1, "connection_quality": 1,
"language": DEFAULT_LANG, "language": DEFAULT_LANG,
"tray_notifications": True, "tray_notifications": True,
"enable_badges_emotes": False,
"available_drops_check": False,
"priority_mode": PriorityMode.PRIORITY_ONLY, "priority_mode": PriorityMode.PRIORITY_ONLY,
} }
@@ -54,6 +58,8 @@ class Settings:
autostart_tray: bool autostart_tray: bool
connection_quality: int connection_quality: int
tray_notifications: bool tray_notifications: bool
enable_badges_emotes: bool
available_drops_check: bool
priority_mode: PriorityMode priority_mode: PriorityMode
PASSTHROUGH = ("_settings", "_args", "_altered") PASSTHROUGH = ("_settings", "_args", "_altered")
+12
View File
@@ -166,6 +166,12 @@ class GUISettingsGeneral(TypedDict):
proxy: str proxy: str
class GUISettingsAdvanced(TypedDict):
name: str
enable_badges_emotes: str
available_drops_check: str
class GUIPriorityModes(TypedDict): class GUIPriorityModes(TypedDict):
priority_only: str priority_only: str
ending_soonest: str ending_soonest: str
@@ -174,6 +180,7 @@ class GUIPriorityModes(TypedDict):
class GUISettings(TypedDict): class GUISettings(TypedDict):
general: GUISettingsGeneral general: GUISettingsGeneral
advanced: GUISettingsAdvanced
priority_modes: GUIPriorityModes priority_modes: GUIPriorityModes
game_name: str game_name: str
priority: str priority: str
@@ -366,6 +373,11 @@ default_translation: Translation = {
"priority_mode": "Priority mode: ", "priority_mode": "Priority mode: ",
"proxy": "Proxy (requires restart):", "proxy": "Proxy (requires restart):",
}, },
"advanced": {
"name": "Advanced",
"enable_badges_emotes": "Enable partial support for badges and emotes: ",
"available_drops_check": "Enable extra available drops check: ",
},
"priority_modes": { "priority_modes": {
"priority_only": "Priority list only", "priority_only": "Priority list only",
"ending_soonest": "Ending soonest", "ending_soonest": "Ending soonest",
+27 -27
View File
@@ -1562,7 +1562,7 @@ class Twitch:
async def bulk_check_online(self, channels: abc.Iterable[Channel]): async def bulk_check_online(self, channels: abc.Iterable[Channel]):
""" """
Utilize batch GQL requests to check ONLINE status for a lot of channels at once. Utilize batch GQL requests to check ONLINE status for a lot of channels at once.
Also handles the drops_enabled check. Also handles the drops_enabled check (if enabled).
""" """
acl_streams_map: dict[int, JsonType] = {} acl_streams_map: dict[int, JsonType] = {}
stream_gql_ops: list[GQLOperation] = [channel.stream_gql for channel in channels] stream_gql_ops: list[GQLOperation] = [channel.stream_gql for channel in channels]
@@ -1587,29 +1587,30 @@ class Twitch:
task.cancel() task.cancel()
raise raise
# for all channels with an active stream, check the available drops as well # for all channels with an active stream, check the available drops as well
# acl_available_drops_map: dict[int, list[JsonType]] = {} acl_available_drops_map: dict[int, list[JsonType]] = {}
# available_gql_ops: list[GQLOperation] = [ if self.settings.available_drops_check:
# GQL_OPERATIONS["AvailableDrops"].with_variables({"channelID": str(channel_id)}) available_gql_ops: list[GQLOperation] = [
# for channel_id, channel_data in acl_streams_map.items() GQL_OPERATIONS["AvailableDrops"].with_variables({"channelID": str(channel_id)})
# if channel_data["stream"] is not None # only do this for ONLINE channels for channel_id, channel_data in acl_streams_map.items()
# ] if channel_data["stream"] is not None # only do this for ONLINE channels
# available_gql_tasks: list[asyncio.Task[list[JsonType]]] = [ ]
# asyncio.create_task(self.gql_request(available_gql_chunk)) available_gql_tasks: list[asyncio.Task[list[JsonType]]] = [
# for available_gql_chunk in chunk(available_gql_ops, 20) asyncio.create_task(self.gql_request(available_gql_chunk))
# ] for available_gql_chunk in chunk(available_gql_ops, 20)
# try: ]
# for coro in asyncio.as_completed(available_gql_tasks): try:
# response_list = await coro for coro in asyncio.as_completed(available_gql_tasks):
# for response_json in response_list: response_list = await coro
# available_info: JsonType = response_json["data"]["channel"] for response_json in response_list:
# acl_available_drops_map[int(available_info["id"])] = ( available_info: JsonType = response_json["data"]["channel"]
# available_info["viewerDropCampaigns"] or [] acl_available_drops_map[int(available_info["id"])] = (
# ) available_info["viewerDropCampaigns"] or []
# except Exception: )
# # asyncio.as_completed doesn't cancel tasks on errors except Exception:
# for task in available_gql_tasks: # asyncio.as_completed doesn't cancel tasks on errors
# task.cancel() for task in available_gql_tasks:
# raise task.cancel()
raise
for channel in channels: for channel in channels:
channel_id = channel.id channel_id = channel.id
if channel_id not in acl_streams_map: if channel_id not in acl_streams_map:
@@ -1617,6 +1618,5 @@ class Twitch:
channel_data = acl_streams_map[channel_id] channel_data = acl_streams_map[channel_id]
if channel_data["stream"] is None: if channel_data["stream"] is None:
continue continue
# available_drops: list[JsonType] = acl_available_drops_map[channel_id] available_drops: list[JsonType] = acl_available_drops_map.get(channel_id, [])
# channel.external_update(channel_data, available_drops) channel.external_update(channel_data, available_drops)
channel.external_update(channel_data, [])