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
42 changes: 42 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Report a problem
description: Something in Roku Control doesn't work the way it should.
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time — every report makes the app better for everyone.

**Only volume works?** That's your Roku's *Limited* mode. On the Roku open
**Settings › System › Advanced system settings › Control by mobile apps** and choose **Enabled**.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: What did you try, what did you expect, and what happened instead?
placeholder: I pressed Home and the app said "Can't reach the Roku"…
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1. Open Roku Control
2. Pick my living room TV
3. …
- type: textarea
id: environment
attributes:
label: Your setup
description: >-
Filled in for you when you use "Report a problem" in the app. It lists your Roku's model and
software version — never its IP address, name or serial number.
render: text
- type: input
id: version
attributes:
label: App version
description: Shown at the bottom of the app.
placeholder: "2.1.0"
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: MoveWeight.com
url: https://moveweight.com
about: The people who make Roku Control.
26 changes: 26 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Suggest an idea
description: A feature, a button, a shortcut — anything that would make Roku Control better for you.
title: "[Idea]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Roku Control is free, open source and ad-free. Ideas from people who use it decide what gets built next — thank you!
- type: textarea
id: idea
attributes:
label: Your idea
description: What would you like the app to do?
validations:
required: true
- type: textarea
id: why
attributes:
label: Why it would help
description: What are you trying to do today, and what gets in the way?
- type: input
id: version
attributes:
label: App version
placeholder: "2.1.0"
191 changes: 191 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Build installers

# Runs by hand from the Actions tab (optionally publishing a release and
# submitting to the Microsoft Store), on a version tag (v2.1.0), or on a pull
# request labelled "build-installers" (a downloadable preview for testing).
on:
workflow_dispatch:
inputs:
release:
description: Publish a GitHub release with this tag (e.g. v2.1.0). Leave empty to only build.
required: false
default: ''
store:
description: Also submit the MSIX to the Microsoft Store (needs the PARTNER_CENTER_* secrets)
type: boolean
default: false
push:
tags: ['v*']
pull_request:
types: [labeled, synchronize, reopened]

permissions:
contents: read

concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: ${{ matrix.os }}
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'build-installers')
strategy:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-22.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install Tauri system libraries
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev libayatana-appindicator3-dev patchelf

- name: Install Rust
run: |
rustup toolchain install stable --profile minimal
rustup default stable

- run: npm ci

# tauri:build builds the frontend first (beforeBuildCommand), so the
# installer never ships an empty window.
- run: npm run tauri:build

# The Microsoft Store listing is an MSIX product: package the same exe
# with msix/AppxManifest.xml using the Windows SDK's makeappx. Partner
# Center signs it on upload.
- name: Package for the Microsoft Store (MSIX)
if: runner.os == 'Windows'
shell: pwsh
run: |
$makeappx = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\makeappx.exe" |
Sort-Object { [version]$_.Directory.Parent.Name } -Descending | Select-Object -First 1
if (-not $makeappx) { throw 'makeappx.exe not found in the Windows SDK' }
$version = (Select-Xml -Path msix/AppxManifest.xml -Namespace @{ m = 'http://schemas.microsoft.com/appx/manifest/foundation/windows10' } -XPath '/m:Package/m:Identity').Node.Version
$stage = Join-Path $env:RUNNER_TEMP 'msix'
New-Item -ItemType Directory -Force (Join-Path $stage 'Assets') | Out-Null
Copy-Item src-tauri/target/release/roku-control.exe $stage
Copy-Item msix/AppxManifest.xml $stage
Copy-Item msix/Assets/* (Join-Path $stage 'Assets')
New-Item -ItemType Directory -Force src-tauri/target/release/bundle/msix | Out-Null
& $makeappx.FullName pack /d $stage /p "src-tauri/target/release/bundle/msix/RokuControl_${version}_x64.msix" /o
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- uses: actions/upload-artifact@v4
with:
name: roku-control-${{ runner.os }}
if-no-files-found: error
path: |
src-tauri/target/release/bundle/msix/*.msix
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/nsis/*.exe
src-tauri/target/release/bundle/dmg/*.dmg
src-tauri/target/release/bundle/deb/*.deb
src-tauri/target/release/bundle/rpm/*.rpm
src-tauri/target/release/bundle/appimage/*.AppImage

# Publish the installers as a GitHub release, so there is a direct download
# link. A release asked for from the Actions tab is published right away;
# pushed tags with a suffix (v2.1.0-beta.1) publish a pre-release and plain
# version tags make a draft to review first; a labelled PR gets a
# "preview-pr<N>" pre-release that is replaced on every push.
release:
name: Publish release
needs: build
if: ${{ !cancelled() && needs.build.result != 'skipped' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || inputs.release != '') }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: installers
merge-multiple: true

- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
INPUT_TAG: ${{ inputs.release }}
run: |
mapfile -t files < <(find installers -type f)
printf 'Attaching: %s\n' "${files[@]}"
if [ "$EVENT" = pull_request ]; then
tag="preview-pr$PR_NUMBER"
gh release delete "$tag" --repo "$GITHUB_REPOSITORY" --cleanup-tag --yes || true
gh release create "$tag" "${files[@]}" --repo "$GITHUB_REPOSITORY" --target "$PR_SHA" --prerelease \
--title "Preview build — PR #$PR_NUMBER" \
--notes "Test build of pull request #$PR_NUMBER at $PR_SHA. Not a release. Installers are unsigned, so Windows may show a SmartScreen warning: choose More info → Run anyway."
elif [ "$EVENT" = workflow_dispatch ]; then
if ! [[ "$INPUT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
echo "::error::Release tag must look like v2.1.0 or v2.1.0-beta.1, got '$INPUT_TAG'"
exit 1
fi
kind=--latest
case "$INPUT_TAG" in *-*) kind=--prerelease ;; esac
gh release create "$INPUT_TAG" "${files[@]}" --repo "$GITHUB_REPOSITORY" --target "$GITHUB_SHA" \
--title "Roku Control $INPUT_TAG" --generate-notes "$kind"
else
kind=--draft
case "$REF_NAME" in *-*) kind=--prerelease ;; esac
gh release create "$REF_NAME" "${files[@]}" --repo "$GITHUB_REPOSITORY" \
--title "Roku Control $REF_NAME" --generate-notes "$kind"
fi

# Submit the MSIX to the Microsoft Store listing (product 9MTNNGZKJ1WK) with
# Microsoft's Store CLI. Needs an Azure AD app linked in Partner Center; its
# IDs go in the PARTNER_CENTER_* repository secrets. Certification then
# takes Microsoft a few hours to a few days.
store:
name: Submit to Microsoft Store
needs: build
if: ${{ github.event_name == 'workflow_dispatch' && inputs.store && needs.build.result == 'success' }}
runs-on: windows-latest
steps:
- uses: actions/download-artifact@v4
with:
name: roku-control-Windows
path: windows

- name: Check the Partner Center secrets
shell: pwsh
env:
TENANT_ID: ${{ secrets.PARTNER_CENTER_TENANT_ID }}
SELLER_ID: ${{ secrets.PARTNER_CENTER_SELLER_ID }}
CLIENT_ID: ${{ secrets.PARTNER_CENTER_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.PARTNER_CENTER_CLIENT_SECRET }}
run: |
$missing = 'TENANT_ID', 'SELLER_ID', 'CLIENT_ID', 'CLIENT_SECRET' | Where-Object { -not [Environment]::GetEnvironmentVariable($_) }
if ($missing) {
Write-Output "::error::Missing repository secrets: $($missing | ForEach-Object { "PARTNER_CENTER_$_" }). See STORE.md."
exit 1
}

- uses: microsoft/setup-msstore-cli@v1

- name: Submit the MSIX
shell: pwsh
env:
TENANT_ID: ${{ secrets.PARTNER_CENTER_TENANT_ID }}
SELLER_ID: ${{ secrets.PARTNER_CENTER_SELLER_ID }}
CLIENT_ID: ${{ secrets.PARTNER_CENTER_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.PARTNER_CENTER_CLIENT_SECRET }}
run: |
$msix = Get-ChildItem windows -Recurse -Filter *.msix | Select-Object -First 1
if (-not $msix) { throw 'No .msix in the Windows build' }
msstore reconfigure --tenantId $env:TENANT_ID --sellerId $env:SELLER_ID --clientId $env:CLIENT_ID --clientSecret $env:CLIENT_SECRET
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
msstore publish -v -i $msix.FullName -id 9MTNNGZKJ1WK
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

on:
push:
branches: [master]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install Tauri system libraries
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev libayatana-appindicator3-dev

- name: Install Rust
run: |
rustup toolchain install stable --profile minimal --component clippy
rustup default stable

- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: cargo-${{ runner.os }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: cargo-${{ runner.os }}-

# The Rust build embeds ../dist, so the frontend has to exist first.
- run: npm ci
- run: npm run build

- name: Clippy
working-directory: src-tauri
run: cargo clippy --all-targets --locked -- -D warnings

- name: Tests
working-directory: src-tauri
run: cargo test --locked
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Changelog

## 2.1.0

Fixes for the problems people reported in Microsoft Store reviews ("didn't
work at all", "only volume works", "still doesn't work on the living room TV"),
plus keyboard control and power-on.

### Fixed
- The app opened pointed at the developer's own TVs (hard-coded IP addresses),
so for everyone else no button did anything until they found and changed the
device menu. Those entries are gone; the app now searches your network on
launch and picks what it finds.
- Buttons failed silently. Every command now checks the Roku's answer and says
what went wrong and how to fix it.
- Roku OS 14.1+ "Limited" mode (the reason only volume worked) is detected, and
the app shows the exact setting to change.
- The fallback network scan tried 762 addresses one at a time with a 3-second
timeout — up to ~38 minutes of spinner. It now probes your actual subnet in
parallel in a couple of seconds.
- Discovery now asks on every network adapter, so VPNs and Hyper-V/WSL virtual
adapters no longer swallow the search.
- A system or corporate proxy could intercept requests meant for the Roku; the
app now always connects directly.
- The green "connected" dot was always green. Status is now a real check every
few seconds.
- When the router gives a Roku a new IP, the app finds it again by serial number.
- The close button and window dragging were blocked by missing permissions.
- Rapid presses could arrive out of order; commands are now sent in sequence.
- Typed text now encodes spaces, symbols and accented letters correctly.
- Channel names with `&` showed as `&amp;`.

### Added
- **Turn on from off**: `PowerOn`, then Wake-on-LAN with retries.
- **Keyboard shortcuts** for every remote button, press-and-hold repeat.
- **Live typing** straight into the TV's search box.
- Real channel icons, a channel filter, "on now", and TV inputs by name.
- Add a Roku by IP, scan the whole network, forget old devices.
- Mini remote that stays on top.
- Help: troubleshooting steps, shortcut list, copy diagnostics.
- Report a problem / Suggest an idea — opens a pre-filled GitHub issue.
- MoveWeight.com and source code links in the footer.
- A one-time invitation to rate the app after ten minutes of use.
- CI (Linux + Windows) and a workflow that builds installers for all three OSes,
packs the Microsoft Store MSIX, publishes GitHub releases and can submit to
the Store.

### Removed
- The volume slider. Rokus can't report their volume, so the slider only ever
guessed; press-and-hold − / + is accurate.
Loading
Loading