massive changes for 1.5.0

This commit is contained in:
2026-06-28 08:17:36 -07:00
parent 57b7082564
commit 766bf75dd7
11 changed files with 1603 additions and 261 deletions
+2 -1
View File
@@ -180,4 +180,5 @@ cython_debug/
*.snirf
*.json
flares-*
*.flare
*.flare
*.cfg
+17
View File
@@ -1,3 +1,20 @@
# Version 1.5.0
- This release introduces a new configuration file that may break existing installs. If your application does not update correctly, please download fresh from [this link.](https://git.research.dezeeuw.ca/tyler/flares/releases/)
- New configuration file has been added! Now your choices of preferences will be saved when the application is closed and re-opened. If the configuration file is missing, a new one will be generated
- The new option "Reset to Default Configuration" will reset the configuration file to it's default values
- Recent files and recent projects are now saved and appear under the File menu for quick resuming
- A welcome dialog will now display the changelog after every update. This popup will only appear once but can be reopened under the Options menu through "Show Update Changelog"
- Changed the hotkey for "Update optodes in snirf file..." to be F9 instead of F6
- Revamped the fOLD channels window. Images containing the pie charts are now interactable! Click whitespace to expand the whole image and click a chart to expand it.
- fOLD progress bar when processing now updates the percentages live. Fixes [Issue 76](https://git.research.dezeeuw.ca/tyler/flares/issues/76)
- Overall pie charts on an individal and global basis are now genereted. Fixes [Issue 78](https://git.research.dezeeuw.ca/tyler/flares/issues/78)
- Brodmann images are now available when examining a pie chart to understand which area is being reported. Fixes [Issue 77](https://git.research.dezeeuw.ca/tyler/flares/issues/77)
- Added a new option 'Folding Bypass' to the Preferences Menu. This skips most processing steps and the only analysis option available will be to fold. Parameters on the right will be ignored. Fixes [Issue 75](https://git.research.dezeeuw.ca/tyler/flares/issues/75)
- Added a feature to hover over the 28 stage progress bar and see which state the progress bar is at. Fixes [Issue 74](https://git.research.dezeeuw.ca/tyler/flares/issues/74)
- Loading a broken snirf file no longer hangs its processing and can now be removed from the list. Fixes [Issue 73](https://git.research.dezeeuw.ca/tyler/flares/issues/73)
# Version 1.4.3
- Fixed an issue where the fOLD files could not be located
+185 -142
View File
@@ -213,6 +213,8 @@ AGE: int = 25 # Assume 25 if not set from the GUI. This will result in a reason
GENDER: str = ""
GROUP: str = "Default"
FOLDING_BYP: bool = False
# These are parameters that are required for the analysis
REQUIRED_KEYS: dict[str, Any] = {
@@ -1487,7 +1489,7 @@ def make_design_matrix(raw_haemo, short_chans):
pass
# 2) Create design matrix
if SHORT_CHANNEL_REGRESSION:
if SHORT_CHANNEL_REGRESSION and not FOLDING_BYP:
design_matrix = make_first_level_design_matrix(
raw=raw_haemo,
stim_dur=STIM_DUR,
@@ -1739,161 +1741,197 @@ def resource_path(relative_path):
def fold_channels(raw: BaseRaw) -> None:
# def fold_channels(raw: BaseRaw) -> None:
# Locate the fOLD excel files
if getattr(sys, 'frozen', False):
set_config('MNE_NIRS_FOLD_PATH', resource_path("./mne_data/fOLD/fOLD-public-master/Supplementary")) # type: ignore
else:
path = os.path.expanduser("~") + "/mne_data/fOLD/fOLD-public-master/Supplementary"
set_config('MNE_NIRS_FOLD_PATH', resource_path(path)) # type: ignore
# # Locate the fOLD excel files
# if getattr(sys, 'frozen', False):
# set_config('MNE_NIRS_FOLD_PATH', resource_path("./mne_data/fOLD/fOLD-public-master/Supplementary")) # type: ignore
# else:
# path = os.path.expanduser("~") + "/mne_data/fOLD/fOLD-public-master/Supplementary"
# set_config('MNE_NIRS_FOLD_PATH', resource_path(path)) # type: ignore
output = None
# output = None
# List to store the results
landmark_specificity_data: list[dict[str, Any]] = []
# # List to store the results
# landmark_specificity_data: list[dict[str, Any]] = []
# Filter the data to only what we want
hbo_channel_names = cast(list[str], getattr(raw.copy().pick(picks='hbo'), "ch_names")) # type: ignore
# # Filter the data to only what we want
# hbo_channel_names = cast(list[str], getattr(raw.copy().pick(picks='hbo'), "ch_names")) # type: ignore
# Format the output to make it slightly easier to read
# # Format the output to make it slightly easier to read
if True:
num_channels = len(hbo_channel_names)
rows, cols = 4, 7 # 6 rows and 4 columns of pie charts
fig, axes = plt.subplots(rows, cols, figsize=(16, 10), constrained_layout=True)
axes = axes.flatten() # Flatten the axes array for easier indexing
# if True:
# num_channels = len(hbo_channel_names)
# rows, cols = 4, 7 # 6 rows and 4 columns of pie charts
# fig, axes = plt.subplots(rows, cols, figsize=(16, 10), constrained_layout=True)
# axes = axes.flatten() # Flatten the axes array for easier indexing
# If more pie charts than subplots, create extra subplots
if num_channels > rows * cols:
fig, axes = plt.subplots((num_channels // cols) + 1, cols, figsize=(16, 10), constrained_layout=True)
axes = axes.flatten()
# # If more pie charts than subplots, create extra subplots
# if num_channels > rows * cols:
# fig, axes = plt.subplots((num_channels // cols) + 1, cols, figsize=(16, 10), constrained_layout=True)
# axes = axes.flatten()
# Create a list for consistent color mapping
landmarks = [
"1 - Primary Somatosensory Cortex",
"2 - Primary Somatosensory Cortex",
"3 - Primary Somatosensory Cortex",
"4 - Primary Motor Cortex",
"5 - Somatosensory Association Cortex",
"6 - Pre-Motor and Supplementary Motor Cortex",
"7 - Somatosensory Association Cortex",
"8 - Includes Frontal eye fields",
"9 - Dorsolateral prefrontal cortex",
"10 - Frontopolar area",
"11 - Orbitofrontal area",
"17 - Primary Visual Cortex (V1)",
"18 - Visual Association Cortex (V2)",
"19 - V3",
"20 - Inferior Temporal gyrus",
"21 - Middle Temporal gyrus",
"22 - Superior Temporal Gyrus",
"23 - Ventral Posterior cingulate cortex",
"24 - Ventral Anterior cingulate cortex",
"25 - Subgenual cortex",
"32 - Dorsal anterior cingulate cortex",
"37 - Fusiform gyrus",
"38 - Temporopolar area",
"39 - Angular gyrus, part of Wernicke's area",
"40 - Supramarginal gyrus part of Wernicke's area",
"41 - Primary and Auditory Association Cortex",
"42 - Primary and Auditory Association Cortex",
"43 - Subcentral area",
"44 - pars opercularis, part of Broca's area",
"45 - pars triangularis Broca's area",
"46 - Dorsolateral prefrontal cortex",
"47 - Inferior prefrontal gyrus",
"48 - Retrosubicular area",
"Brain_Outside",
]
# # Create a list for consistent color mapping
# landmarks = [
# "1 - Primary Somatosensory Cortex",
# "2 - Primary Somatosensory Cortex",
# "3 - Primary Somatosensory Cortex",
# "4 - Primary Motor Cortex",
# "5 - Somatosensory Association Cortex",
# "6 - Pre-Motor and Supplementary Motor Cortex",
# "7 - Somatosensory Association Cortex",
# "8 - Includes Frontal eye fields",
# "9 - Dorsolateral prefrontal cortex",
# "10 - Frontopolar area",
# "11 - Orbitofrontal area",
# "17 - Primary Visual Cortex (V1)",
# "18 - Visual Association Cortex (V2)",
# "19 - V3",
# "20 - Inferior Temporal gyrus",
# "21 - Middle Temporal gyrus",
# "22 - Superior Temporal Gyrus",
# "23 - Ventral Posterior cingulate cortex",
# "24 - Ventral Anterior cingulate cortex",
# "25 - Subgenual cortex",
# "32 - Dorsal anterior cingulate cortex",
# "37 - Fusiform gyrus",
# "38 - Temporopolar area",
# "39 - Angular gyrus, part of Wernicke's area",
# "40 - Supramarginal gyrus part of Wernicke's area",
# "41 - Primary and Auditory Association Cortex",
# "42 - Primary and Auditory Association Cortex",
# "43 - Subcentral area",
# "44 - pars opercularis, part of Broca's area",
# "45 - pars triangularis Broca's area",
# "46 - Dorsolateral prefrontal cortex",
# "47 - Inferior prefrontal gyrus",
# "48 - Retrosubicular area",
# "Brain_Outside",
# ]
cmap1 = plt.get_cmap('tab20') # First 20 colors
cmap2 = plt.get_cmap('tab20b') # Next 20 colors
# cmap1 = plt.get_cmap('tab20') # First 20 colors
# cmap2 = plt.get_cmap('tab20b') # Next 20 colors
# Combine the colors from both colormaps
colors = [cmap1(i) for i in range(20)] + [cmap2(i) for i in range(20)] # Total 40 colors
# # Combine the colors from both colormaps
# colors = [cmap1(i) for i in range(20)] + [cmap2(i) for i in range(20)] # Total 40 colors
landmarks.sort(key=lambda x: (int(x.split(" - ")[0]) if x.split(" - ")[0].isdigit() else float('inf')))
# landmarks.sort(key=lambda x: (int(x.split(" - ")[0]) if x.split(" - ")[0].isdigit() else float('inf')))
landmark_color_map = {landmark: colors[i % len(colors)] for i, landmark in enumerate(landmarks)}
# landmark_color_map = {landmark: colors[i % len(colors)] for i, landmark in enumerate(landmarks)}
# Iterate over each channel
print(len(hbo_channel_names))
# # Iterate over each channel
# print(len(hbo_channel_names))
for idx, channel_name in enumerate(hbo_channel_names):
# for idx, channel_name in enumerate(hbo_channel_names):
print(idx, channel_name)
# Run the fOLD on the selected channel
channel_data = raw.copy().pick(picks=channel_name) # type: ignore
# print(idx, channel_name)
# # Run the fOLD on the selected channel
# channel_data = raw.copy().pick(picks=channel_name) # type: ignore
output = cast(list[DataFrame], fold_channel_specificity_normal(channel_data, interpolate=True, atlas='Brodmann'))
# output = cast(list[DataFrame], fold_channel_specificity_normal(channel_data, interpolate=True, atlas='Brodmann'))
# Process each DataFrame that fold_channel_specificity returns
for df_data in output:
# # Process each DataFrame that fold_channel_specificity returns
# for df_data in output:
# Extract the relevant columns
useful_data = df_data[['Landmark', 'Specificity']]
# # Extract the relevant columns
# useful_data = df_data[['Landmark', 'Specificity']]
# Store the results
landmark_specificity_data.append({
'Channel': channel_name,
'Data': useful_data,
})
# # Store the results
# landmark_specificity_data.append({
# 'Channel': channel_name,
# 'Data': useful_data,
# })
# Plot the results
# TODO: Fix this
if True:
unique_landmarks = sorted(useful_data['Landmark'].unique())
color_list = [landmark_color_map[landmark] for landmark in useful_data['Landmark']]
# # Plot the results
# # TODO: Fix this
# if True:
# unique_landmarks = sorted(useful_data['Landmark'].unique())
# color_list = [landmark_color_map[landmark] for landmark in useful_data['Landmark']]
# Plot specificity for each channel
ax = axes[idx]
# # Plot specificity for each channel
# ax = axes[idx]
labels = [f'{landmark.split(" - ")[0]}' if landmark != 'Brain_Outside' else 'B' for landmark in useful_data['Landmark']]
# labels = [f'{landmark.split(" - ")[0]}' if landmark != 'Brain_Outside' else 'B' for landmark in useful_data['Landmark']]
wedges, texts, autotexts = ax.pie(
useful_data['Specificity'],
autopct='%1.1f%%',
startangle=90,
labels=labels,
labeldistance=1.05,
colors=color_list)
# wedges, texts, autotexts = ax.pie(
# useful_data['Specificity'],
# autopct='%1.1f%%',
# startangle=90,
# labels=labels,
# labeldistance=1.05,
# colors=color_list)
ax.set_title(f'{channel_name}')
ax.axis('equal')
# ax.set_title(f'{channel_name}')
# ax.axis('equal')
landmark_specificity_data = []
# landmark_specificity_data = []
# TODO: Fix this
if True:
handles = [
plt.Line2D([0], [0], marker='o', color='w', label=landmark, markersize=10,
markerfacecolor=landmark_color_map[landmark])
for landmark in landmarks
]
n_landmarks = len(landmarks)
# # TODO: Fix this
# if True:
# handles = [
# plt.Line2D([0], [0], marker='o', color='w', label=landmark, markersize=10,
# markerfacecolor=landmark_color_map[landmark])
# for landmark in landmarks
# ]
# n_landmarks = len(landmarks)
# Calculate the figure size based on number of rows and columns
fig_width = 5
fig_height = n_landmarks / 4
# # Calculate the figure size based on number of rows and columns
# fig_width = 5
# fig_height = n_landmarks / 4
# Create a new figure window for the legend
legend_fig = plt.figure(figsize=(fig_width, fig_height))
legend_axes = legend_fig.add_subplot(111)
legend_axes.axis('off') # Turn off axis for the legend window
legend_axes.legend(handles=handles, loc='center', fontsize=10, title="Landmarks")
# # Create a new figure window for the legend
# legend_fig = plt.figure(figsize=(fig_width, fig_height))
# legend_axes = legend_fig.add_subplot(111)
# legend_axes.axis('off') # Turn off axis for the legend window
# legend_axes.legend(handles=handles, loc='center', fontsize=10, title="Landmarks")
for ax in axes[len(hbo_channel_names):]:
ax.axis('off')
# for ax in axes[len(hbo_channel_names):]:
# ax.axis('off')
#plt.show()
fig_dict = {"main": fig, "legend": legend_fig}
return convert_fig_dict_to_png_bytes(fig_dict)
# #plt.show()
# fig_dict = {"main": fig, "legend": legend_fig}
# return convert_fig_dict_to_png_bytes(fig_dict)
def fold_channels(raw: BaseRaw, p_name: str, progress_queue=None) -> dict[str, list[dict[str, Any]]]:
"""Runs in background process.
Does only heavy math/lookup. Returns data instead of a static image.
"""
if getattr(sys, 'frozen', False):
set_config('MNE_NIRS_FOLD_PATH', resource_path("./mne_data/fOLD/fOLD-public-master/Supplementary"))
else:
path = os.path.expanduser("~") + "/mne_data/fOLD/fOLD-public-master/Supplementary"
set_config('MNE_NIRS_FOLD_PATH', resource_path(path))
hbo_channel_names = cast(list[str], getattr(raw.copy().pick(picks='hbo'), "ch_names"))
# Store clean, picklable data lists instead of complex DataFrames
channel_results = {}
step_idx = 0
for channel_name in hbo_channel_names:
channel_data = raw.copy().pick(picks=channel_name)
output = cast(list[DataFrame], fold_channel_specificity_normal(channel_data, interpolate=True, atlas='Brodmann'))
channel_results[channel_name] = []
for df_data in output:
# Extract just raw primitive types so they transfer over process channels flawlessly
for _, row in df_data.iterrows():
channel_results[channel_name].append({
'Landmark': str(row['Landmark']),
'Specificity': float(row['Specificity'])
})
step_idx += 1
if progress_queue is not None:
progress_queue.put((p_name, step_idx))
# Return raw data dictionary to the result_queue
return channel_results
def individual_significance(raw_haemo, glm_est):
@@ -3939,6 +3977,7 @@ def hr_calc(raw):
def process_participant(file_path, progress_callback=None):
fig_individual: dict[str, Figure] = {}
logger.info(f"Folding Bypass: {FOLDING_BYP}")
# Step 1: Preprocessing
raw = load_snirf(file_path)
@@ -3949,7 +3988,7 @@ def process_participant(file_path, progress_callback=None):
# Step 2: Trimming
# TODO: Clean this into a method
if TRIM:
if TRIM and not FOLDING_BYP:
if hasattr(raw, 'annotations') and len(raw.annotations) > 0:
# Get time of first event
first_event_time = raw.annotations.onset[0]
@@ -3985,7 +4024,7 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 3 Completed.")
# Step 4: Short/Long Channels
if SHORT_CHANNEL:
if SHORT_CHANNEL and not FOLDING_BYP:
short_chans = get_short_channels(raw, max_dist=SHORT_CHANNEL_THRESH)
fig_short_chans = short_chans.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Short Channels Only", show=False)
fig_individual["short"] = fig_short_chans
@@ -3996,7 +4035,7 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 4 Completed.")
# Step 5: Heart Rate
if HEART_RATE:
if HEART_RATE and not FOLDING_BYP:
fig, hr1, hr2, low, high = hr_calc(raw)
fig_individual["PSD"] = fig
fig_individual['HeartRate_PSD'] = hr1
@@ -4017,7 +4056,7 @@ def process_participant(file_path, progress_callback=None):
# Step 6: Scalp Coupling Index
bad_sci = []
if SCI:
if SCI and not FOLDING_BYP:
if HEART_RATE:
bad_sci, fig_sci_1, fig_sci_2 = calculate_scalp_coupling(raw, low, high)
else:
@@ -4029,7 +4068,7 @@ def process_participant(file_path, progress_callback=None):
# Step 7: Signal to Noise Ratio
bad_snr = []
if SNR:
if SNR and not FOLDING_BYP:
bad_snr, fig_snr = calculate_signal_noise_ratio(raw)
fig_individual["SNR1"] = fig_snr
if progress_callback: progress_callback(7)
@@ -4037,7 +4076,7 @@ def process_participant(file_path, progress_callback=None):
# Step 8: Peak Spectral Power
bad_psp = []
if PSP:
if PSP and not FOLDING_BYP:
bad_psp, fig_psp1, fig_psp2 = calculate_peak_power(raw)
fig_individual["PSP1"] = fig_psp1
fig_individual["PSP2"] = fig_psp2
@@ -4045,35 +4084,35 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 8 Completed.")
bad_cv = []
if CV:
if CV and not FOLDING_BYP:
bad_cv, fig_cv = find_bad_channels_cv(raw, cv_threshold=CV_THRESHOLD)
fig_individual['cv'] = fig_cv
if progress_callback: progress_callback(9)
logger.info("Step 9 Completed.")
bad_range = []
if MAD:
if MAD and not FOLDING_BYP:
bad_range, fig_range = find_bad_channels_range(raw, threshold=MAD_THRESHOLD)
fig_individual['range'] = fig_range
if progress_callback: progress_callback(10)
logger.info("Step 10 Completed.")
bad_noise = []
if PSD_NOISE:
if PSD_NOISE and not FOLDING_BYP:
bad_noise, fig_noise = detect_high_freq_noise(raw, db_limit=DB_LIMIT, freq_div=TARGET_FREQ_DIV)
fig_individual['psd_noise'] = fig_noise
if progress_callback: progress_callback(11)
logger.info("Step 11 Completed.")
bad_disp = []
if CHANNEL_VAR:
if CHANNEL_VAR and not FOLDING_BYP:
bad_disp, fig_disp = detect_sensor_displacement(raw, threshold_ratio=CHANNEL_THRESH)
fig_individual['displacement'] = fig_disp
if progress_callback: progress_callback(12)
logger.info("Step 12 Completed.")
# Step 9: Bad Channels Handling
if BAD_CHANNELS_HANDLING != "None":
if BAD_CHANNELS_HANDLING != "None" and not FOLDING_BYP:
raw, fig_dropped, fig_raw_before, bad_channels = mark_bads(raw, bad_sci, bad_snr, bad_psp, bad_cv, bad_range, bad_noise, bad_disp)
if fig_dropped and fig_raw_before is not None:
fig_individual["fig2"] = fig_dropped
@@ -4108,7 +4147,7 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 14 Completed.")
# Step 11: Temporal Derivative Distribution Repair Filtering
if TDDR:
if TDDR and not FOLDING_BYP:
raw_od = temporal_derivative_distribution_repair(raw_od)
fig_raw_od_tddr = raw_od.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="After TDDR (Motion Correction)", show=False)
fig_individual["TDDR"] = fig_raw_od_tddr
@@ -4116,7 +4155,7 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 15 Completed.")
# Step 12: Wavelet Filtering
if WAVELET:
if WAVELET and not FOLDING_BYP:
raw_od, fig = calculate_and_apply_wavelet(raw_od)
fig_individual["Wavelet"] = fig
if progress_callback: progress_callback(16)
@@ -4130,7 +4169,7 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 17 Completed.")
# Step 14: Enhance Negative Correlation
if ENHANCE_NEGATIVE_CORRELATION:
if ENHANCE_NEGATIVE_CORRELATION and not FOLDING_BYP:
raw_haemo = enhance_negative_correlation(raw_haemo)
fig_raw_haemo_enc = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Enhance Negative Correlation", show=False)
fig_individual["ENC"] = fig_raw_haemo_enc
@@ -4138,7 +4177,7 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 18 Completed.")
# Step 15: Filter
if FILTER:
if FILTER and not FOLDING_BYP:
raw_haemo, fig_filter, fig_raw_haemo_filter = filter_the_data(raw_haemo)
fig_individual["filter1"] = fig_filter
fig_individual["filter2"] = fig_raw_haemo_filter
@@ -4146,16 +4185,18 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 19 Completed.")
# Step 16: Extracting Events
events, event_dict = events_from_annotations(raw_haemo)
fig_events = plot_events(events, event_id=event_dict, sfreq=raw_haemo.info["sfreq"], show=False)
fig_individual["events"] = fig_events
if not FOLDING_BYP:
events, event_dict = events_from_annotations(raw_haemo)
fig_events = plot_events(events, event_id=event_dict, sfreq=raw_haemo.info["sfreq"], show=False)
fig_individual["events"] = fig_events
if progress_callback: progress_callback(20)
logger.info("Step 20 Completed.")
# Step 17: Epoch Calculations
epochs, fig_epochs = epochs_calculations(raw_haemo, events, event_dict)
for name, fig in fig_epochs:
fig_individual[f"epochs_{name}"] = fig
if not FOLDING_BYP:
epochs, fig_epochs = epochs_calculations(raw_haemo, events, event_dict)
for name, fig in fig_epochs:
fig_individual[f"epochs_{name}"] = fig
if progress_callback: progress_callback(21)
logger.info("Step 21 Completed.")
@@ -4274,6 +4315,8 @@ def process_participant(file_path, progress_callback=None):
# Step 24: Finishing Up
fig_bytes = convert_fig_dict_to_png_bytes(fig_individual)
if FOLDING_BYP:
epochs = None
sanitize_paths_for_pickle(raw_haemo, epochs)
if progress_callback: progress_callback(28)
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M400-280h160v-80H400v80Zm0-160h280v-80H400v80ZM280-600h400v-80H280v80Zm200 120ZM265-80q-79 0-134.5-55.5T75-270q0-57 29.5-102t77.5-68H80v-80h240v240h-80v-97q-37 8-61 38t-24 69q0 46 32.5 78t77.5 32v80Zm135-40v-80h360v-560H200v160h-80v-160q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H400Z"/></svg>

After

Width:  |  Height:  |  Size: 443 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-120q-138 0-240.5-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 444 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-80q-155 0-269-103T82-440h81q15 121 105.5 200.5T480-160q134 0 227-93t93-227q0-134-93-227t-227-93q-86 0-159.5 42.5T204-640h116v80H88q29-140 139-230t253-90q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm112-232L440-464v-216h80v184l128 128-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 416 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M520-330v-60h160v60H520Zm60 210v-50h-60v-60h60v-50h60v160h-60Zm100-50v-60h160v60H680Zm40-110v-160h60v50h60v60h-60v50h-60Zm111-280h-83q-26-88-99-144t-169-56q-117 0-198.5 81.5T200-480q0 72 32.5 132t87.5 98v-110h80v240H160v-80h94q-62-50-98-122.5T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q129 0 226.5 79.5T831-560Z"/></svg>

After

Width:  |  Height:  |  Size: 449 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M451.5-251.5Q440-263 440-280t11.5-28.5Q463-320 480-320t28.5 11.5Q520-297 520-280t-11.5 28.5Q497-240 480-240t-28.5-11.5ZM440-360v-161l80 80v81h-80Zm433 158L655-419 480-720l-47 80-58-58 105-182 393 678Zm-695 2h469L350-497 178-200ZM819-28l-92-92H40l252-435L27-820l57-57L876-85l-57 57ZM499-348Zm45-181Z"/></svg>

After

Width:  |  Height:  |  Size: 423 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+1381 -116
View File
File diff suppressed because it is too large Load Diff
+13 -2
View File
@@ -16,6 +16,7 @@ import shutil
import zipfile
import traceback
import subprocess
import configparser
# External library imports
import psutil
@@ -415,13 +416,24 @@ def wait_for_process_to_exit(process_name, timeout=10):
return False
def finish_update_if_needed(platform_name, app_name):
def finish_update_if_needed(platform_name, app_name, cfg_path):
"""
Completes a pending application update if '--finish-update' is present in the command-line arguments.
"""
if "--finish-update" in sys.argv:
print("Finishing update...")
update_cfg = configparser.ConfigParser()
try:
update_cfg.read(cfg_path)
update_cfg.set("Options", "show_welcome_dialog", "true")
with open(cfg_path, "w") as f:
update_cfg.write(f)
print("Welcome dialog flag successfully reset to 'true' for next run.")
except Exception as e:
print(f"Warning: Could not update welcome dialog preference flag: {e}")
if platform_name == 'darwin':
app_dir = f'/tmp/{app_name}tempupdate'
@@ -519,7 +531,6 @@ def finish_update_if_needed(platform_name, app_name):
except Exception as e:
print(f"Failed to delete update folder: {e}")
QMessageBox.information(None, "Update Complete", "The application has been successfully updated.")
sys.argv.remove("--finish-update")