Added macOS support (#968)
Co-authored-by: DevilXD <4180725+DevilXD@users.noreply.github.com>
This commit is contained in:
1 parent
b685b445e9
commit
1166170fae
6 files changed
+143
-25
No files matched your search
@@ -106,6 +106,58 @@ jobs:
|
|||||||
name: Twitch.Drops.Miner.Windows
|
name: Twitch.Drops.Miner.Windows
|
||||||
path: Twitch.Drops.Miner.Windows.zip
|
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:
|
linux-pyinstaller:
|
||||||
name: Linux (PyInstaller) (${{matrix.arch}})
|
name: Linux (PyInstaller) (${{matrix.arch}})
|
||||||
strategy:
|
strategy:
|
||||||
@@ -256,6 +308,7 @@ jobs:
|
|||||||
- windows
|
- windows
|
||||||
- linux-pyinstaller
|
- linux-pyinstaller
|
||||||
- linux-appimage
|
- linux-appimage
|
||||||
|
- macos
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -23,3 +23,5 @@ settings.json
|
|||||||
/appimage-build
|
/appimage-build
|
||||||
/*.AppImage
|
/*.AppImage
|
||||||
/*.AppImage.zsync
|
/*.AppImage.zsync
|
||||||
|
.DS_Store
|
||||||
|
*.app
|
||||||
@@ -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.
|
- 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!
|
- 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:
|
### 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
|
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.
|
@guihkx - For the CI script, CI maintenance, and everything related to Linux builds.
|
||||||
@kWAYTV - For the implementation of the dark mode theme.
|
@kWAYTV - For the implementation of the dark mode theme.
|
||||||
|
@crocchetto - For the macOS port.
|
||||||
|
|
||||||
@Bamboozul - For the entirety of the Arabic (العربية) translation.
|
@Bamboozul - For the entirety of the Arabic (العربية) translation.
|
||||||
@Suz1e - For the entirety of the Chinese (简体中文) translation and revisions.
|
@Suz1e - For the entirety of the Chinese (简体中文) translation and revisions.
|
||||||
|
|||||||
Executable → Regular
+11
-1
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
|||||||
from PyInstaller.building.splash import Splash
|
from PyInstaller.building.splash import Splash
|
||||||
from PyInstaller.building.build_main import Analysis
|
from PyInstaller.building.build_main import Analysis
|
||||||
from PyInstaller.building.datastruct import _TOCTuple
|
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"
|
PYZTypeCOLLECT: TypeAlias = "abc.Iterable[_TOCTuple] | PYZ"
|
||||||
@@ -142,3 +142,13 @@ if one_dir:
|
|||||||
upx=upx,
|
upx=upx,
|
||||||
name=app_name,
|
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',
|
||||||
|
)
|
||||||
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import shlex
|
||||||
import ctypes
|
import ctypes
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import plistlib
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from collections import abc
|
from collections import abc
|
||||||
@@ -28,6 +30,9 @@ if sys.platform == "win32":
|
|||||||
import win32con
|
import win32con
|
||||||
import win32gui
|
import win32gui
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
import AppKit
|
||||||
|
|
||||||
from translate import _
|
from translate import _
|
||||||
from cache import ImageCache
|
from cache import ImageCache
|
||||||
from exceptions import MinerException, ExitRequest
|
from exceptions import MinerException, ExitRequest
|
||||||
@@ -1087,7 +1092,10 @@ class TrayIcon:
|
|||||||
}
|
}
|
||||||
self._icon_state: str = "pickaxe"
|
self._icon_state: str = "pickaxe"
|
||||||
self._button = ttk.Button(master, command=self.minimize, text=_("gui", "tray", "minimize"))
|
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:
|
def __del__(self) -> None:
|
||||||
self.stop()
|
self.stop()
|
||||||
@@ -1153,6 +1161,8 @@ class TrayIcon:
|
|||||||
self._manager.close()
|
self._manager.close()
|
||||||
|
|
||||||
def minimize(self):
|
def minimize(self):
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return
|
||||||
if self.icon is None:
|
if self.icon is None:
|
||||||
self._start()
|
self._start()
|
||||||
else:
|
else:
|
||||||
@@ -1635,22 +1645,23 @@ class SettingsPanel:
|
|||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
checkboxes_frame, variable=self._vars["autostart"], command=self.update_autostart
|
checkboxes_frame, variable=self._vars["autostart"], command=self.update_autostart
|
||||||
).grid(column=1, row=irow, sticky="w")
|
).grid(column=1, row=irow, sticky="w")
|
||||||
ttk.Label(
|
if sys.platform != "darwin":
|
||||||
checkboxes_frame, text=_("gui", "settings", "general", "tray")
|
ttk.Label(
|
||||||
).grid(column=0, row=(irow := irow + 1), sticky="e")
|
checkboxes_frame, text=_("gui", "settings", "general", "tray")
|
||||||
ttk.Checkbutton(
|
).grid(column=0, row=(irow := irow + 1), sticky="e")
|
||||||
checkboxes_frame, variable=self._vars["tray"], command=self.update_autostart
|
ttk.Checkbutton(
|
||||||
).grid(column=1, row=irow, sticky="w")
|
checkboxes_frame, variable=self._vars["tray"], command=self.update_autostart
|
||||||
ttk.Label(
|
).grid(column=1, row=irow, sticky="w")
|
||||||
checkboxes_frame, text=_("gui", "settings", "general", "tray_notifications")
|
ttk.Label(
|
||||||
).grid(column=0, row=(irow := irow + 1), sticky="e")
|
checkboxes_frame, text=_("gui", "settings", "general", "tray_notifications")
|
||||||
ttk.Checkbutton(
|
).grid(column=0, row=(irow := irow + 1), sticky="e")
|
||||||
checkboxes_frame,
|
ttk.Checkbutton(
|
||||||
variable=self._vars["tray_notifications"],
|
checkboxes_frame,
|
||||||
command=lambda: setattr(
|
variable=self._vars["tray_notifications"],
|
||||||
self._settings, "tray_notifications", bool(self._vars["tray_notifications"].get())
|
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")
|
||||||
).grid(column=0, row=(irow := irow + 1), sticky="e")
|
).grid(column=0, row=(irow := irow + 1), sticky="e")
|
||||||
@@ -1757,7 +1768,7 @@ class SettingsPanel:
|
|||||||
ttk.Button( # Move to top
|
ttk.Button( # Move to top
|
||||||
priority_frame,
|
priority_frame,
|
||||||
width=2,
|
width=2,
|
||||||
text="⭱",
|
text="⇈",
|
||||||
style="Arrow.TButton",
|
style="Arrow.TButton",
|
||||||
command=partial(self.priority_move, MAX_INT),
|
command=partial(self.priority_move, MAX_INT),
|
||||||
).grid(column=1, row=1, sticky="nsew")
|
).grid(column=1, row=1, sticky="nsew")
|
||||||
@@ -1765,7 +1776,7 @@ class SettingsPanel:
|
|||||||
ttk.Button( # Move up
|
ttk.Button( # Move up
|
||||||
priority_frame,
|
priority_frame,
|
||||||
width=2,
|
width=2,
|
||||||
text="🠙",
|
text="↑",
|
||||||
style="Arrow.TButton",
|
style="Arrow.TButton",
|
||||||
command=partial(self.priority_move, 1),
|
command=partial(self.priority_move, 1),
|
||||||
).grid(column=1, row=2, sticky="nsew")
|
).grid(column=1, row=2, sticky="nsew")
|
||||||
@@ -1773,7 +1784,7 @@ class SettingsPanel:
|
|||||||
ttk.Button( # Move down
|
ttk.Button( # Move down
|
||||||
priority_frame,
|
priority_frame,
|
||||||
width=2,
|
width=2,
|
||||||
text="🠛",
|
text="↓",
|
||||||
style="Arrow.TButton",
|
style="Arrow.TButton",
|
||||||
command=partial(self.priority_move, -1),
|
command=partial(self.priority_move, -1),
|
||||||
).grid(column=1, row=3, sticky="nsew")
|
).grid(column=1, row=3, sticky="nsew")
|
||||||
@@ -1781,7 +1792,7 @@ class SettingsPanel:
|
|||||||
ttk.Button( # Move to bottom
|
ttk.Button( # Move to bottom
|
||||||
priority_frame,
|
priority_frame,
|
||||||
width=2,
|
width=2,
|
||||||
text="⭳",
|
text="⇊",
|
||||||
style="Arrow.TButton",
|
style="Arrow.TButton",
|
||||||
command=partial(self.priority_move, -MAX_INT),
|
command=partial(self.priority_move, -MAX_INT),
|
||||||
).grid(column=1, row=4, sticky="nsew")
|
).grid(column=1, row=4, sticky="nsew")
|
||||||
@@ -1867,6 +1878,9 @@ class SettingsPanel:
|
|||||||
autostart_folder = config_autostart
|
autostart_folder = config_autostart
|
||||||
return autostart_folder / f"{self.AUTOSTART_NAME}.desktop"
|
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:
|
def _query_autostart(self) -> bool:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
with RegistryKey(self.AUTOSTART_KEY, read_only=True) as key:
|
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:
|
with autostart_file.open('r', encoding="utf8") as file:
|
||||||
# TODO: Consider deleting the old file to avoid autostart errors
|
# TODO: Consider deleting the old file to avoid autostart errors
|
||||||
return self._get_self_path() in file.read()
|
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:
|
def update_autostart(self) -> None:
|
||||||
enabled = bool(self._vars["autostart"].get())
|
enabled = bool(self._vars["autostart"].get())
|
||||||
@@ -1917,6 +1937,21 @@ class SettingsPanel:
|
|||||||
file.write(file_contents)
|
file.write(file_contents)
|
||||||
else:
|
else:
|
||||||
autostart_file.unlink(missing_ok=True)
|
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:
|
def update_excluded_choices(self) -> None:
|
||||||
self._exclude_entry.config(
|
self._exclude_entry.config(
|
||||||
@@ -2250,7 +2285,7 @@ class GUIManager:
|
|||||||
self._orig_theme_name = ''
|
self._orig_theme_name = ''
|
||||||
self.apply_theme(self._twitch.settings.dark_mode)
|
self.apply_theme(self._twitch.settings.dark_mode)
|
||||||
# stay hidden in tray if needed, otherwise show the window when everything's ready
|
# 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
|
# NOTE: this starts the tray icon thread
|
||||||
self._root.after_idle(self.tray.minimize)
|
self._root.after_idle(self.tray.minimize)
|
||||||
else:
|
else:
|
||||||
@@ -2422,7 +2457,7 @@ class GUIManager:
|
|||||||
# Palette
|
# Palette
|
||||||
if dark:
|
if dark:
|
||||||
# Switch to a configurable ttk theme for better color control
|
# 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")
|
self._style.theme_use("clam")
|
||||||
bg = "#1e1e1e"
|
bg = "#1e1e1e"
|
||||||
fg = "#e6e6e6"
|
fg = "#e6e6e6"
|
||||||
@@ -2452,6 +2487,16 @@ class GUIManager:
|
|||||||
muted = "#404040"
|
muted = "#404040"
|
||||||
accent = "#0a84ff"
|
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
|
s = self._style
|
||||||
# Fonts
|
# Fonts
|
||||||
default_font = nametofont("TkDefaultFont")
|
default_font = nametofont("TkDefaultFont")
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ def lock_file(path: Path) -> tuple[bool, io.TextIOWrapper]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False, file
|
return False, file
|
||||||
return True, file
|
return True, file
|
||||||
if sys.platform == "linux":
|
if sys.platform in ("linux", "darwin"):
|
||||||
import fcntl
|
import fcntl
|
||||||
try:
|
try:
|
||||||
fcntl.lockf(file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.lockf(file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
|||||||
Reference in new issue
Block a user