Skip to content

Commit 11191fe

Browse files
committed
- When using the Get the Canary miner by @Vagelis1608, display the last updated date for each device.
Make the columns sortable. - Update Github action `action-gh-release` to version 3
1 parent faf68d3 commit 11191fe

9 files changed

Lines changed: 119 additions & 27 deletions

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ jobs:
355355
# Create release
356356
- name: Create release
357357
id: create_release
358-
uses: softprops/action-gh-release@v2
358+
uses: softprops/action-gh-release@v3
359359
with:
360360
token: ${{ secrets.GITHUB_TOKEN }}
361361
tag_name: v${{ steps.get_version.outputs.version }}

build-on-mac-intel-only.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ exe = EXE(pyz,
5959
icon='images/icon-dark-256.icns')
6060
app = BUNDLE(exe,
6161
name='PixelFlasher.app',
62-
version='9.0.1.0',
62+
version='9.0.2.0',
6363
icon='./images/icon-dark-256.icns',
6464
bundle_identifier='com.badabing.pixelflasher')

build-on-mac.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,6 @@ exe = EXE(pyz,
6060
icon='images/icon-dark-256.icns')
6161
app = BUNDLE(exe,
6262
name='PixelFlasher.app',
63-
version='9.0.1.0',
63+
version='9.0.2.0',
6464
icon='./images/icon-dark-256.icns',
6565
bundle_identifier='com.badabing.pixelflasher')

build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
# <https://www.gnu.org/licenses/>.
3333

3434
rm -rf build dist
35-
VERSION=9.0.1.0
35+
VERSION=9.0.2.0
3636
NAME="PixelFlasher"
3737
DIST_NAME="PixelFlasher"
3838

constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
APPNAME = 'PixelFlasher'
3737
CONFIG_FILE_NAME = 'PixelFlasher.json'
38-
VERSION = '9.0.1.0'
38+
VERSION = '9.0.2.0'
3939
SDKVERSION = '33.0.3'
4040
MAIN_WIDTH = 1400
4141
MAIN_HEIGHT = 1040

device_selector.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,35 @@
3434
# <https://www.gnu.org/licenses/>.
3535

3636
import wx
37+
import wx.lib.mixins.listctrl as listmix
38+
import images
3739
from i18n import _
3840

3941

42+
# ============================================================================
43+
# Class ListCtrl
44+
# ============================================================================
45+
class ListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
46+
def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0):
47+
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
48+
listmix.ListCtrlAutoWidthMixin.__init__(self)
49+
50+
4051
# ============================================================================
4152
# Class DeviceSelectorDialog
4253
# ============================================================================
43-
class DeviceSelectorDialog(wx.Dialog):
54+
class DeviceSelectorDialog(wx.Dialog, listmix.ColumnSorterMixin):
4455

4556
# ============================================================================
4657
# Function __init__
4758
# ============================================================================
48-
def __init__(self, parent, devices, title=_("Select Device"), message=_("Select a device:"), select_device=None):
49-
super().__init__(parent, title=title, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
59+
def __init__(self, parent, devices, title=_("Select Device"), message=_("Select a device:"), select_device=None, show_filename=True):
60+
wx.Dialog.__init__(self, parent, title=title, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
5061

5162
self.devices = devices
5263
self.selected_device = None
5364
self._select_device = select_device
65+
self.show_filename = show_filename
5466

5567
self._create_ui(message)
5668
self._bind_events()
@@ -67,18 +79,45 @@ def _create_ui(self, message):
6779
message_label = wx.StaticText(self, label=message)
6880
main_sizer.Add(message_label, 0, wx.ALL | wx.EXPAND, 10)
6981

82+
# Sort arrow images
83+
self.il = wx.ImageList(16, 16)
84+
self.sm_up = self.il.Add(images.SmallUpArrow.GetBitmap())
85+
self.sm_dn = self.il.Add(images.SmallDnArrow.GetBitmap())
86+
7087
# Device list
71-
self.device_list = wx.ListCtrl(self, style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
72-
self.device_list.AppendColumn("Device", width=200)
73-
self.device_list.AppendColumn("Filename", width=500)
88+
self.device_list = ListCtrl(self, -1, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.BORDER_NONE)
89+
self.device_list.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
90+
self.device_list.AppendColumn(_("Device"), width=200)
91+
if self.show_filename:
92+
self.device_list.AppendColumn(_("Filename"), width=400)
93+
94+
has_dates = any('last_updated' in d for d in self.devices)
95+
if has_dates:
96+
self.device_list.AppendColumn(_("Last Updated"), width=110)
7497

7598
# Populate the list
99+
self.itemDataMap = {}
76100
selected_index = None
77101

78102
for i, device in enumerate(self.devices):
79103
index = self.device_list.InsertItem(i, device.get('device', 'Unknown'))
80-
self.device_list.SetItem(index, 1, device.get('zip_filename', ''))
104+
self.device_list.SetItemColumnImage(index, 0, -1) # suppress row icon
105+
col = 1
106+
if self.show_filename:
107+
self.device_list.SetItem(index, col, device.get('zip_filename', ''))
108+
col += 1
109+
if has_dates:
110+
self.device_list.SetItem(index, col, device.get('last_updated', ''))
81111
self.device_list.SetItemData(index, i)
112+
113+
# Build sort map tuple, must match column order
114+
map_vals = [device.get('device', '')]
115+
if self.show_filename:
116+
map_vals.append(device.get('zip_filename', ''))
117+
if has_dates:
118+
map_vals.append(device.get('last_updated', ''))
119+
self.itemDataMap[i] = tuple(map_vals)
120+
82121
if selected_index is None and self._matches_select_device(device):
83122
selected_index = index
84123

@@ -89,6 +128,10 @@ def _create_ui(self, message):
89128
elif self.devices:
90129
self.device_list.Select(0)
91130

131+
# Init column sorter, must be after itemDataMap and device_list are ready
132+
num_cols = 1 + (1 if self.show_filename else 0) + (1 if has_dates else 0)
133+
listmix.ColumnSorterMixin.__init__(self, num_cols)
134+
92135
main_sizer.Add(self.device_list, 1, wx.ALL | wx.EXPAND, 10)
93136

94137
# Buttons
@@ -106,6 +149,20 @@ def _create_ui(self, message):
106149

107150
self.SetSizer(main_sizer)
108151

152+
# ============================================================================
153+
# Function GetListCtrl
154+
# ============================================================================
155+
# Used by ColumnSorterMixin
156+
def GetListCtrl(self):
157+
return self.device_list
158+
159+
# ============================================================================
160+
# Function GetSortImages
161+
# ============================================================================
162+
# Used by ColumnSorterMixin
163+
def GetSortImages(self):
164+
return (self.sm_dn, self.sm_up)
165+
109166
# ============================================================================
110167
# Function _matches_select_device
111168
# ============================================================================
@@ -196,7 +253,7 @@ def get_selected_device(self):
196253
# ============================================================================
197254
# Function show_device_selector
198255
# ============================================================================
199-
def show_device_selector(parent, devices, title=_("Select Device"), message=_("Select a device:"), select_device=None):
256+
def show_device_selector(parent, devices, title=_("Select Device"), message=_("Select a device:"), select_device=None, show_filename=True):
200257
"""
201258
Show device selector dialog and return selected device.
202259
@@ -206,6 +263,7 @@ def show_device_selector(parent, devices, title=_("Select Device"), message=_("S
206263
title: Dialog title
207264
message: Message to display above the list
208265
select_device: Preferred device (dict or string identifier) to pre-select if available
266+
show_filename: Whether to show the Filename column (default True)
209267
210268
Returns:
211269
Selected device dictionary or None if cancelled
@@ -214,7 +272,7 @@ def show_device_selector(parent, devices, title=_("Select Device"), message=_("S
214272
wx.MessageBox(_("No devices available."), _("Error"), wx.OK | wx.ICON_ERROR, parent)
215273
return None
216274

217-
dialog = DeviceSelectorDialog(parent, devices, title, message, select_device=select_device)
275+
dialog = DeviceSelectorDialog(parent, devices, title, message, select_device=select_device, show_filename=show_filename)
218276

219277
try:
220278
if dialog.ShowModal() == wx.ID_OK:

runtime.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import random
5757
import tarfile
5858
import tempfile
59+
import concurrent.futures
5960
import threading
6061
import time
6162
import traceback
@@ -3718,21 +3719,53 @@ def get_canary_miner(device_model='random', default_selection=None, miner_url=No
37183719
page_html = response.text
37193720
file_list = []
37203721
directory = path.basename(urlparse(miner_url).path) or "devices"
3722+
3723+
# Build file list from the embedded JSON (deduplicated, GitHub repeats entries)
37213724
file_pattern = rf'{{"name":"([^"]+)","path":"({re.escape(directory)}/[^"]+)","contentType":"file"}}'
3722-
matches = re.findall(file_pattern, page_html)
3723-
for match in matches:
3725+
seen_paths = set()
3726+
for match in re.findall(file_pattern, page_html):
37243727
file_name = match[0]
37253728
if '.pif.prop' not in file_name:
37263729
continue
3730+
repo_file_path = match[1]
3731+
if repo_file_path in seen_paths:
3732+
continue
3733+
seen_paths.add(repo_file_path)
37273734
file_name = file_name.replace('.pif.prop', '')
3728-
file_path = match[1]
3729-
file_path = f"https://raw.githubusercontent.com/Vagelis1608/get_the_canary_miner/refs/heads/main/{file_path}"
3730-
file_list.append({"device": file_name, "path": file_path})
3735+
file_path = f"https://raw.githubusercontent.com/Vagelis1608/get_the_canary_miner/refs/heads/main/{repo_file_path}"
3736+
file_list.append({"device": file_name, "path": file_path, "repo_path": repo_file_path})
37313737
if device_model != 'random' and device_model != '_select_' and device_model in file_name:
37323738
canary_url = file_path
37333739
canary_device = device_model
37343740

37353741
debug(f"Found {len(file_list)} Canary PIF files")
3742+
3743+
# Fetch per-file last-commit dates via GitHub API
3744+
if device_model == '_select_' and file_list:
3745+
parsed_miner = urlparse(miner_url)
3746+
miner_pp = [p for p in parsed_miner.path.split('/') if p]
3747+
if parsed_miner.netloc in ('github.com', 'www.github.com') and len(miner_pp) >= 2:
3748+
gh_owner, gh_repo = miner_pp[0], miner_pp[1]
3749+
3750+
def _fetch_commit_date(item):
3751+
try:
3752+
r = requests.get(
3753+
f"https://api.github.com/repos/{gh_owner}/{gh_repo}/commits",
3754+
params={"path": item['repo_path'], "per_page": 1},
3755+
timeout=10
3756+
)
3757+
if r.status_code == 200:
3758+
data = r.json()
3759+
if data:
3760+
item['last_updated'] = data[0]['commit']['committer']['date'][:10]
3761+
except Exception:
3762+
pass
3763+
3764+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(file_list)) as executor:
3765+
list(executor.map(_fetch_commit_date, file_list))
3766+
dates_found = sum(1 for item in file_list if 'last_updated' in item)
3767+
debug(f"Fetched commit dates for {dates_found}/{len(file_list)} files")
3768+
37363769
if device_model == 'random':
37373770
selected_file = random.choice(file_list)
37383771
canary_url = selected_file['path']
@@ -3744,7 +3777,7 @@ def get_canary_miner(device_model='random', default_selection=None, miner_url=No
37443777
canary_url = only_file['path']
37453778
canary_device = only_file['device']
37463779
else:
3747-
result = select_pif_device(file_list, default_selection, device_type="Canary")
3780+
result = select_pif_device(file_list, default_selection, device_type="Canary", show_filename=False)
37483781
canary_url, canary_device = result if result is not None else (None, False)
37493782
if not canary_url or not canary_device:
37503783
return "Selection cancelled."
@@ -3768,15 +3801,16 @@ def get_canary_miner(device_model='random', default_selection=None, miner_url=No
37683801
# ============================================================================
37693802
# Function select_pif_device
37703803
# ============================================================================
3771-
def select_pif_device(devices_data, default_selection=None, device_type="") -> tuple[str, str] | tuple[None, bool] | None:
3804+
def select_pif_device(devices_data, default_selection=None, device_type="", show_filename=True) -> tuple[str, str] | tuple[None, bool] | None:
37723805
try:
37733806
from device_selector import show_device_selector
37743807
selected_device = show_device_selector(
37753808
parent=None,
37763809
devices=devices_data,
37773810
title=f"Select {device_type} Device",
37783811
message=f"Select a {device_type} device:",
3779-
select_device=default_selection
3812+
select_device=default_selection,
3813+
show_filename=show_filename
37803814
)
37813815
if selected_device:
37823816
pif_url = selected_device['path']

windows-metadata.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# https://github.com/DudeNr33/pyinstaller-versionfile
22
# create-version-file windows-metadata.yaml --outfile windows-version-info.txt
3-
Version: 9.0.1.0
3+
Version: 9.0.2.0
44
FileDescription: PixelFlasher
55
InternalName: PixelFlasher
66
OriginalFilename: PixelFlasher.exe

windows-version-info.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ VSVersionInfo(
77
ffi=FixedFileInfo(
88
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
99
# Set not needed items to zero 0. Must always contain 4 elements.
10-
filevers=(9,0,1,0),
11-
prodvers=(9,0,1,0),
10+
filevers=(9,0,2,0),
11+
prodvers=(9,0,2,0),
1212
# Contains a bitmask that specifies the valid bits 'flags'r
1313
mask=0x3f,
1414
# Contains a bitmask that specifies the Boolean attributes of the file.
@@ -32,12 +32,12 @@ VSVersionInfo(
3232
u'040904B0',
3333
[StringStruct(u'CompanyName', u''),
3434
StringStruct(u'FileDescription', u'PixelFlasher'),
35-
StringStruct(u'FileVersion', u'9.0.1.0'),
35+
StringStruct(u'FileVersion', u'9.0.2.0'),
3636
StringStruct(u'InternalName', u'PixelFlasher'),
3737
StringStruct(u'LegalCopyright', u''),
3838
StringStruct(u'OriginalFilename', u'PixelFlasher.exe'),
3939
StringStruct(u'ProductName', u'PixelFlasher'),
40-
StringStruct(u'ProductVersion', u'9.0.1.0')])
40+
StringStruct(u'ProductVersion', u'9.0.2.0')])
4141
]),
4242
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
4343
]

0 commit comments

Comments
 (0)