diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..343f94f --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,40 @@ +# Builds the Linux packages and the Windows installer when a version tag (v1.2.3) is pushed, +# and attaches them to a Gitea release of that tag. macOS builds need a Mac and are not made here. +name: release + +on: + push: + tags: ['v*'] + +jobs: + build: + runs-on: ubuntu-latest + # Has Wine, so the Windows installer builds on Linux + container: electronuserland/builder:wine + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm ci + - run: npm run check + - run: npm test + - run: npm run build + - run: npx electron-builder --linux --win --publish never + - name: Upload to the release + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -eu + release=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" || true) + if [ -z "$release" ]; then + release=$(curl -sf -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"mumh5 $TAG\"}" "$API/releases") + fi + id=$(printf '%s' "$release" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).id))") + for f in release/*.AppImage release/*.deb release/*.tar.gz release/*.exe; do + curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@$f" "$API/releases/$id/assets?name=$(basename "$f")" >/dev/null + echo "uploaded $(basename "$f")" + done diff --git a/README.md b/README.md index 74651d5..c3c7ae5 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ mumh5 keeps the foundation and replaces the experience. | | Mumble desktop | mumh5 | |---|---|---| -| Interface | Qt widgets, dense dialogs | Modern layout: server rail, channel tree, chat, side panels | +| Interface | Qt widgets, dense dialogs | Modern layout (server rail, channel tree, chat, side panels), or the classic and stacked layouts of the desktop client | | Several servers at the same time | One connection | Many; voice on one, text on all, unread badges | | Sharing files | Images only, pasted inline and heavily limited | Any allowed file through your own [f0ckm](#file-sharing-with-f0ckm) host, inline players for images, video and audio. Images still work without one | | YouTube links | Plain links | Click-to-play player (privacy mode), optional thumbnails | @@ -111,7 +111,18 @@ Dark, Light, Midnight (true black), Frost, and a faithful **Windows 95** theme w ## Getting started -There are no release builds yet. For now, build it yourself; it takes a few minutes. +Download a build from the [releases page](https://git.lat/kibi/mumh5/releases): + +| System | File | +|---|---| +| Windows | `mumh5--win-x64.exe` (installer; not signed yet, so Windows SmartScreen warns once) | +| Linux, any distribution | `mumh5--linux-x86_64.AppImage` (make it executable, then run it) | +| Debian, Ubuntu | `mumh5--linux-amd64.deb` | +| Linux, portable | `mumh5--linux-x64.tar.gz` | + +There is no macOS build yet; build it yourself on a Mac (below). + +### Running from source You need [Node.js](https://nodejs.org/) 22 or newer and git. @@ -132,7 +143,7 @@ npm run dist:win # NSIS installer npm run dist:mac # .dmg ``` -Installers are written to `release/`. Build each platform on that platform for the best results. +Installers are written to `release/`. The Windows installer also builds on Linux with Wine installed; the macOS one needs a Mac. Pushing a version tag (`git tag v0.1.0 && git push origin v0.1.0`) makes the Gitea workflow in `.gitea/workflows/release.yml` build the Linux and Windows files and attach them to a release. --- diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..fa5ef81 Binary files /dev/null and b/build/icon.png differ diff --git a/build/icon.svg b/build/icon.svg new file mode 100644 index 0000000..cb3707d --- /dev/null +++ b/build/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/electron/main.ts b/electron/main.ts index 3130eaf..f3aba65 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -136,6 +136,8 @@ function createWindow() { minHeight: 480, backgroundColor: '#111214', title: 'mumh5', + // Window and taskbar icon on Linux (Windows and macOS take it from the package) + icon: path.join(__dirname, '../dist/icon.png'), autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, 'preload.cjs'), diff --git a/package.json b/package.json index 51e3272..890a4fd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mumh5", "productName": "mumh5", "version": "0.1.0", - "description": "Modern Mumble client with rich chat, file sharing and video", + "description": "A modern Mumble client: Discord-like interface, file sharing, voice and more, for any Mumble server", "license": "MIT", "type": "module", "main": "dist-electron/main.cjs", @@ -26,24 +26,37 @@ "files": [ "dist/**", "dist-electron/**", + "!dist-electron/*.map", "package.json", "LICENSE", "THIRD_PARTY_NOTICES.md" ], "directories": { - "output": "release" + "output": "release", + "buildResources": "build" }, "linux": { "target": [ "AppImage", - "deb" + "deb", + "tar.gz" ], - "category": "Network" + "category": "Network", + "executableName": "mumh5", + "synopsis": "Modern Mumble client", + "desktop": { + "entry": { + "StartupWMClass": "mumh5", + "Keywords": "mumble;voice;chat;voip;" + } + }, + "syncDesktopName": true }, "win": { "target": [ "nsis" - ] + ], + "icon": "build/icon.png" }, "mac": { "target": [ @@ -54,6 +67,22 @@ "NSMicrophoneUsageDescription": "mumh5 uses the microphone for voice chat.", "NSCameraUsageDescription": "mumh5 uses the camera for video chat." } + }, + "icon": "build/icon.png", + "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", + "deb": { + "depends": [ + "libnotify4", + "libxtst6", + "libnss3", + "libasound2 | libasound2t64" + ] + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "perMachine": false, + "shortcutName": "mumh5" } }, "devDependencies": { @@ -81,9 +110,14 @@ "esbuild@0.28.2": true, "electron-winstaller": false }, - "author": "Kibi Kelburton", + "author": { + "name": "Kibi Kelburton", + "email": "git@f0ck.it" + }, "repository": { "type": "git", "url": "gitea@git.lat:kibi/mumh5.git" - } + }, + "homepage": "https://git.lat/kibi/mumh5", + "desktopName": "mumh5.desktop" } diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..5b7f54d Binary files /dev/null and b/public/icon.png differ diff --git a/src/App.svelte b/src/App.svelte index ebe0237..ac110f2 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -14,6 +14,7 @@ import UserInfoDialog from './ui/UserInfoDialog.svelte'; import ChannelDialog from './ui/ChannelDialog.svelte'; import PublicServers from './ui/PublicServers.svelte'; + import ResizeHandle from './ui/ResizeHandle.svelte'; import { identities } from './lib/identities.svelte.ts'; import { session } from './lib/session.svelte.ts'; import { ui } from './lib/ui.svelte.ts'; @@ -23,6 +24,13 @@ // wide: rail + sidebar + chat + panel; medium: panel as an overlay; narrow: everything but chat in drawers const layout = $derived(width >= 1100 ? 'wide' : width >= 760 ? 'medium' : 'narrow'); + // Arrangement (Appearance setting): side is the default, classic puts the channel list right + // of the chat, stacked above it. Phones always use drawers. + const arrangement = $derived(layout === 'narrow' ? 'side' : store.settings.arrangement ?? 'side'); + // The member panel sits inline only in the side arrangement on wide windows, otherwise it overlays + const panelInline = $derived(layout === 'wide' && arrangement === 'side'); + let treeHeight = $state(Math.max(120, Number((() => { try { return localStorage.getItem('mumh5.treeHeight'); } catch { return null; } })()) || 280)); + let navOpen = $state(false); let editing = $state<{ server: SavedServer | null } | null>(null); @@ -35,12 +43,13 @@ else delete document.documentElement.dataset.theme; }); - // The panel starts open on wide layouts only + // The panel starts open only where it sits inline let lastLayout = ''; $effect(() => { - if (layout === lastLayout) return; - ui.panelOpen = layout === 'wide'; - lastLayout = layout; + const key = `${layout}/${arrangement}`; + if (key === lastLayout) return; + ui.panelOpen = panelInline; + lastLayout = key; }); // On phones one drawer at a time $effect(() => { if (layout === 'narrow' && ui.panelOpen) navOpen = false; }); @@ -53,26 +62,48 @@ const closeOverlays = () => { navOpen = false; - if (layout !== 'wide') ui.panelOpen = false; + if (!panelInline) ui.panelOpen = false; }; -
- {#if layout !== 'narrow' || navOpen} -