Added macOS support (#968)

Co-authored-by: DevilXD <4180725+DevilXD@users.noreply.github.com>
This commit is contained in:
CrocchettoandDevilXD authored and GitHub committed 2026-03-07 09:49:58 +01:00
1 parent b685b445e9
commit 1166170fae
6 files changed
+143 -25

No files matched your search

+53
View File
@@ -106,6 +106,58 @@ jobs:
name: Twitch.Drops.Miner.Windows
path: Twitch.Drops.Miner.Windows.zip
macos:
name: macOS
runs-on: macos-latest
needs:
- validate
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up variables
id: vars
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: Append git revision to project version
run: |
sed -i '' -E "s/^(__version__\s*=\s*\"[^\"]+)/\1.${{steps.vars.outputs.sha_short}}/" version.py
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{env.PYTHON_VERSION}}
- name: Install project dependencies
run: |
python3 -m pip install wheel
python3 -m pip install -r requirements.txt
- name: Install PyInstaller
run: |
python3 -m pip install pyinstaller
- name: Create portable executable
run: |
pyinstaller build.spec --noconfirm
- name: Create release folder and Zip
run: |
FOLDER="Twitch Drops Miner"
mkdir -p "$FOLDER"
cp -r "dist/Twitch Drops Miner (by DevilXD).app" "$FOLDER/"
cp manual.txt "$FOLDER/"
zip -r Twitch.Drops.Miner.MacOS.zip "$FOLDER"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
if-no-files-found: error
name: Twitch.Drops.Miner.MacOS
path: Twitch.Drops.Miner.MacOS.zip
linux-pyinstaller:
name: Linux (PyInstaller) (${{matrix.arch}})
strategy:
@@ -256,6 +308,7 @@ jobs:
- windows
- linux-pyinstaller
- linux-appimage
- macos
runs-on: ubuntu-latest
permissions:
contents: write
+2
View File
@@ -23,3 +23,5 @@ settings.json
/appimage-build
/*.AppImage
/*.AppImage.zsync
.DS_Store
*.app
+8
View File
@@ -65,6 +65,13 @@ Every several seconds, the application pretends to watch a particular stream by
- The size of the Linux app is significantly larger than the Windows app due to the inclusion of the `gtk3` library (and its dependencies), which is required for proper system tray/notifications support.
- As an alternative to the native Linux app, you can run the Windows app via [Wine](https://www.winehq.org/) instead. It works really well!
### Notes about the macOS build:
- The macOS version is packaged using PyInstaller into a standalone `.app` bundle, distributed as a ZIP archive.
- Since this application is not signed with a paid Apple Developer Certificate, **macOS Gatekeeper will block it** on the first run (saying it "The application is damaged and can't be opened").
- **To fix this**: Either open the Terminal in the folder the app is in (or navigating with `cd path/to/folder`) and enter `xattr -cr Twitch Drops Miner (by DevilXD).app` or just type `xattr -cr ` (make sure to put a space at the end), drag and drop the `Twitch Drops Miner (by DevilXD).app` file into the terminal window (this will auto-fill the path) and enter
- Persistent files (like `cookies.jar`, `settings.json`, `lock.file` and the `cache` folder) are stored inside the application bundle in `Twitch Drops Miner (by DevilXD).app/Contents/MacOS` (to access them Right-click the application and select `Show Package Contents`)
### Advanced Usage:
If you'd be interested in running the latest master from source or building your own executable, see the wiki page explaining how to do so: https://github.com/DevilXD/TwitchDropsMiner/wiki/Setting-up-the-environment,-building-and-running
@@ -135,6 +142,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.
@crocchetto - For the macOS port.
@Bamboozul - For the entirety of the Arabic (العربية) translation.
@Suz1e - For the entirety of the Chinese (简体中文) translation and revisions.
Executable → Regular
+11 -1
View File
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
from PyInstaller.building.splash import Splash
from PyInstaller.building.build_main import Analysis
from PyInstaller.building.datastruct import _TOCTuple
from PyInstaller.building.api import PYZ, EXE, COLLECT
from PyInstaller.building.api import PYZ, EXE, COLLECT, BUNDLE
PYZTypeCOLLECT: TypeAlias = "abc.Iterable[_TOCTuple] | PYZ"
@@ -142,3 +142,13 @@ if one_dir:
upx=upx,
name=app_name,
)
# macOS bundle support
if sys.platform == "darwin":
source = coll if one_dir else exe
app = BUNDLE(
source,
name=f'{app_name}.app',
icon="icons/pickaxe.ico",
bundle_identifier='com.twitchdrops.miner',
)
+68 -23
View File
@@ -3,9 +3,11 @@ from __future__ import annotations
import os
import re
import sys
import shlex
import ctypes
import asyncio
import logging
import plistlib
import tkinter as tk
from pathlib import Path
from collections import abc
@@ -28,6 +30,9 @@ if sys.platform == "win32":
import win32con
import win32gui
if sys.platform == "darwin":
import AppKit
from translate import _
from cache import ImageCache
from exceptions import MinerException, ExitRequest
@@ -1087,7 +1092,10 @@ class TrayIcon:
}
self._icon_state: str = "pickaxe"
self._button = ttk.Button(master, command=self.minimize, text=_("gui", "tray", "minimize"))
self._button.grid(column=0, row=0, sticky="ne")
# Hides Tray button for macOS
if sys.platform != "darwin":
self._button.grid(column=0, row=0, sticky="ne")
def __del__(self) -> None:
self.stop()
@@ -1153,6 +1161,8 @@ class TrayIcon:
self._manager.close()
def minimize(self):
if sys.platform == "darwin":
return
if self.icon is None:
self._start()
else:
@@ -1635,22 +1645,23 @@ class SettingsPanel:
ttk.Checkbutton(
checkboxes_frame, variable=self._vars["autostart"], command=self.update_autostart
).grid(column=1, row=irow, sticky="w")
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "tray")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
checkboxes_frame, variable=self._vars["tray"], command=self.update_autostart
).grid(column=1, row=irow, sticky="w")
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "tray_notifications")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
checkboxes_frame,
variable=self._vars["tray_notifications"],
command=lambda: setattr(
self._settings, "tray_notifications", bool(self._vars["tray_notifications"].get())
),
).grid(column=1, row=irow, sticky="w")
if sys.platform != "darwin":
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "tray")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
checkboxes_frame, variable=self._vars["tray"], command=self.update_autostart
).grid(column=1, row=irow, sticky="w")
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "tray_notifications")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
checkboxes_frame,
variable=self._vars["tray_notifications"],
command=lambda: setattr(
self._settings, "tray_notifications", bool(self._vars["tray_notifications"].get())
),
).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")
@@ -1757,7 +1768,7 @@ class SettingsPanel:
ttk.Button( # Move to top
priority_frame,
width=2,
text="⭱",
text="⇈",
style="Arrow.TButton",
command=partial(self.priority_move, MAX_INT),
).grid(column=1, row=1, sticky="nsew")
@@ -1765,7 +1776,7 @@ class SettingsPanel:
ttk.Button( # Move up
priority_frame,
width=2,
text="🠙",
text="↑",
style="Arrow.TButton",
command=partial(self.priority_move, 1),
).grid(column=1, row=2, sticky="nsew")
@@ -1773,7 +1784,7 @@ class SettingsPanel:
ttk.Button( # Move down
priority_frame,
width=2,
text="🠛",
text="↓",
style="Arrow.TButton",
command=partial(self.priority_move, -1),
).grid(column=1, row=3, sticky="nsew")
@@ -1781,7 +1792,7 @@ class SettingsPanel:
ttk.Button( # Move to bottom
priority_frame,
width=2,
text="⭳",
text="⇊",
style="Arrow.TButton",
command=partial(self.priority_move, -MAX_INT),
).grid(column=1, row=4, sticky="nsew")
@@ -1867,6 +1878,9 @@ class SettingsPanel:
autostart_folder = config_autostart
return autostart_folder / f"{self.AUTOSTART_NAME}.desktop"
def _get_mac_autostart_filepath(self) -> Path:
return Path(Path.home(), f"Library/LaunchAgents/com.devilxd.{self.AUTOSTART_NAME.lower()}.plist")
def _query_autostart(self) -> bool:
if sys.platform == "win32":
with RegistryKey(self.AUTOSTART_KEY, read_only=True) as key:
@@ -1886,6 +1900,12 @@ class SettingsPanel:
with autostart_file.open('r', encoding="utf8") as file:
# TODO: Consider deleting the old file to avoid autostart errors
return self._get_self_path() in file.read()
elif sys.platform == "darwin":
plist_file = self._get_mac_autostart_filepath()
if not plist_file.exists():
return False
with plist_file.open('r', encoding="utf8") as file:
return str(SELF_PATH.resolve()) in file.read()
def update_autostart(self) -> None:
enabled = bool(self._vars["autostart"].get())
@@ -1917,6 +1937,21 @@ class SettingsPanel:
file.write(file_contents)
else:
autostart_file.unlink(missing_ok=True)
elif sys.platform == "darwin":
plist_file = self._get_mac_autostart_filepath()
if enabled:
command_parts = shlex.split(self._get_autostart_path())
plist_data = {
"Label": f"com.devilxd.{self.AUTOSTART_NAME.lower()}",
"ProgramArguments": command_parts,
"RunAtLoad": True,
}
plist_file.parent.mkdir(parents=True, exist_ok=True)
with plist_file.open("wb") as file:
plistlib.dump(plist_data, file)
else:
plist_file.unlink(missing_ok=True)
def update_excluded_choices(self) -> None:
self._exclude_entry.config(
@@ -2250,7 +2285,7 @@ class GUIManager:
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:
if self._twitch.settings.tray and sys.platform != "darwin":
# NOTE: this starts the tray icon thread
self._root.after_idle(self.tray.minimize)
else:
@@ -2422,7 +2457,7 @@ class GUIManager:
# Palette
if dark:
# Switch to a configurable ttk theme for better color control
if self._style.theme_use() != "clam":
if sys.platform != "darwin" and self._style.theme_use() != "clam":
self._style.theme_use("clam")
bg = "#1e1e1e"
fg = "#e6e6e6"
@@ -2452,6 +2487,16 @@ class GUIManager:
muted = "#404040"
accent = "#0a84ff"
# Setting theme for macOS
if sys.platform == "darwin":
app = AppKit.NSApplication.sharedApplication()
if dark:
appearance = AppKit.NSAppearance.appearanceNamed_(AppKit.NSAppearanceNameDarkAqua)
else:
appearance = AppKit.NSAppearance.appearanceNamed_(AppKit.NSAppearanceNameAqua)
app.setAppearance_(appearance)
s = self._style
# Fonts
default_font = nametofont("TkDefaultFont")
+1 -1
View File
@@ -82,7 +82,7 @@ def lock_file(path: Path) -> tuple[bool, io.TextIOWrapper]:
except Exception:
return False, file
return True, file
if sys.platform == "linux":
if sys.platform in ("linux", "darwin"):
import fcntl
try:
fcntl.lockf(file, fcntl.LOCK_EX | fcntl.LOCK_NB)