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
1 change: 1 addition & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
and (.kinds | sort) == (["bar-widget", "overlay"] | sort)
and .entryPoints.barWidget == "BarWidget.qml"
and .entryPoints.overlay == "StationPicker.qml"
and .barWidget.defaultSection == "left"
' manifest.json
test -x subwave-fetch
test -x subwave-player
Expand Down
17 changes: 13 additions & 4 deletions BarWidget.qml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ BarWidget {
readonly property string label: trackTitle
? trackTitle + (trackArtist ? " · " + trackArtist : "")
: stationName
readonly property bool tooltipHovered: interactionArea.containsMouse
readonly property string tooltipText: StationModel.barTooltip({
running: playerRunning,
paused: playerPaused,
stationName: stationName,
trackTitle: trackTitle,
trackArtist: trackArtist,
volume: playerVolume
})

onTooltipTextChanged: if (tooltipHovered && bar) bar.showTooltip(root, tooltipText)

function applyStatus(raw) {
try {
Expand Down Expand Up @@ -134,6 +145,7 @@ BarWidget {
}

MouseArea {
id: interactionArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
Expand All @@ -144,10 +156,7 @@ BarWidget {
else root.toggleOverlay()
}
onWheel: function(wheel) { root.changeVolume(wheel.angleDelta.y) }
onEntered: if (root.bar) root.bar.showTooltip(root,
(root.playerRunning ? (root.playerPaused ? "Paused · " : "Playing · ") : "Open · ")
+ root.stationName + (root.trackTitle ? "\n" + root.label : "")
+ " · " + root.playerVolume + "%")
onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText)
onExited: if (root.bar) root.bar.hideTooltip(root)
}
}
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ that works with Omarchy's existing media controls.

![SUB/WAVE station picker in Omarchy](preview.png)

## What is SUB/WAVE?

SUB/WAVE is a self-hosted shared internet radio station: every listener hears
the same live broadcast. An AI DJ selects music from the station's library and
speaks between tracks, while the operator controls its sound, schedule,
personas, and listener requests. This Omarchy plugin lets you tune into your own
station or discover other public SUB/WAVE stations.

## Install

```bash
Expand Down Expand Up @@ -54,6 +62,7 @@ fragment. The plugin derives the API and stream paths from the origin.
| Type | Search names, places, genres, operators, and descriptions |
| Up / Down | Move through stations |
| Enter | Play the selected station |
| ↗ button | Open that station in its web player without interrupting playback |
| Escape | Clear search, then close |

Selecting the station that is already playing closes the picker. Playback uses
Expand Down
16 changes: 16 additions & 0 deletions StationModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ function configuredStationUpdate(value) {
}
}

function barTooltip(value) {
var state = value && typeof value === "object" ? value : ({})
var station = singleLine(state.stationName, 160) || "SUB/WAVE"
var title = singleLine(state.trackTitle, MAX_FIELD)
var artist = singleLine(state.trackArtist, MAX_FIELD)
var volume = Math.max(0, Math.min(100, Math.round(Number(state.volume) || 0)))
var status = state.running === true ? (state.paused === true ? "Paused" : "Playing") : "Open"
var details = station + " · " + status + " · " + volume + "%"
return title ? title + (artist ? " — " + artist : "") + "\n" + details : station + "\n" + status + " · " + volume + "%"
}

function webPlayerCommand(value) {
var origin = normalizeOrigin(value)
return origin ? ["omarchy", "launch", "browser", origin] : []
}

function fallbackSlug(name) {
return singleLine(name, 80).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 49)
}
Expand Down
27 changes: 25 additions & 2 deletions StationPicker.qml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ Item {
playProcess.running = true
}

function openWebPlayer(url) {
var command = StationModel.webPlayerCommand(url)
if (command.length) Quickshell.execDetached(command)
}

function queueVisibleProbes() {
var next = []
var limit = Math.min(24, visibleStations.count)
Expand Down Expand Up @@ -541,8 +546,8 @@ Item {

Text {
id: status
anchors.right: parent.right
anchors.rightMargin: Style.space(14)
anchors.right: webPlayerButton.left
anchors.rightMargin: Style.space(10)
anchors.verticalCenter: parent.verticalCenter
text: root.playingUrl === url ? "PLAYING" : (!live ? "CHECKING" : (live.online ? "ON AIR" : "OFFLINE"))
color: live && live.online ? root.accent : root.foreground
Expand All @@ -551,6 +556,24 @@ Item {
font.pixelSize: Style.font.caption
font.bold: true
}

Button {
id: webPlayerButton
z: 1
anchors.right: parent.right
anchors.rightMargin: Style.space(8)
anchors.verticalCenter: parent.verticalCenter
text: "↗"
tooltipText: "Open web player"
focusable: true
foreground: root.foreground
accent: root.accent
fontFamily: Style.font.menuFamily
fontSize: Style.font.body
horizontalPadding: Style.spacing.controlGap
verticalPadding: Style.spacing.labelGap
onClicked: root.openWebPlayer(url)
}
}

Text {
Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"version": "0.1.0",
"author": "SUB/WAVE",
"license": "MIT",
"description": "Listen to your SUB/WAVE station and discover community stations from the Omarchy bar.",
"description": "Tune into self-hosted AI DJ radio and discover public SUB/WAVE community stations.",
"homepage": "https://github.com/getsubwave/omarchy-subwave",
"repository": "https://github.com/getsubwave/omarchy-subwave",
"keywords": ["radio", "subwave", "music", "mpris"],
"keywords": ["radio", "subwave", "music", "mpris", "ai", "self-hosted"],
"kinds": ["overlay", "bar-widget"],
"keepLoaded": true,
"entryPoints": {
Expand Down
Binary file modified preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions tests/model.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ assert.deepEqual(
assert.equal(model.configuredStationUpdate("https://user:pass@radio.example.com").ok, false)
assert.equal(model.configuredStationUpdate("file:///tmp/stream").ok, false)

assert.equal(typeof model.barTooltip, "function")
assert.equal(model.barTooltip({
running: true,
paused: false,
stationName: "ChillWave",
trackTitle: "Wish We Had History",
trackArtist: "Bexy",
volume: 65
}), "Wish We Had History — Bexy\nChillWave · Playing · 65%")
assert.equal(model.barTooltip({
running: false,
stationName: "SUB/WAVE",
volume: 70
}), "SUB/WAVE\nOpen · 70%")
assert.equal(model.barTooltip({
running: true,
paused: true,
stationName: "Chill\nWave",
trackTitle: "Track\nName",
volume: 101
}), "Track Name\nChill Wave · Paused · 100%")

assert.equal(typeof model.webPlayerCommand, "function")
assert.deepEqual(
Array.from(model.webPlayerCommand("https://radio.example.com/listen")),
["omarchy", "launch", "browser", "https://radio.example.com"]
)
assert.deepEqual(Array.from(model.webPlayerCommand("https://user:pass@radio.example.com")), [])
assert.deepEqual(Array.from(model.webPlayerCommand("javascript:alert(1)")), [])

const rows = model.normalizeCatalog([
{ slug: "zeta", name: "Zeta", url: "https://zeta.example", genre: "Jazz" },
{ slug: "featured", name: "Featured", url: "https://featured.example", featured: true },
Expand Down
Loading