Browse Source

create Electron app

imwald
Silberengel 1 month ago
parent
commit
6bfef27e04
  1. 1
      .gitignore
  2. 51
      electron/main.cjs
  3. 7
      electron/preload.cjs
  4. 2
      eslint.config.js
  5. 4689
      package-lock.json
  6. 31
      package.json
  7. 43
      src/components/Sidebar/DownloadDesktopSidebarButton.tsx
  8. 2
      src/components/Sidebar/index.tsx
  9. 4
      src/components/StartupSessionBanner.tsx
  10. 7
      src/constants.ts
  11. 1
      src/i18n/locales/de.ts
  12. 1
      src/i18n/locales/en.ts
  13. 7
      src/main.tsx
  14. 2
      src/vite-env.d.ts

1
.gitignore vendored

@ -11,6 +11,7 @@ node_modules @@ -11,6 +11,7 @@ node_modules
.npm-cache
dist
dist-ssr
release
dev-dist
*.local
.env

51
electron/main.cjs

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
'use strict'
const { app, BrowserWindow, shell } = require('electron')
const path = require('path')
/** True when running from source (`electron .`); false when packaged. */
const isDev = !app.isPackaged
function createWindow() {
const win = new BrowserWindow({
width: 1280,
height: 840,
minWidth: 400,
minHeight: 500,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
})
win.once('ready-to-show', () => win.show())
if (isDev) {
const devUrl = process.env.VITE_DEV_SERVER_URL || 'http://127.0.0.1:5173'
win.loadURL(devUrl)
win.webContents.openDevTools({ mode: 'detach' })
} else {
win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'))
}
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('http:') || url.startsWith('https:')) {
void shell.openExternal(url)
}
return { action: 'deny' }
})
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})

7
electron/preload.cjs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
'use strict'
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('jumbleElectron', {
isElectron: true
})

2
eslint.config.js

@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh' @@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist', 'dev-dist', 'node_modules', '**/*.refactored.ts'] },
{ ignores: ['dist', 'dev-dist', 'node_modules', 'release', 'electron', '**/*.refactored.ts'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],

4689
package-lock.json generated

File diff suppressed because it is too large Load Diff

31
package.json

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true,
"type": "module",
"main": "electron/main.cjs",
"author": "Silberengel",
"license": "MIT",
"repository": {
@ -20,7 +21,10 @@ @@ -20,7 +21,10 @@
"test": "vitest",
"test:run": "vitest run",
"i18n:sync": "npx tsx scripts/sync-i18n-locales.ts && prettier --write \"src/i18n/locales/*.ts\"",
"i18n:audit": "npx tsx scripts/i18n-audit.ts"
"i18n:audit": "npx tsx scripts/i18n-audit.ts",
"electron:dev": "concurrently -k -n vite,electron -c blue,green \"vite --host\" \"wait-on http://127.0.0.1:5173 && cross-env NODE_ENV=development electron .\"",
"build:electron": "tsc -b && vite build --base ./",
"electron:pack": "npm run build:electron && electron-builder"
},
"dependencies": {
"@asciidoctor/core": "^3.0.4",
@ -118,7 +122,30 @@ @@ -118,7 +122,30 @@
"typescript-eslint": "^8.18.1",
"vite": "^6.0.3",
"vite-plugin-pwa": "^0.21.1",
"vitest": "^4.0.8"
"vitest": "^4.0.8",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"electron": "^34.2.0",
"electron-builder": "^25.1.8",
"wait-on": "^8.0.1"
},
"build": {
"appId": "eu.imwald.jumble",
"productName": "Jumble",
"copyright": "Copyright © Silberengel",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"electron/**/*",
"package.json"
],
"linux": {
"target": ["AppImage", "deb"],
"category": "Network",
"maintainer": "Silberengel"
}
},
"overrides": {
"sucrase": {

43
src/components/Sidebar/DownloadDesktopSidebarButton.tsx

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
import { Button } from '@/components/ui/button'
import { DESKTOP_APP_DOWNLOAD_URL_DEFAULT } from '@/constants'
import { cn } from '@/lib/utils'
import { Download } from 'lucide-react'
import { useTranslation } from 'react-i18next'
function resolveDesktopDownloadUrl(): string | null {
if (typeof window === 'undefined') return DESKTOP_APP_DOWNLOAD_URL_DEFAULT
const fromConfig = window.__RUNTIME_CONFIG__?.DESKTOP_DOWNLOAD_URL
if (fromConfig !== undefined) {
const trimmed = fromConfig.trim()
return trimmed.length > 0 ? trimmed : null
}
return DESKTOP_APP_DOWNLOAD_URL_DEFAULT
}
/** Bottom-of-sidebar link to native (Electron) builds; hidden in the packaged app and when URL is disabled. */
export default function DownloadDesktopSidebarButton() {
const { t } = useTranslation()
if (typeof window !== 'undefined' && window.jumbleElectron?.isElectron) {
return null
}
const href = resolveDesktopDownloadUrl()
if (!href) return null
return (
<Button
variant="ghost"
className={cn(
'flex shadow-none items-center transition-colors duration-500 bg-transparent w-12 h-12 xl:w-full xl:h-auto xl:min-w-0 p-3 m-0 xl:py-2 xl:pl-3 xl:pr-4 rounded-lg xl:justify-start gap-3 text-lg font-semibold [&_svg]:size-full xl:[&_svg]:size-4 xl:[&_svg]:shrink-0',
'text-muted-foreground hover:text-foreground'
)}
asChild
>
<a href={href} target="_blank" rel="noopener noreferrer" title={t('downloadDesktopApp')}>
<Download strokeWidth={2.5} aria-hidden />
<div className="max-xl:hidden min-w-0 flex-1 text-left break-words leading-snug pr-0.5">
{t('downloadDesktopApp')}
</div>
</a>
</Button>
)
}

2
src/components/Sidebar/index.tsx

@ -11,6 +11,7 @@ import RssButton from './RssButton' @@ -11,6 +11,7 @@ import RssButton from './RssButton'
import SearchButton from './SearchButton'
import SpellsButton from './SpellsButton'
import PaneModeToggle from './PaneModeToggle'
import DownloadDesktopSidebarButton from './DownloadDesktopSidebarButton'
export default function PrimaryPageSidebar() {
const { isSmallScreen } = useScreenSize()
@ -40,6 +41,7 @@ export default function PrimaryPageSidebar() { @@ -40,6 +41,7 @@ export default function PrimaryPageSidebar() {
<div className="space-y-2">
<HelpAndAccountMenu variant="sidebar" />
<PaneModeToggle />
<DownloadDesktopSidebarButton />
</div>
</div>
)

4
src/components/StartupSessionBanner.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { useNostr } from '@/providers/NostrProvider'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
import { Loader2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -36,7 +36,7 @@ export default function StartupSessionBanner() { @@ -36,7 +36,7 @@ export default function StartupSessionBanner() {
'bg-background px-3 py-2 text-center text-sm text-muted-foreground'
)}
>
<Skeleton className="size-4 shrink-0 rounded-sm" aria-hidden />
<Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground" aria-hidden />
<span>
{t('startupSessionHydrating', {
defaultValue: 'Syncing your relays and profile from the network…'

7
src/constants.ts

@ -8,6 +8,13 @@ export const JUMBLE_API_BASE_URL = @@ -8,6 +8,13 @@ export const JUMBLE_API_BASE_URL =
export const HIVETALK_BASE_URL =
(import.meta.env.VITE_HIVETALK_BASE_URL as string | undefined) ?? 'https://vanilla.hivetalk.org'
/**
* Default URL for the sidebar Download desktop app entry (e.g. GitHub Releases with AppImage/deb).
* Override per deploy with `DESKTOP_DOWNLOAD_URL` in `/config.json` (empty string hides the entry).
*/
export const DESKTOP_APP_DOWNLOAD_URL_DEFAULT =
'https://github.com/Silberengel/jumble/releases'
export const DEFAULT_FAVORITE_RELAYS = [
'wss://theforest.nostr1.com',
'wss://orly-relay.imwald.eu',

1
src/i18n/locales/de.ts

@ -87,6 +87,7 @@ export default { @@ -87,6 +87,7 @@ export default {
"username's used relays": '{{username}}s verwendete Relays',
"username's muted": '{{username}}s stummgeschaltet',
Login: 'Anmelden',
downloadDesktopApp: 'App herunterladen',
'Please log in to view notifications.': 'Please log in to view notifications.',
'Follows you': 'Folgt dir',
'Relay Settings': 'Relay-Einstellungen',

1
src/i18n/locales/en.ts

@ -84,6 +84,7 @@ export default { @@ -84,6 +84,7 @@ export default {
"username's used relays": "{{username}}'s used relays",
"username's muted": "{{username}}'s muted",
Login: 'Login',
downloadDesktopApp: 'Download app',
'Please log in to view notifications.': 'Please log in to view notifications.',
'Follows you': 'Follows you',
'Relay Settings': 'Relays and Storage Settings',

7
src/main.tsx

@ -13,7 +13,7 @@ import storage from './services/local-storage.service' @@ -13,7 +13,7 @@ import storage from './services/local-storage.service'
declare global {
interface Window {
__RUNTIME_CONFIG__?: { NIP66_MONITOR_NPUB?: string }
__RUNTIME_CONFIG__?: { NIP66_MONITOR_NPUB?: string; DESKTOP_DOWNLOAD_URL?: string }
}
}
@ -36,7 +36,10 @@ async function bootstrap() { @@ -36,7 +36,10 @@ async function bootstrap() {
try {
const r = await fetch('/config.json')
if (r.ok) {
window.__RUNTIME_CONFIG__ = (await r.json()) as { NIP66_MONITOR_NPUB?: string }
window.__RUNTIME_CONFIG__ = (await r.json()) as {
NIP66_MONITOR_NPUB?: string
DESKTOP_DOWNLOAD_URL?: string
}
}
} catch {
window.__RUNTIME_CONFIG__ = {}

2
src/vite-env.d.ts vendored

@ -9,5 +9,7 @@ declare module '*.md?raw' { @@ -9,5 +9,7 @@ declare module '*.md?raw' {
declare global {
interface Window {
nostr?: TNip07
/** Set by {@link electron/preload.cjs} when running inside Electron. */
jumbleElectron?: { isElectron: true }
}
}

Loading…
Cancel
Save