Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b446c64466 | ||
|
|
8422ff4636 | ||
|
|
8fa2b643a0 | ||
|
|
a77f13545a | ||
|
|
94b1d1cf83 | ||
|
|
0255212c95 | ||
|
|
a4cd330e2f | ||
|
|
4975dce39a | ||
|
|
7e6130695c | ||
|
|
6b14eff740 |
No files matched your search
@@ -1,8 +1,10 @@
|
||||
# Continuous integration. Manual only while the repository is private (docs/release.md, step 4: add push and
|
||||
# pull_request when it becomes public).
|
||||
# Continuous integration: the Linux tests (debug and ASan), a short fuzz run and the PS4 package.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0 (not released yet)
|
||||
## 1.0.1 (2026-10-17)
|
||||
|
||||
- Home screen icon: the halo above "Playfin." in Jellyfin's purple-to-blue gradient.
|
||||
|
||||
## 1.0.0 (2026-10-17)
|
||||
|
||||
- Servers: several servers and several users per server, found on the network or added by address; sign-in with
|
||||
a password or Quick Connect; HTTPS with certificate pinning for self-signed servers.
|
||||
|
||||
@@ -8,7 +8,7 @@ if(PLATFORM_PS4 AND NOT CMAKE_TOOLCHAIN_FILE)
|
||||
set(CMAKE_TOOLCHAIN_FILE "$ENV{OPENORBIS}/cmake/ps4.cmake" CACHE FILEPATH "")
|
||||
endif()
|
||||
|
||||
project(Playfin VERSION 0.1.0 LANGUAGES C CXX)
|
||||
project(Playfin VERSION 1.0.1 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
# Playfin
|
||||
<h1>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/images/logo-on-dark.png">
|
||||
<img alt="Playfin" src="docs/images/logo-on-light.png" width="300">
|
||||
</picture>
|
||||
</h1>
|
||||
|
||||
A Jellyfin client for the PlayStation 4, for consoles running homebrew (GoldHEN).
|
||||
|
||||
<a href="https://github.com/LowganDev/PlayfinPS4/releases/latest/download/IV0001-PLFN00000_00-PLFN000000100000.pkg"><img alt="Download for PS4" src="docs/images/download.png" width="281"></a>
|
||||
|
||||
The latest version's package; earlier versions are on the [Releases](https://github.com/LowganDev/PlayfinPS4/releases)
|
||||
page.
|
||||
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -30,8 +40,8 @@ A Jellyfin client for the PlayStation 4, for consoles running homebrew (GoldHEN)
|
||||
|
||||
## Installing
|
||||
|
||||
1. Copy `IV0001-PLFN00000_00-PLFN000000100000.pkg` to the console (USB drive or FTP) and install it from
|
||||
GoldHEN → Debug Settings → Package Installer.
|
||||
1. Download `IV0001-PLFN00000_00-PLFN000000100000.pkg` with the button at the top, copy it to the console (USB
|
||||
drive or FTP) and install it from GoldHEN → Debug Settings → Package Installer.
|
||||
2. Start Playfin from the home screen. Servers on your network are found automatically; others are added by
|
||||
address (for example `192.168.1.20:8096` or `https://media.example.com`).
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
@@ -31,14 +31,13 @@ The checklist for a release; release tags are the version number without "v" (`1
|
||||
|
||||
1. Commit "build: Playfin x.y.z", tag `x.y.z`, push the tag.
|
||||
2. GitHub release "Playfin x.y.z" on the tag, with:
|
||||
- `IV0001-PLFN00000_00-PLFN000000100000.pkg`;
|
||||
- the dependencies' source: an archive of the upstream tarballs named in `scripts/ps4/*/PKGBUILD` (and the
|
||||
pacbrew image's libraries: FriBidi, zlib, libsamplerate) with the patches, and the OpenOrbis toolchain's
|
||||
source at the version in the pacbrew image (GPL-3.0; its `crt1.o`, `crti.o` and `crtn.o` are linked into
|
||||
`eboot.bin`), so that the GPL and LGPL components' corresponding source is offered with the binary;
|
||||
- `IV0001-PLFN00000_00-PLFN000000100000.pkg` (this exact name: the README's download button links to
|
||||
`releases/latest/download/` with it, and "latest" skips pre-releases);
|
||||
- the corresponding source, `dist/playfin-x.y.z-sources.tar`, made by `scripts/ps4/source-archive.sh` (run at
|
||||
the release's commit): Playfin, every recipe of `scripts/ps4` with its patches and upstream sources, and the
|
||||
build image's FriBidi, zlib, libsamplerate and OpenOrbis toolchain (GPL-3.0: crt and `link.x`) with their
|
||||
PacBrew recipes, all checked against pinned checksums;
|
||||
- the changelog section as notes.
|
||||
3. For the first public release: make the repository public, then switch CI on (add `push` and `pull_request` to
|
||||
`on:` in `.github/workflows/ci.yml`; it runs on `workflow_dispatch` only until then).
|
||||
|
||||
## Dependency packages
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 58 KiB |
@@ -5,7 +5,10 @@
|
||||
set -euo pipefail
|
||||
elf=$1 eboot=$2
|
||||
bad=$(readelf -d "$elf" | grep NEEDED | grep '/' || true)
|
||||
if [ -n "$bad" ] || strings "$eboot" | grep -q '^/opt/'; then
|
||||
# a library path left in eboot.bin (whole output read: grep -q would stop strings early, and pipefail would hide the
|
||||
# match); other /opt/ strings, such as mpv's default configuration folder, are not imports
|
||||
bad+=$(strings "$eboot" | grep -E '^/opt/.*\.(so|prx|sprx)$' || true)
|
||||
if [ -n "$bad" ]; then
|
||||
echo "PS4 imports must be bare module names:" >&2
|
||||
echo "$bad" >&2
|
||||
exit 1
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Makes the PS4 home screen art of Playfin from the shipped Sora font (needs Pillow).
|
||||
"""Makes the PS4 home screen art and the README logo of Playfin from the shipped Sora font (needs Pillow).
|
||||
|
||||
python3 scripts/make-ps4-art.py
|
||||
|
||||
icon0.png (512x512): "Playfin." under a blue halo. pic1.png (1920x1080): the app's background with the halo and the
|
||||
logo, shown by the PS4 behind the selected app. Colours from the theme (src/ui/theme.cpp).
|
||||
icon0.png (512x512): "Playfin." under a halo in Jellyfin's purple-to-blue gradient. pic1.png (1920x1080): the same
|
||||
halo and logo, shown by the PS4 behind the selected app. Jellyfin's branding guide forbids its logo inside another
|
||||
logo without permission and allows its gradient on a shape of our own: only the gradient is used. docs/images/logo-on-dark.png and logo-on-light.png: the "Playfin."
|
||||
wordmark on a transparent background for the README (GitHub's dark and light themes). docs/images/download.png: the
|
||||
README's download button. Colours from the theme (src/ui/theme.cpp).
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
@@ -16,11 +19,13 @@ BACKGROUND = (0x0E, 0x0E, 0x11)
|
||||
ACCENT = (0x00, 0x70, 0xD1)
|
||||
TEXT = (0xF2, 0xF2, 0xF5)
|
||||
DOT = (0x2F, 0x8C, 0xFF) # lighter than the accent: legible at icon size
|
||||
GRADIENT = ((0xAA, 0x5C, 0xC3), (0x00, 0xA4, 0xDC)) # Jellyfin's, left to right, for the home screen art
|
||||
FONT = os.path.join(ROOT, "resources", "fonts", "Sora-Bold.ttf")
|
||||
|
||||
|
||||
def halo(width, height, centre_y, radius_x, radius_y, strength):
|
||||
"""Background with an elliptical light of the accent (strength at the centre, gone at 70 % of the radii)."""
|
||||
def halo(width, height, centre_y, radius_x, radius_y, strength, colours=(ACCENT, ACCENT)):
|
||||
"""Background with an elliptical light (strength at the centre, gone at 70 % of the radii) whose colour goes from
|
||||
colours[0] on the left edge to colours[1] on the right."""
|
||||
image = Image.new("RGB", (width, height))
|
||||
pixels = image.load()
|
||||
cx, cy = width / 2, centre_y * height
|
||||
@@ -30,28 +35,72 @@ def halo(width, height, centre_y, radius_x, radius_y, strength):
|
||||
dy = (y + 0.5 - cy) / (radius_y * height)
|
||||
d = math.sqrt(dx * dx + dy * dy)
|
||||
light = 0.0 if d >= 0.7 else strength * (1 - d / 0.7)
|
||||
pixels[x, y] = tuple(round(b + (a - b) * light) for a, b in zip(ACCENT, BACKGROUND))
|
||||
t = (x + 0.5) / width
|
||||
colour = tuple(l + (r - l) * t for l, r in zip(*colours))
|
||||
pixels[x, y] = tuple(round(b + (a - b) * light) for a, b in zip(colour, BACKGROUND))
|
||||
return image
|
||||
|
||||
|
||||
def logo(image, size, centre_y):
|
||||
def logo(image, size, centre_y, dot=DOT):
|
||||
draw = ImageDraw.Draw(image)
|
||||
font = ImageFont.truetype(FONT, size)
|
||||
name_width = draw.textlength("Playfin", font=font)
|
||||
dot_width = draw.textlength(".", font=font)
|
||||
x = (image.width - name_width - dot_width) / 2
|
||||
draw.text((x, centre_y), "Playfin", font=font, fill=TEXT, anchor="lm")
|
||||
draw.text((x + name_width, centre_y), ".", font=font, fill=DOT, anchor="lm")
|
||||
draw.text((x + name_width, centre_y), ".", font=font, fill=dot, anchor="lm")
|
||||
|
||||
|
||||
def wordmark(text_colour, dot_colour, size=160, margin=16):
|
||||
""""Playfin." alone, cropped to its ink plus a margin, on a transparent background."""
|
||||
font = ImageFont.truetype(FONT, size)
|
||||
probe = ImageDraw.Draw(Image.new("RGBA", (1, 1)))
|
||||
name_width = probe.textlength("Playfin", font=font)
|
||||
left, top, right, bottom = probe.textbbox((0, 0), "Playfin.", font=font)
|
||||
image = Image.new("RGBA", (right - left + 2 * margin, bottom - top + 2 * margin), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
origin = (margin - left, margin - top)
|
||||
draw.text(origin, "Playfin", font=font, fill=text_colour)
|
||||
draw.text((origin[0] + name_width, origin[1]), ".", font=font, fill=dot_colour)
|
||||
return image
|
||||
|
||||
|
||||
def download_button(scale=2):
|
||||
""""Download for PS4" with an arrow into a tray, white on the accent, drawn at `scale` for sharp screens."""
|
||||
font = ImageFont.truetype(FONT, 22 * scale)
|
||||
label = "Download for PS4"
|
||||
probe = ImageDraw.Draw(Image.new("RGBA", (1, 1)))
|
||||
text_width = probe.textlength(label, font=font)
|
||||
pad, icon, gap, height = 22 * scale, 22 * scale, 12 * scale, 56 * scale
|
||||
image = Image.new("RGBA", (round(pad + icon + gap + text_width + pad), height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rounded_rectangle((0, 0, image.width - 1, height - 1), radius=height // 2, fill=ACCENT)
|
||||
stroke = round(2.6 * scale)
|
||||
left, top = pad, (height - icon) / 2
|
||||
middle = left + icon / 2
|
||||
draw.line((middle, top, middle, top + icon * 0.62), fill=TEXT, width=stroke)
|
||||
head = icon * 0.3
|
||||
draw.line((middle - head, top + icon * 0.62 - head, middle, top + icon * 0.62), fill=TEXT, width=stroke, joint="curve")
|
||||
draw.line((middle + head, top + icon * 0.62 - head, middle, top + icon * 0.62), fill=TEXT, width=stroke, joint="curve")
|
||||
draw.line((left, top + icon * 0.72, left, top + icon, left + icon, top + icon, left + icon, top + icon * 0.72),
|
||||
fill=TEXT, width=stroke, joint="curve")
|
||||
draw.text((left + icon + gap, height / 2), label, font=font, fill=TEXT, anchor="lm")
|
||||
return image
|
||||
|
||||
|
||||
def main():
|
||||
out = os.path.join(ROOT, "resources", "ps4", "sce_sys")
|
||||
icon = halo(512, 512, 0.0, 0.9, 0.9, 0.6)
|
||||
logo(icon, 104, 270)
|
||||
icon = halo(512, 512, 0.0, 0.9, 0.9, 0.6, GRADIENT)
|
||||
logo(icon, 104, 270, GRADIENT[1])
|
||||
icon.save(os.path.join(out, "icon0.png"))
|
||||
picture = halo(1920, 1080, -0.1, 0.7, 0.55, 0.3)
|
||||
logo(picture, 160, 330)
|
||||
picture = halo(1920, 1080, -0.1, 0.7, 0.55, 0.3, GRADIENT)
|
||||
logo(picture, 160, 330, GRADIENT[1])
|
||||
picture.save(os.path.join(out, "pic1.png"))
|
||||
images = os.path.join(ROOT, "docs", "images")
|
||||
os.makedirs(images, exist_ok=True)
|
||||
wordmark(TEXT, DOT).save(os.path.join(images, "logo-on-dark.png"))
|
||||
wordmark(BACKGROUND, ACCENT).save(os.path.join(images, "logo-on-light.png"))
|
||||
download_button().save(os.path.join(images, "download.png"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# The corresponding source of the PS4 package, to attach to a release next to it:
|
||||
# - Playfin's own source at HEAD;
|
||||
# - for every recipe of scripts/ps4: the recipe, its patches and the upstream sources it downloads;
|
||||
# - the build image's libraries and toolchain files that end up in the package (FriBidi, zlib, libsamplerate,
|
||||
# the OpenOrbis toolchain's crt and link.x) with the PacBrew recipes they were built with.
|
||||
# Every download with a known checksum is checked; SHA256SUMS lists every file of the archive.
|
||||
# Usage: scripts/ps4/source-archive.sh [output.tar] (default: dist/playfin-<version>-sources.tar)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
VERSION="$(sed -n 's/^project(Playfin VERSION \([0-9.]*\).*/\1/p' "$ROOT/CMakeLists.txt")"
|
||||
NAME="playfin-$VERSION-sources"
|
||||
OUT="${1:-$ROOT/dist/$NAME.tar}"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
DEST="$WORK/$NAME"
|
||||
mkdir -p "$DEST/recipes" "$DEST/image" "$(dirname "$OUT")"
|
||||
|
||||
# fetch URL FILE SHA256 (SKIP: no published checksum)
|
||||
fetch() {
|
||||
echo " $(basename "$2")"
|
||||
curl -fsSL --retry 3 -o "$2" "$1"
|
||||
if [ "$3" != SKIP ]; then
|
||||
echo "$3 $2" | sha256sum -c --quiet -
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Playfin $VERSION"
|
||||
git -C "$ROOT" archive --prefix="playfin-$VERSION/" -o "$DEST/playfin-$VERSION.tar.gz" HEAD
|
||||
|
||||
echo "Recipes of scripts/ps4"
|
||||
for recipe in "$ROOT"/scripts/ps4/*/PKGBUILD; do
|
||||
dir="$(dirname "$recipe")"
|
||||
name="$(basename "$dir")"
|
||||
mkdir -p "$DEST/recipes/$name"
|
||||
cp "$dir"/* "$DEST/recipes/$name/"
|
||||
# the recipe's source and sha256sums arrays, one "url<TAB>sum" per line
|
||||
entries="$(cd "$dir" && set +u && source ./PKGBUILD && for i in "${!source[@]}"; do
|
||||
printf '%s\t%s\n' "${source[$i]}" "${sha256sums[$i]:-SKIP}"; done)"
|
||||
while IFS=$'\t' read -r src sum; do
|
||||
case "$src" in
|
||||
*::*) file="${src%%::*}" url="${src#*::}" ;;
|
||||
*://*) file="$(basename "$src")" url="$src" ;;
|
||||
*) continue ;; # a local file (patch, cross file): copied with the recipe
|
||||
esac
|
||||
fetch "$url" "$DEST/recipes/$name/$file" "$sum"
|
||||
done <<< "$entries"
|
||||
done
|
||||
|
||||
echo "Build image (xfangfang/pacbrew:250221): libraries and toolchain files in the package"
|
||||
PORTLIBS=d6a523a5a4a7b86846d91134d40a34357721a273
|
||||
TOOLCHAIN_RECIPES=16f27e38760ea21eaf1187d0edbf4342c4a44021
|
||||
TOOLCHAIN=cd480230b946239f758333ef0dff40d67c5f0c20
|
||||
while read -r url file sum; do
|
||||
fetch "$url" "$DEST/image/$file" "$sum"
|
||||
done <<EOF
|
||||
https://github.com/fribidi/fribidi/releases/download/v1.0.4/fribidi-1.0.4.tar.bz2 fribidi-1.0.4.tar.bz2 94bdfe553e004d8bd095b109e182682311dd511740d5083326d1582f1df237be
|
||||
https://zlib.net/fossils/zlib-1.3.1.tar.gz zlib-1.3.1.tar.gz 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
|
||||
https://github.com/libsndfile/libsamplerate/releases/download/0.1.9/libsamplerate-0.1.9.tar.gz libsamplerate-0.1.9.tar.gz 0a7eb168e2f21353fb6d84da152e4512126f7dc48ccb0be80578c565413444c1
|
||||
https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/archive/$TOOLCHAIN.zip OpenOrbis-PS4-Toolchain-$TOOLCHAIN.zip b77f9ba633cfe1cc829220321ea808b8990f4a625ccbad75b4f8f7686a9d8161
|
||||
https://github.com/PacBrew/ps4-openorbis-portlibs/archive/$PORTLIBS.tar.gz pacbrew-ps4-openorbis-portlibs-$PORTLIBS.tar.gz 662559c4771c674e99ae37895a1c6ee64ff1aabf15ad363412f166d44a25b0f8
|
||||
https://github.com/PacBrew/ps4-openorbis/archive/$TOOLCHAIN_RECIPES.tar.gz pacbrew-ps4-openorbis-$TOOLCHAIN_RECIPES.tar.gz f2b8d148def25ea75ea01c8e2a38af498e4d89f9a0e10c5fb8a956620f194c96
|
||||
EOF
|
||||
|
||||
cat > "$DEST/README.txt" <<EOF
|
||||
Corresponding source of Playfin $VERSION for the PS4 (IV0001-PLFN00000_00-PLFN000000100000.pkg).
|
||||
|
||||
playfin-$VERSION.tar.gz Playfin itself (GPL-3.0-or-later).
|
||||
recipes/<name>/ How each library linked into the package was built (pacbrew PKGBUILD, patches,
|
||||
cross files) and the upstream sources it downloads. Built with
|
||||
scripts/ps4/build-deps.sh in the Docker image xfangfang/pacbrew:250221.
|
||||
image/ Libraries and toolchain files taken from that image: FriBidi 1.0.4 (LGPL-2.1),
|
||||
zlib 1.3.1, libsamplerate 0.1.9, the OpenOrbis PS4 Toolchain at $TOOLCHAIN
|
||||
(GPL-3.0: crt and link.x), and the PacBrew recipes they were built with
|
||||
(ps4-openorbis-portlibs, ps4-openorbis).
|
||||
|
||||
The licences are listed in THIRD_PARTY_LICENSES and resources/licenses/ of Playfin's source.
|
||||
SHA256SUMS lists every file of this archive.
|
||||
EOF
|
||||
|
||||
(cd "$DEST" && find . -type f ! -name SHA256SUMS -printf '%P\n' | sort | xargs sha256sum > SHA256SUMS)
|
||||
tar -cf "$OUT" -C "$WORK" "$NAME"
|
||||
echo "$OUT ($(du -h "$OUT" | cut -f1))"
|
||||
@@ -29,7 +29,7 @@ for script in tests/ui/*.script; do
|
||||
sleep 0.1
|
||||
done
|
||||
fi
|
||||
SDL_VIDEODRIVER=offscreen MESA_LOADER_DRIVER_OVERRIDE=llvmpipe timeout 120 \
|
||||
LC_ALL=fr_FR.UTF-8 SDL_VIDEODRIVER=offscreen MESA_LOADER_DRIVER_OVERRIDE=llvmpipe timeout 120 \
|
||||
build/linux-debug/playfin --hidden --no-discovery --screen "$screen" --data-dir "$out/data" \
|
||||
--test-script "$script" --test-output "$out" $appOptions
|
||||
if [ -n "$server" ]; then
|
||||
|
||||
@@ -178,13 +178,13 @@ void ItemCard::draw(NVGcontext* vg) const {
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
void ItemCard::show(const jellyfin::Item& item, const std::string& imageUrl) {
|
||||
void ItemCard::show(const jellyfin::Item& item, const std::string& imageUrl, int utcOffsetMinutes) {
|
||||
// a programme without its own picture shows its channel's logo whole
|
||||
image_->fit = item.type == jellyfin::ItemType::Program && item.images.primary.empty() ? ui::ImageView::Fit::Contain
|
||||
: ui::ImageView::Fit::Cover;
|
||||
image_->setUrl(imageUrl);
|
||||
title_->setText(cardTitle(item));
|
||||
subtitle_->setText(cardSubtitle(item));
|
||||
subtitle_->setText(cardSubtitle(item, utcOffsetMinutes));
|
||||
const double percent = item.userData.playedPercentage;
|
||||
progress_->visible = percent > 0 && percent < 100;
|
||||
progress_->value = static_cast<float>(percent / 100.0);
|
||||
@@ -284,7 +284,8 @@ void ItemsSource::bind(ui::View& view, int index) {
|
||||
const auto client = session_.client();
|
||||
card.show(item, client ? imageUrlFor(*client, item, shape_ == CardShape::Wide, static_cast<int>(size.width),
|
||||
static_cast<int>(size.height))
|
||||
: std::string());
|
||||
: std::string(),
|
||||
session_.utcOffsetMinutes(item.startUtc));
|
||||
card.onPress = [this, index] {
|
||||
if (onPress_ && index < count()) {
|
||||
onPress_(items_[static_cast<std::size_t>(index)]);
|
||||
|
||||
@@ -29,7 +29,7 @@ public:
|
||||
explicit ItemCard(CardShape shape);
|
||||
explicit ItemCard(bool wide) : ItemCard(shapeOf(wide)) {}
|
||||
~ItemCard() override;
|
||||
void show(const jellyfin::Item& item, const std::string& imageUrl);
|
||||
void show(const jellyfin::Item& item, const std::string& imageUrl, int utcOffsetMinutes);
|
||||
std::function<void()> onPress;
|
||||
float focusProgress() const { return focus_; }
|
||||
void onFocusChanged(bool focused) override;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "app/shell/guide.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "ui/i18n.hpp"
|
||||
|
||||
namespace playfin::app {
|
||||
@@ -76,7 +75,7 @@ std::string cardTitle(const jellyfin::Item& item) {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
std::string cardSubtitle(const jellyfin::Item& item) {
|
||||
std::string cardSubtitle(const jellyfin::Item& item, int utcOffsetMinutes) {
|
||||
if (item.type == jellyfin::ItemType::Episode) {
|
||||
return ui::tr("shell.details.season_episode",
|
||||
{std::to_string(item.parentIndexNumber), std::to_string(item.indexNumber)}) +
|
||||
@@ -86,7 +85,7 @@ std::string cardSubtitle(const jellyfin::Item& item) {
|
||||
return !item.albumArtist.empty() ? item.albumArtist : !item.artists.empty() ? item.artists.front() : "";
|
||||
}
|
||||
if (item.type == jellyfin::ItemType::Program && item.startUtc > 0) {
|
||||
return timeRange(item, platform::utcOffsetMinutes(item.startUtc));
|
||||
return timeRange(item, utcOffsetMinutes);
|
||||
}
|
||||
if (item.type == jellyfin::ItemType::Playlist && item.childCount > 0) {
|
||||
return ui::translator().plural("music.track_count", item.childCount);
|
||||
|
||||
@@ -15,8 +15,9 @@ std::string clockText(double seconds);
|
||||
std::string metaLine(const jellyfin::Item& item);
|
||||
// Episodes show their series; everything else its own name.
|
||||
std::string cardTitle(const jellyfin::Item& item);
|
||||
// Episodes: "S1 · E2 Name"; albums and tracks: the artist; playlists: the count; movies and series: the year.
|
||||
std::string cardSubtitle(const jellyfin::Item& item);
|
||||
// Episodes: "S1 · E2 Name"; albums and tracks: the artist; playlists: the count; programmes: their times at
|
||||
// `utcOffsetMinutes` (AppSession::utcOffsetMinutes); movies and series: the year.
|
||||
std::string cardSubtitle(const jellyfin::Item& item, int utcOffsetMinutes = 0);
|
||||
// Wide: episode image, thumb, series thumb, backdrop, series backdrop, primary. Poster: series primary for
|
||||
// episodes, else primary. Empty when the item has no usable image.
|
||||
std::string imageUrlFor(const jellyfin::Client& client, const jellyfin::Item& item, bool wide, int width, int height);
|
||||
|
||||
@@ -6,7 +6,7 @@ playfin=$1 imgdiff=$2 script=$3 refs=$4 out=$5
|
||||
shift 5
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
SDL_VIDEODRIVER=offscreen MESA_LOADER_DRIVER_OVERRIDE=llvmpipe timeout 120 \
|
||||
LC_ALL=fr_FR.UTF-8 SDL_VIDEODRIVER=offscreen MESA_LOADER_DRIVER_OVERRIDE=llvmpipe timeout 120 \
|
||||
"$playfin" --hidden "$@" --data-dir "$out/data" --test-script "$script" --test-output "$out"
|
||||
shopt -s nullglob
|
||||
references=("$refs"/*.png)
|
||||
|
||||
@@ -16,7 +16,7 @@ for _ in $(seq 1 50); do
|
||||
[ -s "$out/url" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
SDL_VIDEODRIVER=offscreen MESA_LOADER_DRIVER_OVERRIDE=llvmpipe timeout 120 \
|
||||
LC_ALL=fr_FR.UTF-8 SDL_VIDEODRIVER=offscreen MESA_LOADER_DRIVER_OVERRIDE=llvmpipe timeout 120 \
|
||||
"$playfin" --hidden --no-discovery --screen shell --data-dir "$out/data" --test-script "$script" --test-output "$out" \
|
||||
$appOptions
|
||||
if grep -q "tok-demo" "$out/data/playfin.log"; then
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core/version.hpp"
|
||||
#include "jellyfin/client.hpp"
|
||||
#include "net/crypto.hpp"
|
||||
#include "support/fake_jellyfin.hpp"
|
||||
@@ -37,8 +38,8 @@ TEST_CASE("authorization header") {
|
||||
Session s = session("http://x");
|
||||
s.deviceName = "PlayStation 4";
|
||||
s.deviceId = "d1";
|
||||
const std::string base =
|
||||
"MediaBrowser Client=\"Playfin\", Device=\"PlayStation 4\", DeviceId=\"d1\", Version=\"0.1.0\"";
|
||||
const std::string base = "MediaBrowser Client=\"Playfin\", Device=\"PlayStation 4\", DeviceId=\"d1\", Version=\"" +
|
||||
std::string(core::version()) + "\"";
|
||||
CHECK(jellyfin::authorizationHeader(s) == base);
|
||||
s.token = "t\"x,y\n";
|
||||
CHECK(jellyfin::authorizationHeader(s) == base + ", Token=\"txy\"");
|
||||
|
||||
@@ -65,6 +65,11 @@ TEST_CASE("item texts") {
|
||||
const auto episode = fixtureItem("jellyfin/item_episode.json");
|
||||
CHECK(app::cardTitle(episode) == "Test Show");
|
||||
CHECK(app::cardSubtitle(episode) == "S1 · E1 Pilot");
|
||||
jellyfin::Item program;
|
||||
program.type = jellyfin::ItemType::Program;
|
||||
program.startUtc = jellyfin::parseUtc("2026-10-16T19:30:00Z");
|
||||
program.endUtc = jellyfin::parseUtc("2026-10-16T20:30:00Z");
|
||||
CHECK(app::cardSubtitle(program, 120) == "21:30 \xE2\x80\x93 22:30"); // the app's clock (Paris), not the machine's
|
||||
}
|
||||
|
||||
TEST_CASE("image choices: thumbnails for wide cards, series posters for episodes") {
|
||||
|
||||