Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5f49149
feat(linux): add Avalonia MPRIS desktop app
aodjo Aug 5, 2026
047f708
ci(linux): publish x64 and arm64 app archives
aodjo Aug 5, 2026
333647f
feat(linux): talk to MPRIS over D-Bus instead of playerctl
aodjo Sep 5, 2026
365343d
fix(macos): draw the menu bar item in the menu bar's label colour
aodjo Sep 5, 2026
68daec2
test(macos): wait for the full session list before snapshotting
aodjo Sep 5, 2026
8b162f7
docs: add API documentation comments across Core, Desktop, and macOS
aodjo Sep 5, 2026
e3c12be
chore: ignore CLAUDE.md
aodjo Sep 5, 2026
3a68274
feat(linux): add MPRIS seek and volume control
aodjo Sep 5, 2026
5f55f91
feat(linux): port the macOS player panel to Avalonia
aodjo Sep 5, 2026
de6d285
feat(core): add synced lyrics service
aodjo Sep 6, 2026
061f7ba
feat(linux): show the track title beside the tray icon
aodjo Sep 6, 2026
574c814
feat(linux): show synced lyrics in the panel
aodjo Sep 6, 2026
d2c6be2
fix(linux): square the panel only when the desktop cannot composite
aodjo Sep 6, 2026
af6abfb
feat(linux): add the settings window from macOS
aodjo Sep 6, 2026
659879a
feat(linux): open the panel on a tray click and set the UI in Pretendard
aodjo Sep 6, 2026
e89dfee
fix(linux): rebuild the settings tab strip and paint it from our own …
aodjo Sep 6, 2026
cdf26f7
fix(linux): keep the tray label to the width the settings ask for
aodjo Sep 6, 2026
33b1f63
fix(linux): stop cutting the tray label to imitate a marquee
aodjo Sep 6, 2026
c07f823
feat(linux): draw the top bar entry from a GNOME extension
aodjo Sep 6, 2026
d48e52c
feat(linux): ship a Debian package with the GNOME extension
aodjo Sep 6, 2026
7df6de5
feat(linux): enable the GNOME extension without asking the user
aodjo Sep 6, 2026
a192ee9
fix(linux): leave the top bar marquee to surfaces Reprise draws
aodjo Sep 6, 2026
94f6d49
fix(linux): make the top bar title actually slide
aodjo Sep 6, 2026
9d29698
fix(linux): stamp each package build so apt upgrades it
aodjo Sep 6, 2026
c4e13e8
feat(linux): build an AppImage that runs without installing
aodjo Sep 6, 2026
adc2d53
feat(linux): publish an apt repository to GitHub Pages
aodjo Sep 6, 2026
85aef70
ci(linux): publish the apt repository from develop too
aodjo Sep 6, 2026
149d6f9
ci(linux): keep stable and develop as separate apt channels
aodjo Sep 6, 2026
3677e6e
fix(desktop): slide the lyric sheet on the macOS spring
aodjo Sep 6, 2026
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
201 changes: 201 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
- src/Reprise.Core/**
- src/Reprise.Desktop/**
- src/Reprise.Platform.Linux/**
- BrowserExtension/Chromium/icons/icon32.png
- BrowserExtension/Chromium/icons/icon128.png
- tests/unit/**
- tests/contracts/**
- tests/integration/linux/**
Expand All @@ -26,6 +28,8 @@ on:
- src/Reprise.Core/**
- src/Reprise.Desktop/**
- src/Reprise.Platform.Linux/**
- BrowserExtension/Chromium/icons/icon32.png
- BrowserExtension/Chromium/icons/icon128.png
- tests/unit/**
- tests/contracts/**
- tests/integration/linux/**
Expand Down Expand Up @@ -174,6 +178,33 @@ jobs:
with:
dotnet-version: 10.0.x

- name: Restore Linux application
if: steps.discover.outputs.ready == 'true'
shell: bash
run: |
dotnet restore \
src/Reprise.Platform.Linux/Reprise.Platform.Linux.csproj \
--runtime '${{ matrix.runtime }}'

- name: Publish Linux application
if: steps.discover.outputs.ready == 'true'
shell: bash
run: |
publish_directory="$GITHUB_WORKSPACE/Artifacts/${{ matrix.target }}/app"
dotnet publish \
src/Reprise.Platform.Linux/Reprise.Platform.Linux.csproj \
--configuration Release \
--runtime '${{ matrix.runtime }}' \
--self-contained true \
--no-restore \
--output "$publish_directory"

- name: Smoke test Linux executable
if: steps.discover.outputs.ready == 'true'
shell: bash
run: |
'Artifacts/${{ matrix.target }}/app/reprise' --health-check

- name: Run applicable tests
if: steps.discover.outputs.ready == 'true'
shell: bash
Expand Down Expand Up @@ -206,6 +237,81 @@ jobs:
--logger "trx;LogFileName=${project_name}.trx"
done

- name: Package Linux application
if: steps.discover.outputs.ready == 'true'
shell: bash
run: |
artifact_directory="Artifacts/${{ matrix.target }}"
cp packaging/linux/dev.junx.Reprise.desktop "$artifact_directory/"
cp packaging/linux/README.md "$artifact_directory/"
cp packaging/linux/install-gnome-extension.sh "$artifact_directory/"
cp -r packaging/linux/gnome-extension "$artifact_directory/"
tar \
-C Artifacts \
-czf "Artifacts/reprise-${{ matrix.runtime }}.tar.gz" \
'${{ matrix.target }}'

- name: Build Debian package
if: steps.discover.outputs.ready == 'true'
shell: bash
run: |
case '${{ matrix.architecture }}' in
x64) debian_architecture=amd64 ;;
arm64) debian_architecture=arm64 ;;
*) echo 'unsupported architecture' >&2; exit 1 ;;
esac

version="$(grep -oPm1 '(?<=<VersionPrefix>)[^<]+' Directory.Build.props)"
suffix="$(grep -oPm1 '(?<=<VersionSuffix>)[^<]+' Directory.Build.props || true)"
if [[ -n "$suffix" ]]; then
version="$version~$suffix"
fi

packaging/linux/build-deb.sh \
"Artifacts/${{ matrix.target }}/app" \
"$debian_architecture" \
"$version" \
Artifacts

ls Artifacts/*.deb

- name: Build AppImage
if: steps.discover.outputs.ready == 'true'
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq squashfs-tools

case '${{ matrix.architecture }}' in
x64) appimage_architecture=x86_64 ;;
arm64) appimage_architecture=aarch64 ;;
*) echo 'unsupported architecture' >&2; exit 1 ;;
esac

version="$(grep -oPm1 '(?<=<VersionPrefix>)[^<]+' Directory.Build.props)"
suffix="$(grep -oPm1 '(?<=<VersionSuffix>)[^<]+' Directory.Build.props || true)"
if [[ -n "$suffix" ]]; then
version="$version-$suffix"
fi

packaging/linux/build-appimage.sh \
"Artifacts/${{ matrix.target }}/app" \
"$appimage_architecture" \
"$version" \
Artifacts

- name: Upload Linux application
if: steps.discover.outputs.ready == 'true'
uses: actions/upload-artifact@v7
with:
name: reprise-${{ matrix.runtime }}
path: |
Artifacts/reprise-${{ matrix.runtime }}.tar.gz
Artifacts/*.deb
Artifacts/*.AppImage
if-no-files-found: error
retention-days: 7

- name: Upload Linux test results
if: ${{ always() && steps.discover.outputs.ready == 'true' }}
uses: actions/upload-artifact@v7
Expand All @@ -214,3 +320,98 @@ jobs:
path: TestResults/${{ matrix.target }}
if-no-files-found: ignore
retention-days: 7

publish-repository:
name: Publish the apt repository
needs: platform-tests
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
pages: write
id-token: write
concurrency:
group: apt-repository
cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Download built packages
uses: actions/download-artifact@v6
with:
pattern: reprise-linux-*
path: Downloads

- name: Install packaging tools
shell: bash
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq dpkg-dev apt-utils

- name: Configure Pages
id: pages
uses: actions/configure-pages@v5

- name: Fetch the published site
shell: bash
run: |
# The site holds both channels, and each branch rewrites only its
# own, so the run has to start from what is already published
# rather than from an empty directory.
if ! git clone --branch apt-repo --depth 1 \
"https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" Site; then
mkdir Site
git -C Site init -q -b apt-repo
git -C Site remote add origin \
"https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git"
fi

- name: Update this channel
shell: bash
run: |
if [[ '${{ github.ref }}' == 'refs/heads/main' ]]; then
suite=stable
else
suite=develop
fi
echo "Publishing to the $suite channel."

packaging/linux/build-apt-repo.sh \
Downloads \
Site \
"${{ steps.pages.outputs.base_url }}" \
"$suite"

# The AppImages ride along, one directory per channel.
mkdir -p "Site/downloads/$suite"
rm -f "Site/downloads/$suite"/*.AppImage
find Downloads -name '*.AppImage' -exec cp {} "Site/downloads/$suite/" \;
find Site -maxdepth 3 -type d | sort

- name: Commit the site
shell: bash
run: |
cd Site
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add -A
if git diff --cached --quiet; then
echo 'Nothing changed.'
else
git commit -q -m "chore: publish ${{ github.ref_name }} build ${{ github.run_number }}"
git push -q origin HEAD:apt-repo
fi

- name: Upload the site
uses: actions/upload-pages-artifact@v4
with:
path: Site

- name: Deploy to Pages
id: deploy
uses: actions/deploy-pages@v4
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@ xcuserdata/
.build/
.swiftpm/

# .NET
**/bin/
**/obj/
TestResults/
Artifacts/

# Local editor and tool state
.idea/
.vscode/
.codex/
*.swp
*.swo
*~
CLAUDE.md

# Local configuration and secrets
.env
Expand Down
12 changes: 12 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>alpha.1</VersionSuffix>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
</Project>
14 changes: 14 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
</Project>
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ open src/Reprise.Platform.MacOS/Reprise.xcodeproj

Build and run the `Reprise` scheme in Xcode 16 or later.

### Linux alpha

The Linux implementation is under active development on `develop`. It uses
Avalonia for the desktop and reads local MPRIS media sessions straight from the
D-Bus session bus, so it needs no extra packages. The panel is the same one
macOS shows: album art, a scrolling title, transport controls, a scrubber,
volume, and the White, Dark, Liquid, and System themes.

Add the apt repository once, and later builds arrive with your usual
updates:

```bash
echo "deb [trusted=yes] https://aodjo.github.io/Reprise stable main" | sudo tee /etc/apt/sources.list.d/reprise.list
sudo apt update && sudo apt install reprise
```

There is also an AppImage on that page for running Reprise without
installing anything, and the source builds directly:

```bash
dotnet run --project src/Reprise.Platform.Linux/Reprise.Platform.Linux.csproj
```

One click on the tray icon opens the panel, which hides again when it loses
focus, like the macOS popover. On panels that support the Ubuntu tray label
extension, the track title or the current lyric line appears beside the icon,
as it does in the macOS menu bar. On GNOME a small shell extension, installed
and switched on by the package, draws that entry itself and scrolls the title
smoothly. The interface is set in Pretendard, bundled
with the application. Linux CI also publishes self-contained `linux-x64` and
`linux-arm64` archives.

## Usage

Click the Reprise icon in your menu bar to open the player panel.
Expand Down
8 changes: 8 additions & 0 deletions Reprise.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Solution>
<Project Path="src/Reprise.Core/Reprise.Core.csproj" />
<Project Path="src/Reprise.Desktop/Reprise.Desktop.csproj" />
<Project Path="src/Reprise.Platform.Linux/Reprise.Platform.Linux.csproj" />
<Project Path="tests/unit/Reprise.Core.Tests/Reprise.Core.Tests.csproj" />
<Project Path="tests/unit/Reprise.Desktop.Tests/Reprise.Desktop.Tests.csproj" />
<Project Path="tests/integration/linux/common/Reprise.Platform.Linux.Tests.csproj" />
</Solution>
4 changes: 2 additions & 2 deletions docs/CROSS_PLATFORM.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ targets for browser-specific manifests and generated artifacts.

## Branch and version policy

- `main` remains the single integration branch.
- `migration/cross-platform` contains the initial migration scaffold.
- `main` is the release branch and `develop` is the integration branch.
- The initial migration scaffold has been merged into `develop`.
- New work uses short-lived `feature/*` and `fix/*` branches.
- Creating the scaffold does not change the application version.
- The first public release of the unified cross-platform application will be
Expand Down
7 changes: 7 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "10.0.301",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
1 change: 0 additions & 1 deletion packaging/linux/.gitkeep

This file was deleted.

Loading