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
28 changes: 28 additions & 0 deletions .changeset/brand-menu-nvdocument.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@niivue/react': minor
'@niivue/pwa': minor
'@niivue/tauri': patch
'niivue': patch
'@niivue/streamlit': patch
'@niivue/jupyter': patch
---

Fold the Home button into the brand, and turn "Save Scene" into an NVDocument Save/Load split button.

- The standalone "Home" menu-bar button is gone. On standalone hosts (web, desktop)
the niivue logo + wordmark is now a dropdown: **Reset Viewer** (the old Home
action) and **About** (a dialog with the app's purpose, NiiVue + NeuroDesk
credits, the data-privacy note, and the build version linking to its commit).
The brand stays static on embedded hosts (VS Code; Streamlit, which sets
`menuItems.home: false`), so their behavior is unchanged.
- "Save Scene" is renamed **NVDocument** and gains a chevron: clicking the label
still saves the active scene as a `.nvd` by default, while the dropdown offers
**Save** and a new **Load** (a `.nvd` file picker, complementing drag-and-drop).
- Hosts can pass an optional `appInfo` ({ version, buildDate, repoUrl }) to
`Menu`; the PWA wires in its build-time git metadata. The About dialog degrades
gracefully (omits the version line) when a host supplies none.
- The standalone home screens (PWA and desktop) now share a `HomeSection`
primitive for consistent styling while keeping their host-specific copy (PWA:
install / bookmarklet / update; desktop: native Open File). Both drop their
"Data Privacy" section, and the PWA also drops its version footer, since the
brand menu's About dialog now carries that information.
31 changes: 9 additions & 22 deletions apps/desktop-tauri/src/components/DesktopHomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HomeSection } from '@niivue/react'
import { open } from '@tauri-apps/plugin-dialog'
import {
MEDICAL_IMAGE_EXTENSIONS,
Expand Down Expand Up @@ -59,13 +60,10 @@ export const DesktopHomeScreen = () => {

return (
<>
<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 p-2 px-4">
NiiVue Tauri
</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
Open medical images from your local filesystem. Drag and drop files
onto this window, or use the button below.
</p>
<HomeSection title="NiiVue Tauri">
Open medical images from your local filesystem. Drag and drop files onto this window, or use
the button below.
</HomeSection>

{isTauri() && (
<button
Expand All @@ -76,21 +74,10 @@ export const DesktopHomeScreen = () => {
</button>
)}

<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 py-2 px-4">
Supported Formats
</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
NIfTI (.nii, .nii.gz), DICOM (.dcm), MHA/MHD, NRRD, FreeSurfer (.mgh,
.mgz), GIfTI (.gii), and many more.
</p>

<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 py-2 px-4">
Data Privacy
</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
All processing happens locally on your machine. No data is sent to any
remote server. Your medical images never leave your computer.
</p>
<HomeSection title="Supported Formats">
NIfTI (.nii, .nii.gz), DICOM (.dcm), MHA/MHD, NRRD, FreeSurfer (.mgh, .mgz), GIfTI (.gii),
and many more.
</HomeSection>
</>
)
}
16 changes: 13 additions & 3 deletions apps/desktop-tauri/test/DesktopHomeScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* open flow that orchestrates registerOpenedPath + initCanvas + per-file
* loads.
*/
import type { ComponentChildren } from 'preact'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { fireEvent, render } from '@testing-library/preact'

Expand Down Expand Up @@ -34,6 +35,13 @@ vi.mock('@niivue/react', () => ({
ImageDrop: ({ children }: { children: unknown }) => children,
Menu: () => null,
listenToMessages: vi.fn(),
// Mirror the real HomeSection's title + body so getByText assertions work.
HomeSection: ({ title, children }: { title: string; children: ComponentChildren }) => (
<section>
<h2>{title}</h2>
<p>{children}</p>
</section>
),
}))

import { invoke, isTauri as tauriIsTauri } from '@tauri-apps/api/core'
Expand Down Expand Up @@ -67,10 +75,12 @@ describe('DesktopHomeScreen', () => {
expect(queryByText(/Open File/)).toBeNull()
})

it('describes supported formats and the data-privacy guarantee', () => {
const { getByText } = render(<DesktopHomeScreen />)
it('introduces the app and describes supported formats', () => {
const { getByText, queryByText } = render(<DesktopHomeScreen />)
expect(getByText(/NiiVue Tauri/)).toBeInTheDocument()
expect(getByText(/Supported Formats/)).toBeInTheDocument()
expect(getByText(/Data Privacy/)).toBeInTheDocument()
// The data-privacy note now lives in the brand menu's About dialog.
expect(queryByText(/Data Privacy/)).toBeNull()
})
})

Expand Down
9 changes: 8 additions & 1 deletion apps/pwa/src/Pwa.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { computed } from '@preact/signals'
import { useEffect } from 'preact/hooks'
import { HomeScreen } from './components/HomeScreen'

// Build metadata for the brand menu's About dialog (injected by vite.config.ts).
const appInfo = {
version: __GIT_HASH__,
buildDate: __BUILD_DATE__,
repoUrl: __GIT_REPO_URL__,
}

export const Pwa = ({ appProps }: { appProps: AppProps }) => {
const nImages = computed(() => appProps.nvArray.value.length)
const showHomeScreen = computed(() => nImages.value == 0)
Expand Down Expand Up @@ -53,7 +60,7 @@ export const Pwa = ({ appProps }: { appProps: AppProps }) => {

return (
<ImageDrop>
<Menu {...appProps} />
<Menu {...appProps} appInfo={appInfo} />
{showHomeScreen.value && <HomeScreen />}
<Container {...appProps} />
{appProps.hideUI.value > 0 && (
Expand Down
53 changes: 9 additions & 44 deletions apps/pwa/src/components/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
import { HomeSection } from '@niivue/react'
import imageUrl from '/resources/pwa_install.png'

export const HomeScreen = () => (
<>
<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 p-2 px-4">
Drop Files to load images
</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
<HomeSection title="Drop Files to load images">
Drag and drop files to an empty space on this window. Many medical image and mesh formats are
supported.
</p>
</HomeSection>

<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 p-2 px-4">Install as local App</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
<HomeSection title="Install as local App">
<img
className="float-right ml-2 w-32 sm:w-40 h-auto"
src={imageUrl}
alt="Install button example"
/>
To install niivue-vscode as a local app, click the install button in the address bar.
<i> This is currently only supported in chromium-based browsers.</i>
</p>
</HomeSection>

<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 py-2 px-4">Update App</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
<HomeSection title="Update App">
The app will automatically check for updates and show a notification when a new version is
available. You can also manually force an update by pressing{' '}
<kbd className="bg-gray-600 px-1 rounded">Ctrl+Shift+R</kbd> to force refresh and clear the
cache.
</p>
</HomeSection>

<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 py-2 px-4">Bookmarklet</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
<HomeSection title="Bookmarklet">
Drag this link ⇨
<a
className="touch-manipulation"
Expand All @@ -50,36 +45,6 @@ export const HomeScreen = () => (
<a href="https://niivue.github.io/niivue-demo-images/mni152.nii.gz">
<b> MNI </b>
</a>
</p>

<h2 className="text-2xl sm:text-3xl font-bold text-gray-200 py-2 px-4">Data Privacy</h2>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
The vscode extension (or static webpage) runs locally and only accesses the images from your
machine for displaying. No data is sent or stored remotely. The extension is a complete
offline solution and does not use any online cache, storage, or network connectivity. The
extension does not track or log any user data. Local logging is minimal and only related to
hardware events.
</p>
<p className="w-full sm:w-96 mb-4 text-m font-normal text-gray-300 px-4">
The extension is an open source project depending on
<a href="https://github.com/niivue/niivue/">
<b> NiiVue</b>
</a>{' '}
and was initially developed at The University of Queensland by the Computational Imaging and
<a href="https://www.neurodesk.org/">
<b> NeuroDesk</b>
</a>{' '}
group.
</p>
<footer className="text-xs py-2 px-4">
<a
href={__GIT_REPO_URL__ ? `${__GIT_REPO_URL__}/commit/${__GIT_HASH__}` : '#'}
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 hover:underline"
>
Version: {__GIT_HASH__} (Built: {new Date(__BUILD_DATE__).toLocaleDateString()})
</a>
</footer>
</HomeSection>
</>
)
17 changes: 16 additions & 1 deletion apps/pwa/test/HomeScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import type { ComponentChildren } from 'preact'
import { render } from '@testing-library/preact'
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'

// HomeScreen composes the shared HomeSection from @niivue/react. The PWA's
// vitest config resolves that package to its built dist, so stub it here to keep
// this unit test self-contained and independent of build order (mirrors the
// desktop home-screen test).
vi.mock('@niivue/react', () => ({
HomeSection: ({ title, children }: { title: string; children: ComponentChildren }) => (
<section>
<h2>{title}</h2>
<p>{children}</p>
</section>
),
}))

import { HomeScreen } from '../src/components/HomeScreen'

describe('HomeScreen', () => {
Expand Down
11 changes: 6 additions & 5 deletions apps/pwa/tests/Document.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { BASE_URL, loadTestImage, waitForImageLoad } from './utils'
* re-import it by dropping the file.
*/

// Load one image, export the active canvas via the Save Scene button, and
// return the downloaded `.nvd` file's text (downloadNvd writes uncompressed JSON).
// Load one image, export the active canvas via the NVDocument button (its label
// click saves by default), and return the downloaded `.nvd` file's text
// (downloadNvd writes uncompressed JSON).
async function exportScene(page: Page): Promise<string> {
await page.goto(BASE_URL)
await loadTestImage(page)
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Save Scene' }).click()
await page.getByRole('button', { name: 'NVDocument' }).click()
const download = await downloadPromise
expect(download.suggestedFilename()).toMatch(/\.nvd$/)
const path = await download.path()
Expand All @@ -42,9 +43,9 @@ test.describe('NVDocument (.nvd) import/export', () => {
expect(viaFacade.blobs).toBeGreaterThan(0)
expect(viaFacade.hasOpts).toBeTruthy()

// Export via the UI (Save Scene) downloads the same embedded shape.
// Export via the UI (NVDocument label click = Save) downloads the same shape.
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Save Scene' }).click()
await page.getByRole('button', { name: 'NVDocument' }).click()
const download = await downloadPromise
expect(download.suggestedFilename()).toMatch(/\.nvd$/)
const doc = JSON.parse(await readFile((await download.path()) as string, 'utf-8'))
Expand Down
18 changes: 14 additions & 4 deletions apps/pwa/tests/Menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@ test.describe('Menu', () => {
test('displays home screen', { tag: '@dom' }, async ({ page }) => {
await page.goto(BASE_URL)

expect(await page.textContent('text=/Home/i')).toBeTruthy()
// The brand doubles as the viewer menu (Reset Viewer / About).
expect(await page.$('[data-testid="menu-brand"]')).toBeTruthy()
expect(await page.textContent('text=/Add Image/i')).toBeTruthy()
expect(await page.textContent('text=/View/i')).toBeTruthy()
expect(await page.textContent('text=/Bookmarklet/i')).toBeTruthy()
expect(await page.textContent('text=/Drop Files to load images/i')).toBeTruthy()
})

test('brand menu opens Reset Viewer and About', { tag: '@dom' }, async ({ page }) => {
await page.goto(BASE_URL)

await page.click('data-testid=menu-brand')
expect(await page.textContent('text=/Reset Viewer/i')).toBeTruthy()

await page.click('text=/About/i')
await expect(page.getByTestId('about-dialog')).toBeVisible()
expect(await page.textContent('text=/NeuroDesk/i')).toBeTruthy()
})

test('menubar updates with loading images', async ({ page }) => {
await page.goto(BASE_URL)

// initially only these menu items are visible
expect(await page.textContent('text=/Home/i')).toBeTruthy()
expect(await page.textContent('text=/Add Image/i')).toBeTruthy()
expect(await page.textContent('text=/View/i')).toBeTruthy()

Expand All @@ -33,7 +44,6 @@ test.describe('Menu', () => {
).toBeTruthy()

// after loading an image these are visible
expect(await page.textContent('text=/Home/i')).toBeTruthy()
expect(await page.textContent('text=/Add Image/i')).toBeTruthy()
expect(await page.textContent('text=/View/i')).toBeTruthy()
expect(await page.textContent('text=/ColorScale/i')).toBeTruthy()
Expand All @@ -56,7 +66,7 @@ test.describe('Menu', () => {
await page.textContent('text=/matrix size:.*voxelsize:/i'),
).toBeTruthy()

const menuBar = ['Home', 'Add Image', 'View', 'ColorScale', 'Overlay', 'Header']
const menuBar = ['Add Image', 'View', 'ColorScale', 'Overlay', 'Header']
for (const item of menuBar) {
expect(await page.textContent(`text=/${item}/i`)).toBeTruthy()
}
Expand Down
5 changes: 3 additions & 2 deletions apps/pwa/tests/MenuOverflow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ test.describe('Menu overflow', () => {
await page.setViewportSize({ width: 1280, height: 800 })
await page.goto(BASE_URL)

await expect(page.getByRole('button', { name: 'View' })).toBeVisible()
// `exact` so this doesn't also match the brand button ("niivue Viewer").
await expect(page.getByRole('button', { name: 'View', exact: true })).toBeVisible()
await expect(page.getByTestId('menu-overflow')).toHaveCount(0)
})

Expand Down Expand Up @@ -39,7 +40,7 @@ test.describe('Menu overflow', () => {
await page.setViewportSize({ width: 760, height: 800 })
await page.goto(BASE_URL)

// With no image only Home/Add Image/View/Zoom show and all fit at 760px.
// With no image only Add Image/View/Zoom show and all fit at 760px.
await expect(page.getByTestId('menu-overflow')).toHaveCount(0)

await page.click('data-testid=menu-item-dropdown-Add Image')
Expand Down
Loading