Compare commits

5 Commits
Author SHA1 Message Date
nvrl 6cdab2fde1 chore(version): bump to 1.1.3
Patch. Fixes two Windows installer defects: an MSI upgrade no longer leaves
the app without libmpv until a second install (B62), and a taskbar pin keeps
its icon, which the exe now carries itself (B63). The catalog schema and
on-disk data are unchanged from 1.1.2.
2026-10-06 03:05:20 +02:00
nvrl 6239780978 fix(app,packaging): keep libmpv and the taskbar icon across Windows upgrades
An MSI upgrade left the app without libmpv-2.dll until it was installed a
second time (B62). Costing runs before RemoveExistingProducts, saw the old
DLL at the same version and skipped the copy, and removing the old product
then deleted it. Both installer definitions now set REINSTALLMODE=amus;
every file is private to the install directory.

A taskbar pin lost its icon after a while (B63). The exe carried no icon
resource, and the Start Menu shortcut's icon lived in the per-ProductCode
installer cache that each upgrade deletes. build.rs now compiles the .ico
into proton-stream.exe, and the shortcut has no Icon of its own (and in the
WiX build is no longer advertised), so both show the exe's icon.
2026-10-06 03:04:07 +02:00
nvrl 67cf3044f1 fix(android): keep the accent button's width and corners
Moving the gradient onto a row inside the button lost both: the button's
own row hands its children no minimum size, so a fillMaxWidth button
painted the ramp only behind its label, and with square corners.

The gradient goes back on the button, and the 48dp touch-target padding
is taken by a box around it instead, with the button inside told not to
add its own. The primary button now matches the other buttons' 40dp
container at every width. The screenshots are re-recorded for it.
2026-10-04 17:14:11 +02:00
nvrl 66be35c3d4 chore(version): bump to 1.1.2
Patch. The only change since 1.1.1 is on Android: the primary button on the
library hero no longer draws taller than the button beside it. The catalog
schema and on-disk data are unchanged from 1.1.1.
2026-10-04 16:55:38 +02:00
nvrl 046cfe62b8 fix(android): paint the accent button's gradient inside its container
The gradient sat on the outer modifier, which Material pads out to the
48dp minimum touch target, so the primary button drew 8dp taller than
the 40dp tonal button beside it on the library hero.
2026-10-04 16:45:25 +02:00
17 changed files with 189 additions and 26 deletions

No files matched your search

Generated
+52 -7
View File
@@ -1866,6 +1866,20 @@ dependencies = [
"bytemuck", "bytemuck",
] ]
[[package]]
name = "embed-resource"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd"
dependencies = [
"cc",
"memchr",
"rustc_version",
"toml",
"vswhom",
"winreg",
]
[[package]] [[package]]
name = "endi" name = "endi"
version = "1.1.1" version = "1.1.1"
@@ -4359,7 +4373,7 @@ dependencies = [
[[package]] [[package]]
name = "pstr-android" name = "pstr-android"
version = "1.1.1" version = "1.1.3"
dependencies = [ dependencies = [
"android_logger", "android_logger",
"jni 0.21.1", "jni 0.21.1",
@@ -4378,7 +4392,7 @@ dependencies = [
[[package]] [[package]]
name = "pstr-app" name = "pstr-app"
version = "1.1.1" version = "1.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -4387,6 +4401,7 @@ dependencies = [
"eframe", "eframe",
"egui", "egui",
"egui-phosphor", "egui-phosphor",
"embed-resource",
"futures", "futures",
"image", "image",
"parking_lot", "parking_lot",
@@ -4409,7 +4424,7 @@ dependencies = [
[[package]] [[package]]
name = "pstr-cli" name = "pstr-cli"
version = "1.1.1" version = "1.1.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -4427,7 +4442,7 @@ dependencies = [
[[package]] [[package]]
name = "pstr-core" name = "pstr-core"
version = "1.1.1" version = "1.1.3"
dependencies = [ dependencies = [
"directories", "directories",
"futures", "futures",
@@ -4445,7 +4460,7 @@ dependencies = [
[[package]] [[package]]
name = "pstr-meta" name = "pstr-meta"
version = "1.1.1" version = "1.1.3"
dependencies = [ dependencies = [
"pstr-core", "pstr-core",
"reqwest", "reqwest",
@@ -4458,7 +4473,7 @@ dependencies = [
[[package]] [[package]]
name = "pstr-player" name = "pstr-player"
version = "1.1.1" version = "1.1.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"libloading", "libloading",
@@ -4473,7 +4488,7 @@ dependencies = [
[[package]] [[package]]
name = "pstr-stream" name = "pstr-stream"
version = "1.1.1" version = "1.1.3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"futures", "futures",
@@ -6248,6 +6263,26 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@@ -7161,6 +7196,16 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.57.1" version = "0.57.1"
+3 -1
View File
@@ -11,7 +11,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "1.1.1" version = "1.1.3"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://github.com/narrrl/proton-stream" repository = "https://github.com/narrrl/proton-stream"
@@ -103,6 +103,8 @@ dbus = "0.9"
windows-sys = "0.61" windows-sys = "0.61"
# Toasts on Windows. Linux notifications go over the `dbus` above. # Toasts on Windows. Linux notifications go over the `dbus` above.
tauri-winrt-notification = "0.8" tauri-winrt-notification = "0.8"
# The exe's own icon resource, which is what Explorer and a taskbar pin draw.
embed-resource = "3"
rpassword = "7" rpassword = "7"
libloading = "0.8" libloading = "0.8"
lru = "0.18" lru = "0.18"
@@ -2,6 +2,7 @@ package io.narl.protonstream.ui.theme
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -9,6 +10,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.NavigationDrawerItemDefaults
@@ -18,14 +20,18 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaul
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteItemColors
import androidx.compose.material3.SliderState import androidx.compose.material3.SliderState
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/** /**
@@ -68,17 +74,30 @@ internal fun AccentButton(
content: @Composable RowScope.() -> Unit, content: @Composable RowScope.() -> Unit,
) { ) {
val shape = MaterialTheme.shapes.small val shape = MaterialTheme.shapes.small
Button( // The button pads itself out to the 48dp minimum touch target, and a
onClick = onClick, // background on its modifier fills that whole target — 8dp taller than
// Only while it is live: a disabled button keeps Material's flat // the 40dp container every other button draws. So the padding is taken
// disabled container, because a greyed control that still wears the // here, outside the ramp, and the button inside is told not to add its
// one strong colour in the window reads as pressable. // own. The box hands its size down, so a `fillMaxWidth` still reaches it.
modifier = if (enabled) modifier.background(accentBrush(), shape) else modifier, Box(
enabled = enabled, modifier.minimumInteractiveComponentSize(),
shape = shape, contentAlignment = Alignment.Center,
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), propagateMinConstraints = true,
content = content, ) {
) CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
Button(
onClick = onClick,
// Only while it is live: a disabled button keeps Material's flat
// disabled container, because a greyed control that still wears the
// one strong colour in the window reads as pressable.
modifier = if (enabled) Modifier.background(accentBrush(), shape) else Modifier,
enabled = enabled,
shape = shape,
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
content = content,
)
}
}
} }
@Composable @Composable
Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 KiB

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 KiB

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 638 KiB

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+3
View File
@@ -34,6 +34,9 @@ directories.workspace = true
chrono.workspace = true chrono.workspace = true
souvlaki.workspace = true souvlaki.workspace = true
[build-dependencies]
embed-resource.workspace = true
[dev-dependencies] [dev-dependencies]
skrifa.workspace = true skrifa.workspace = true
+15
View File
@@ -0,0 +1,15 @@
//! Compiles `proton-stream.rc` into the Windows exe; nothing on any other
//! target. `rc.exe` on an MSVC build, mingw's `windres` on the cross build
//! from Linux.
fn main() {
println!("cargo:rerun-if-changed=proton-stream.rc");
println!("cargo:rerun-if-changed=../../packaging/windows/proton-stream.ico");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
// Only the GUI gets the icon; `pstr` is a console tool.
embed_resource::compile_for("proton-stream.rc", ["proton-stream"], embed_resource::NONE)
.manifest_optional()
.expect("compile the icon resource");
}
+4
View File
@@ -0,0 +1,4 @@
// The exe's icon. The window sets one at runtime too, but that only lasts as
// long as the window: Explorer, the Start menu and a taskbar pin read the icon
// out of the file, and without this one they fall back to the generic one.
1 ICON "../../packaging/windows/proton-stream.ico"
+48
View File
@@ -146,6 +146,54 @@ and [B44](#b44--r8-broke-the-app-under-instrumentation-in-five-places).
## Fixed ## Fixed
### B63 — a taskbar pin on Windows lost its icon
**Symptom.** Proton Stream pinned to the Windows taskbar showed its icon at
first, then after a while — reliably after an update — the generic program
icon.
**Cause.** Two, each enough on its own. `proton-stream.exe` carried no icon
resource: the window sets one at runtime, which is what a pin shows while the
icon cache still holds it, but a pin made from the running window points at
the exe, and once the cache is rebuilt the exe has nothing to give. A pin made
from the Start Menu shortcut instead took the shortcut's `Icon`, which Windows
Installer keeps in `C:\Windows\Installer\{ProductCode}`. Every upgrade has a
new ProductCode and deletes the old product's folder, icon included.
**Fix.** `crates/pstr-app/build.rs` compiles `proton-stream.rc`, which embeds
`packaging/windows/proton-stream.ico`, into the GUI exe: `rc.exe` on MSVC,
mingw's `windres` on the cross build. Both installer definitions drop the
shortcut's `Icon` (and the WiX one `Advertise`), so the shortcut shows the
exe's icon too. A pin that already lost its icon keeps the dead reference;
unpin and pin again once.
**Verified.** `objdump -p` on the cross-built exe lists the `RT_ICON` entries,
all seven sizes. `msiinfo export … Shortcut` on a wixl build has an empty
`Icon_` column. Not yet checked on a Windows taskbar across an upgrade.
### B62 — an MSI upgrade left the app without libmpv
**Symptom.** Installing a new `.msi` over an old one ended with an app that
would not start for want of mpv. Installing the same `.msi` a second time
fixed it.
**Cause.** Both installers remove the old product early — the wixl one at
`RemoveExistingProducts` after `InstallInitialize`, WiX's `MajorUpgrade` by
default after `InstallValidate` — but costing runs before either, at
`CostFinalize`. Costing found the old `libmpv-2.dll` on disk with the same
version as the new one, since mpv is pinned, and decided not to copy it. Then
removing the old product deleted it. The second install found no DLL on disk,
so it copied one.
**Fix.** `REINSTALLMODE=amus` in both installer definitions, which copies every
file whatever is on disk. Every file is private to the install directory, so
the forced overwrite can downgrade nothing shared.
**Verified.** `msiinfo export … InstallExecuteSequence` on a wixl build shows
`FileCost` 900 and `CostFinalize` 1000 ahead of `RemoveExistingProducts` 1501,
and `msiinfo export … Property` has `REINSTALLMODE amus`. Not yet checked as an
upgrade on Windows.
### B61 — later seasons were captioned with season one's episode names ### B61 — later seasons were captioned with season one's episode names
**Symptom.** On a 42-title AniList library, Attack on Titan seasons 2–4 and **Symptom.** On a 42-title AniList library, Attack on Titan seasons 2–4 and
+1 -1
View File
@@ -14,7 +14,7 @@
# builds from the registry alone. # builds from the registry alone.
pkgname=proton-stream pkgname=proton-stream
pkgver=1.1.1 pkgver=1.1.3
pkgrel=1 pkgrel=1
pkgdesc="Netflix-style desktop client for Proton Drive public links: browse and stream a share" pkgdesc="Netflix-style desktop client for Proton Drive public links: browse and stream a share"
arch=('x86_64') arch=('x86_64')
+1 -1
View File
@@ -124,7 +124,7 @@ The `.icns` is rasterized from `io.narl.proton-stream.svg` when `rsvg-convert` o
| `io.narl.proton-stream.desktop` / `.svg` | Freedesktop entry and icon | | `io.narl.proton-stream.desktop` / `.svg` | Freedesktop entry and icon |
| `macos/Info.plist.in` | `.app` bundle plist; `@VERSION@`/`@BIN@`/`@APP_ID@` substituted at build time | | `macos/Info.plist.in` | `.app` bundle plist; `@VERSION@`/`@BIN@`/`@APP_ID@` substituted at build time |
| `windows/proton-stream.wxs` | WiX v4+ installer definition | | `windows/proton-stream.wxs` | WiX v4+ installer definition |
| `windows/proton-stream.ico` | MSI and shortcut icon, rasterized from the SVG | | `windows/proton-stream.ico` | Exe and Add/Remove Programs icon, rasterized from the SVG; `crates/pstr-app/build.rs` compiles it into `proton-stream.exe` |
| `windows/license.rtf` | Fallback for the installer's license dialog; the repo's own `LICENSE` is preferred when it exists | | `windows/license.rtf` | Fallback for the installer's license dialog; the repo's own `LICENSE` is preferred when it exists |
The Debian `control` file has no template — `scripts/build.sh` writes it, because The Debian `control` file has no template — `scripts/build.sh` writes it, because
+17 -3
View File
@@ -78,6 +78,15 @@
<RemoveExistingProducts After="InstallInitialize" /> <RemoveExistingProducts After="InstallInitialize" />
</InstallExecuteSequence> </InstallExecuteSequence>
<!-- Copy every file on an upgrade, whatever is on disk. Costing runs
before RemoveExistingProducts, so it sees the old libmpv-2.dll — the
same version, since mpv is pinned — and decides not to copy it. The
removal of the old product then deletes it, and the upgrade ends with
no DLL: "mpv missing" until a second install, which finds nothing on
disk and copies it. Every file here is private to INSTALLDIR, so
overwriting unconditionally downgrades nothing shared. -->
<Property Id="REINSTALLMODE" Value="amus" />
<Media Id="1" Cabinet="proton_stream.cab" EmbedCab="yes" /> <Media Id="1" Cabinet="proton_stream.cab" EmbedCab="yes" />
<Icon Id="proton-stream.ico" SourceFile="$(var.IconFile)" /> <Icon Id="proton-stream.ico" SourceFile="$(var.IconFile)" />
@@ -98,14 +107,19 @@
and writes the advertised form anyway, with the feature name in and writes the advertised form anyway, with the feature name in
Shortcut/Target. What selects the non-advertised form is being Shortcut/Target. What selects the non-advertised form is being
a child of the Component rather than of the File, plus an a child of the Component rather than of the File, plus an
explicit formatted Target. Keep it out of the <File>. --> explicit formatted Target. Keep it out of the <File>.
No Icon either. A shortcut's Icon is copied to
C:\Windows\Installer\{ProductCode}, and every upgrade has a
new ProductCode and deletes the old folder — taking the icon
of every taskbar pin made from the shortcut with it. Without
one, the shortcut shows the exe's own icon resource. -->
<Shortcut Id="StartMenuShortcut" <Shortcut Id="StartMenuShortcut"
Directory="ProgramMenuFolder" Directory="ProgramMenuFolder"
Name="Proton Stream" Name="Proton Stream"
Description="Browse and stream a Proton Drive public link" Description="Browse and stream a Proton Drive public link"
Target="[INSTALLDIR]proton-stream.exe" Target="[INSTALLDIR]proton-stream.exe"
WorkingDirectory="INSTALLDIR" WorkingDirectory="INSTALLDIR" />
Icon="proton-stream.ico" />
</Component> </Component>
<!-- The CLI, and the PATH entry that makes it worth installing. Tied <!-- The CLI, and the PATH entry that makes it worth installing. Tied
+15 -2
View File
@@ -26,6 +26,15 @@
DowngradeErrorMessage="A newer version of Proton Stream is already installed." /> DowngradeErrorMessage="A newer version of Proton Stream is already installed." />
<MediaTemplate EmbedCab="yes" /> <MediaTemplate EmbedCab="yes" />
<!-- Copy every file on an upgrade, whatever is on disk. Costing runs
before RemoveExistingProducts, so it sees the old libmpv-2.dll — the
same version, since mpv is pinned — and decides not to copy it. The
removal of the old product then deletes it, and the upgrade ends with
no DLL: "mpv missing" until a second install, which finds nothing on
disk and copies it. Every file here is private to INSTALLFOLDER, so
overwriting unconditionally downgrades nothing shared. -->
<Property Id="REINSTALLMODE" Value="amus" />
<Icon Id="AppIcon" SourceFile="$(var.IconFile)" /> <Icon Id="AppIcon" SourceFile="$(var.IconFile)" />
<Property Id="ARPPRODUCTICON" Value="AppIcon" /> <Property Id="ARPPRODUCTICON" Value="AppIcon" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/narrrl/proton-stream" /> <Property Id="ARPURLINFOABOUT" Value="https://github.com/narrrl/proton-stream" />
@@ -45,12 +54,16 @@
<ComponentGroup Id="AppComponents" Directory="INSTALLFOLDER"> <ComponentGroup Id="AppComponents" Directory="INSTALLFOLDER">
<Component Guid="623c8cb8-b826-4d57-a054-e5be56f24c19"> <Component Guid="623c8cb8-b826-4d57-a054-e5be56f24c19">
<File Id="AppExe" Source="$(var.BinDir)\proton-stream.exe" KeyPath="yes" /> <File Id="AppExe" Source="$(var.BinDir)\proton-stream.exe" KeyPath="yes" />
<!-- Not advertised, and no Icon: either one puts the shortcut's icon
in C:\Windows\Installer\{ProductCode}, which the next upgrade
deletes along with the icon of every taskbar pin made from the
shortcut. This way it shows the exe's own icon resource. -->
<Shortcut Id="StartMenuShortcut" <Shortcut Id="StartMenuShortcut"
Directory="ProgramMenuFolder" Directory="ProgramMenuFolder"
Name="Proton Stream" Name="Proton Stream"
Description="Browse and stream a Proton Drive public link" Description="Browse and stream a Proton Drive public link"
Icon="AppIcon" Target="[#AppExe]"
Advertise="yes" /> WorkingDirectory="INSTALLFOLDER" />
</Component> </Component>
<!-- The CLI, and the PATH entry that makes it worth installing. Tied to <!-- The CLI, and the PATH entry that makes it worth installing. Tied to