Skip to content

Releases: ArdaYalinOzkan/parallax-launcher

3.7.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 22:19
Give the library a door that is not a window

A command line, because a program asked to put a game on had no way in.
The library is a text file it would have had to learn the format of, and
starting a game means knowing about Proton, Steam ids and compatibility
tools — all of which this codebase already knows and none of which it
was willing to say out loud. Now it is reachable:

    parallax-launcher --cli games --json

It lives inside the copy of Parallax already installed rather than in a
second program to install, opens no window and needs no display, so it
works over SSH and inside a container. Read the library, start a game,
add an entry, change a field, ask where a game is, ask Steam what is
really installed.

What it deliberately cannot do is delete a game or touch an account.
Those are the operations that cannot be taken back, and they are absent
rather than discouraged: something that reads a web page and decides
from it what to do next should not be holding them. Everything here is
either read-only or a change a person can undo in the window.

Which raised a question the app had never had to answer. Two things now
write to one file, and the window keeps the whole library in memory and
writes it out whole — so an edit made outside would have lived until the
next save and then vanished under an older copy, with nobody ever seeing
what happened. The file is watched now, and a change the app did not
make is read back within a second. Its own writes are marked as they
happen so it does not chase its own tail, and a game being edited at
that moment is left alone: the person holding it keeps it.

Also: how much room a game takes now sits beside its status rather than
in a column of its own, since a column reading "—" for every game you
have not installed is mostly empty space asking to be looked at. And on
Windows the installer has earned enough reputation that SmartScreen no
longer stops it, so the warning has come off the download page and the
guide that explained how to get past it is now a short note in case a
future release starts as a stranger again.

3.6.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 19:38
Let the library answer for itself

Four things a person had to do by hand, that the program knew how to do
all along.

Sorting by cover colour. The artwork is already on disk and already
decoded at start-up; reading the dominant hue out of it costs nothing
more, and a shelf arranged by colour is a shelf you can find things on
by eye. Hues are averaged as vectors on the wheel rather than as
numbers, because 350° and 10° are neighbours and their arithmetic mean
is not. Covers with no colour worth speaking of go to the end, by name.
"Default" is gone from the sort list — it meant "the order they happened
to be added", which is nobody's idea of an order — and Play Time takes
its place as the one that opens.

How much room a game takes. Measured by walking the folder rather than
asking Steam, since a game somebody pointed at by hand has no Steam
record and the question is the same either way. Bounded by a file
budget: a modded install can hold hundreds of thousands of files and
nobody wants a page that waits on a filesystem.

Where a game is. Adding one used to mean finding its folder yourself,
which sounds easy until you try it: the folder is rarely named after the
game, it is often on a second disk, and every shop puts it somewhere
else. Steam's own manifest answers it exactly when there is an appid;
everything else is a search of the places games are kept, including
C:\XboxGames, which is where Flight Simulator lives and where nobody
thinks to look. The old version of this knew one hard-coded Steam path
and matched folder names against titles character for character, which
is to say it almost never worked.

And the button on a game that is not installed. It offered to install
whatever had a Steam id, whether or not the account owned it — so a game
shared through a family, or bought somewhere else entirely, led to Steam
refusing a request it could not honour and the person standing in front
of a button that would not work. It now looks on this computer first and
only turns to Steam if the game is genuinely not here. When it is not,
the choice between downloading and pointing at an existing copy is a
real choice, so it gets asked rather than guessed.

Internal Notes is removed. Every note the program ever wrote there was
the same sentence, nobody had typed anything else, and a field carried
for years by inertia is worth less than the space it takes. The text
goes from everyone's library file on first run; anything that was not
that boilerplate is copied out to notes-backup.txt beside it first,
because deleting is the instruction and doing it without leaving a way
back is not.

Also: play time on a game's page was showing Steam's raw float —
79.7167 Hours, when the cards had been rounding it all along. The file
keeps its precision; the page rounds, and hands the unrounded value back
when you edit it, so saving cannot quietly file down anyone's hours a
minute at a time. And the file picker offered .exe and nothing else,
which on Linux hid exactly the files somebody was looking for.

Two things outside the program. The NOTICE now declines to license the
name and logo under section 7(e): the code is free to take, the name is
not, and a fork ships under one of its own. And every released file now
carries a signed build attestation naming the commit and the workflow
that produced it — readable source says what the code does, this says
the download is that code.

3.5.1

Choose a tag to compare

@github-actions github-actions released this 25 Aug 12:15
Let the loading screen be seen

It was held for at least 900ms so it could not flicker past. On this
machine the work often finishes sooner than that, so what people
actually saw was a flash — which tells them nothing and looks like a
fault.

Three seconds now. It is longer than the work usually needs and that is
the point: the app is doing something on their behalf, and this is the
one place it says so. A launcher that opens instantly and then stutters
feels worse than one that takes a moment and then does not.

3.5.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 09:02
Load it all at the start, so nothing loads later

The loading screen was doing half a job: it warmed the first sixty
covers when an account was picked, which left a second wait in the
middle of using the app and still left every game page to fetch its own
banner. There is one wait now, at launch, and it does the whole thing.

Every account's art is gathered and warmed behind that screen — which
account somebody will pick is not knowable at start-up, and warming the
wrong one is worse than warming none. Covers are decoded, because the
shelf paints them the instant it appears. Banners are only fetched: a
screen-sized picture costs several megabytes decoded and there is one
per game, so holding them all would cost more than it saves. With the
bytes already here, opening a game page decodes one in a few
milliseconds.

Picking an account no longer shows a loading screen at all. It has
nothing left to do.

Measured after the veil lifts, longest frame in each:

  library         100ms
  first game       67ms
  game 120 of 216  33ms
  game 215 of 216  33ms
  settings         67ms

The last two are the point: a game deep in the list opens exactly as
smoothly as the first.

Also — a Steam game uninstalled through Steam went on showing as
installed no matter how often the library was refreshed. The check
asked whether the folder was still there, and Steam leaves it behind,
empty. Steam's own appmanifest_<id>.acf is the authority for a Steam
game and is used now; a game pointed at by hand is still judged by its
folder, which is the only thing there is to judge it by. Three games
here were wrong; the refresh corrects them.

3.4.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 08:40
Do the waiting before the animation, not underneath it

Every screen was worst the first time, and every attempt to shave that
down ran into the same wall: the work is real, and something has to
absorb it. So the app now says so. A loading screen carries the mark
and wordmark the account screen already wears, plus a bar, and it is up
from the very first frame — nothing is shown part-drawn and then
animated over.

It covers two waits. At launch it holds until start-up has finished and
the account screen is painted. On picking an account it returns, decodes
the first sixty covers with the bar moving, swaps the screens
underneath itself, and only then lifts. What used to be a stuttering
transition is now a veil rising off a library that is already finished.

Three things had to be true for that to work.

The veil sits at 0.999 opacity rather than 1. A fully opaque element
covering the window lets the browser skip painting everything behind
it, so the library was not drawn until the veil began to fade — putting
its most expensive frame in the middle of the only animation anybody
sees. At .995 the account screen showed faintly through; at .999
nothing does, and the shelf paints while still hidden.

Only sixty covers are decoded up front. All two hundred takes about
eight seconds, which is not a wait worth imposing to prevent a stutter,
and the stutter only ever came from the first screenful. The rest, and
every banner, are fetched afterwards in small batches with a pause
between them — draining that queue as fast as idle time allowed stalled
a frame for half a second a moment after the library appeared, and idle
work anybody can see is not idle work. A game page opened after the
warming has finished shows no long tasks at all.

And the veil stays up for at least 900ms even when the work finishes
sooner, because a loading screen that comes and goes inside a blink
reads as a glitch.

Measured across the entry: stalls now land before the veil lifts. After
it, the longest frame is 50ms.

3.3.3

Choose a tag to compare

@github-actions github-actions released this 25 Aug 08:21
Stop the first visit to a screen from stuttering

Walking into the library, settings or a game page for the first time
jerked; the second time was smooth. That gap is the whole diagnosis —
the cost is work each screen does once, and it was landing on top of
the animation.

Measured, first visits, before and after:

  library    55 frames /  5 stalls / 613ms of long tasks
          → 101 frames /  2 stalls / 177ms
  settings   68 frames /  4 stalls   → 68 /  2
  again      68 frames /  7 stalls   → 75 /  1
  game page  63 frames /  6 stalls   → 70 /  4

Four changes. Covers are lazy and decode off-thread, so a shelf of two
hundred no longer decodes two hundred pictures to show twelve.
content-visibility lets the browser skip laying out the cards below the
fold at all. The shelf is built in a fragment and put in once instead of
appended card by card. And the game page measured its banner on a 450ms
timer, which put the most expensive frame of the visit exactly halfway
through the transition — it hangs off the end of the transition now,
same work, nothing moving.

A fifth change was tried and reverted with a comment left behind:
moving will-change off the cards and onto :hover, which is the usual
advice, made the library go from 4 stalled frames to 47. The cards sit
under a backdrop-filter whenever a dialog opens, and filtering
unpromoted content rasterises the whole shelf every frame.

Also: opening one game straight after another showed a flash of the
previous game's banner, because an <img> keeps its old picture until
the new one decodes. It is hidden until the new one is ready and fades
in. Sampled every 25ms across the switch: no frame shows the old one.

3.3.2

Choose a tag to compare

@github-actions github-actions released this 25 Aug 08:04
Let dialogs close the way they open

Two separate faults with the same cause: the animation was in the wrong
place.

Opening faded the whole modal by animating its opacity. An element with
animating opacity becomes its own compositing layer, and the
backdrop-filter inside has nothing to sample until that layer settles —
so the window slid in clear and the blur landed afterwards, in one step.

Closing set `display: none`, which cannot be animated at all. Window
and blur went together, mid-frame, with nothing in between.

Neither animates the modal now. The overlay fades and un-blurs on its
own curve, the panel moves on its own, and the modal itself only stops
taking clicks — its visibility switch is delayed past the transition so
both children are still on screen while they leave. Measured on the way
in and out: blur and panel travel together, 0 to 7px and back over
240ms.

3.3.1

Choose a tag to compare

@github-actions github-actions released this 25 Aug 07:52
Let the shelf show through behind a dialog

blur(16px) over 74% black wiped the library out entirely. Both dialogs
were already using the same rule, so the update box only looked heavier
than settings because it is small and leaves far more background in
view — the value was the same, the impression was not.

Eased to blur(7px) over 58%, which both share. The covers behind are
recognisable again without competing: the dialog still sits plainly in
front of them.

3.3.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 07:32
Open in about a second and a half instead of three

The window took 2.9s to appear, and 2.1s of that was the renderer.
Almost all of it was one line: the page asked Google for Inter and
JetBrains Mono, and that request blocked the first paint. A desktop
program sat waiting on a network round-trip before it could draw
anything — and offline it wore whatever the system had instead.

Both faces are in the app now, variable-weight woff2, Latin and
Cyrillic only: 240 KB for the eight languages that use them, with
Chinese and Korean falling through to the system as they always did.
There were two requests, not one — a <link> in the head and an @import
at the top of styles.css — which is why the timings came in bimodal
until the second one was found. Median to a usable window is now 1.41s
across six runs, and nothing leaves the machine.

The Steam catalogue fetch at startup is gone with it. It called an
endpoint that has returned 404 for some time, to fill a cache whose
only reader the renderer never calls.

Updating happens where it starts. The dialog used to close itself and
drop you into Settings to finish the download; now it carries the
status line, the progress bar, Restart, and the automatic-check switch
itself, and the panel in Settings still has all four — one set of
actions driving both. The release notes are gone from both: they are
written for the commit log, arrived with their own markup showing
through as text, and said nothing a person at that moment wants.

And the cards keep the tilt as you sweep along a shelf. Each one now
has an invisible box 10px larger on every side, just under half the
gap, so crossing between two no longer drops the highlight — and no
pixel belongs to two cards. Padding would have done it too, but
box-sizing is border-box here, so it took those pixels out of the
cover instead of adding them outside it.

3.2.4

Choose a tag to compare

@github-actions github-actions released this 25 Aug 07:01
Stop the app from deleting menu entries it did not write

It swept the whole applications directory for any entry whose command
named a Parallax AppImage that was no longer there, and removed it.
That was a wider reach than the job needed. Deciding that somebody
else's menu entry is rubbish is not the app's business, and a mistake
there is invisible until the day the entry is missing.

It also could not do the job it was written for. There is exactly one
entry, rewritten on every launch, so it cannot go stale while the app
still runs — and once the file is deleted the app never runs again.
That case belongs to the uninstall instructions on the site, which
name the two files to remove.

The whole function is gone. The comment above installDesktopEntry says
why, so it does not come back.