Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Build Test

on:
pull_request:
workflow_dispatch:

jobs:
build-desktop:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: macos-latest
spec: DeerAnalysis_MacOS.spec
python-version: '3.12'
- os: windows-latest
spec: DeerAnalysis_Win.spec
python-version: '3.12'

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
pip install -e .

- name: Build with PyInstaller
run: pyinstaller "${{ matrix.spec }}" --distpath dist --workpath build
working-directory: packaging/pyinstaller

- name: Get version
id: get_version
run: |
VERSION=$(python -c "import tomllib; f = open('pyproject.toml', 'rb'); data = tomllib.load(f); print(data['project']['version']); f.close()")
echo "version=$VERSION" >> $GITHUB_OUTPUT
shell: bash

- name: Prepare artifact name
id: artifact_name
run: |
if [ "${{ runner.os }}" = "macOS" ]; then
ARTIFACT_NAME="DeerAnalysis-${{ steps.get_version.outputs.version }}-macos"
elif [ "${{ runner.os }}" = "Windows" ]; then
ARTIFACT_NAME="DeerAnalysis-${{ steps.get_version.outputs.version }}-windows"
fi
echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
shell: bash

- name: Package macOS build
if: runner.os == 'macOS'
run: |
hdiutil create \
-volname "DeerAnalysis" \
-srcfolder packaging/pyinstaller/dist/DeerAnalysis.app \
-ov -format UDZO \
"packaging/pyinstaller/dist/${{ steps.artifact_name.outputs.artifact_name }}.dmg"

- name: Package Windows build
if: runner.os == 'Windows'
# The Win spec names the executable "DeerAnalysis 2026.exe".
run: |
cd packaging/pyinstaller/dist
Compress-Archive -Path "DeerAnalysis 2026.exe" -DestinationPath "${{ steps.artifact_name.outputs.artifact_name }}.zip"
shell: pwsh

- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ steps.artifact_name.outputs.artifact_name }}
path: |
packaging/pyinstaller/dist/*.zip
packaging/pyinstaller/dist/*.dmg
retention-days: 1

build-flatpak:
name: Build Linux Flatpak
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Install flatpak-builder
run: |
sudo apt-get update -qq
sudo apt-get install -y flatpak flatpak-builder xvfb
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo

- name: Install Flatpak SDK
run: flatpak install --user -y --noninteractive flathub org.gnome.Platform//50 org.gnome.Sdk//50

- name: Get version
id: get_version
run: |
VERSION=$(python3 -c "import tomllib; f = open('pyproject.toml', 'rb'); data = tomllib.load(f); print(data['project']['version']); f.close()")
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Build Flatpak bundle
run: |
xvfb-run -a flatpak-builder \
--user \
--repo=repo \
--disable-rofiles-fuse \
--force-clean \
flatpak_app \
packaging/flatpak/io.github.JeschkeLab.DeerAnalysis.yml
flatpak build-bundle \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo \
repo DeerAnalysis.flatpak io.github.JeschkeLab.DeerAnalysis
- name: Upload Flatpak artifact
uses: actions/upload-artifact@v7
with:
name: DeerAnalysis-${{ steps.get_version.outputs.version }}-linux
path: DeerAnalysis.flatpak
retention-days: 1
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "deeranalysis"
version = "2026.0.1"
version = "2026.0.2"
description = "A graphical fitting and data managment tool for DEER spectroscopy, using DeerLab and DeerNet as fitting engines."
authors = [
{name = "Hugo Karas",email = "hkaras@ethz.ch"}
Expand Down
17 changes: 17 additions & 0 deletions src/deeranalysis/assets/dmc.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,21 @@ html[data-mantine-color-scheme="dark"] .ag-theme-alpine {
.mantine-Notification-icon {
width: 28px;
height: 28px;
}

/* ── Fit detail: title-sized TextInput ──────────────────────────────────────
The fit-name TextInput on the fit detail page should render as a page title.
Mantine's `.mantine-Input-input { font-size: var(--input-fz) }` rule (class
specificity) beats the styles-prop class at equal weight, so we scope by id
(#fd-fit-name) to win on specificity without !important.

font-size is tied to --input-height so the text scales with the size prop
(size="lg" → ~50px box → ~42px text) and fills the input box rather than
floating in a sea of whitespace. */
#fd-fit-name {
font-size: calc(var(--input-height) * 0.85);
line-height: 1;
font-weight: var(--mantine-heading-font-weight, 700);
letter-spacing: -0.02em;
padding: 0;
}
132 changes: 132 additions & 0 deletions src/deeranalysis/assets/figure_download.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Routes Plotly's modebar "Download plot as image" button through
// pywebview's native save dialog. Intercepts the camera button click in
// the document-level capture phase. Shows a format picker (SVG / PNG)
// before opening the native save dialog so the choice is always explicit.
(function () {
function figureName(gd) {
var title = gd && gd.layout && gd.layout.title;
if (title && typeof title === 'string') return title;
if (title && title.text) return title.text;
return 'figure';
}

function isDownloadButton(btn) {
if (btn.hasAttribute('data-pywv-download')) return true;
var title = (btn.getAttribute('data-title') || '').toLowerCase();
return title.indexOf('download') !== -1 ||
title.indexOf('save plot') !== -1 ||
title.indexOf('image') !== -1;
}

function showFormatPicker(svgUrl, pngUrl, name) {
var existing = document.getElementById('pywv-fmt-picker');
if (existing) existing.remove();

var overlay = document.createElement('div');
overlay.id = 'pywv-fmt-overlay';
overlay.style.cssText =
'position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.3)';

var picker = document.createElement('div');
picker.id = 'pywv-fmt-picker';
picker.style.cssText =
'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' +
'background:#fff;border-radius:8px;padding:20px 24px;' +
'box-shadow:0 8px 32px rgba(0,0,0,0.2);z-index:9999;' +
'font-family:system-ui,sans-serif;text-align:center;min-width:220px';
picker.innerHTML =
'<p style="margin:0 0 16px;font-size:15px;font-weight:600;color:#212529">' +
'Save figure as&hellip;</p>' +
'<div style="display:flex;gap:10px;justify-content:center">' +
'<button id="pywv-save-svg" style="' +
'flex:1;padding:9px 0;cursor:pointer;border:1.5px solid #228be6;' +
'color:#228be6;background:#fff;border-radius:6px;font-size:14px;' +
'font-weight:600">SVG</button>' +
'<button id="pywv-save-png" style="' +
'flex:1;padding:9px 0;cursor:pointer;border:1.5px solid #868e96;' +
'color:#495057;background:#fff;border-radius:6px;font-size:14px">' +
'PNG</button>' +
'</div>';

function dismiss() {
overlay.remove();
picker.remove();
document.removeEventListener('keydown', onKey, true);
}

function onKey(e) {
if (e.key === 'Escape') dismiss();
}

overlay.addEventListener('click', dismiss);
document.addEventListener('keydown', onKey, true);

picker.querySelector('#pywv-save-svg').addEventListener('click', function (e) {
e.stopPropagation();
dismiss();
fetch('/save-figure', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({data_url: svgUrl, suggested_name: name, fmt: 'svg'})
});
});
picker.querySelector('#pywv-save-png').addEventListener('click', function (e) {
e.stopPropagation();
dismiss();
fetch('/save-figure', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({data_url: pngUrl, suggested_name: name, fmt: 'png'})
});
});

document.body.appendChild(overlay);
document.body.appendChild(picker);
}

function handle(e) {
var t = e.target;
if (!t || !t.closest) return;
var btn = t.closest('.modebar-btn');
if (!btn || !isDownloadButton(btn)) return;
var gd = btn.closest('.js-plotly-plot') || btn.closest('.plot-container');
if (!gd || !window.Plotly) return;
if (!window.DEERANALYSIS_PYWEBVIEW) {
window.alert('Figure download unavailable: not running in pywebview.');
return;
}

e.preventDefault();
e.stopImmediatePropagation();

var layout = gd._fullLayout || {};
var width = layout.width || 800;
var height = layout.height || 600;
var name = figureName(gd);

Promise.all([
window.Plotly.toImage(gd, {format: 'svg', width: width, height: height}),
window.Plotly.toImage(gd, {format: 'png', width: width, height: height, scale: 2})
]).then(function (urls) {
showFormatPicker(urls[0], urls[1], name);
}).catch(function (err) {
console.error('[figure_download] toImage failed:', err);
window.alert('Could not render figure: ' + (err && err.message ? err.message : err));
});
}

function patchDownloadTooltips(root) {
(root || document).querySelectorAll('.modebar-btn').forEach(function (btn) {
if (isDownloadButton(btn)) {
btn.setAttribute('data-title', 'Save as SVG or PNG');
btn.setAttribute('data-pywv-download', '1');
}
});
}

new MutationObserver(function () {
patchDownloadTooltips();
}).observe(document.body, {childList: true, subtree: true});

document.addEventListener('click', handle, true);
})();
Loading
Loading