mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(app): bundled Inter and Phosphor icons, window icon
Headings are set in Inter SemiBold, everything else in Inter Regular, so a
title sets the same on every desktop. Media and navigation glyphs (⏭, 🔊, ⛶,
★) come from the Phosphor icon font instead of whichever fallback font had
them. The CJK fallback still loads on its own thread and now joins the
bundled fonts instead of replacing them. The window carries the app icon
on X11 and Windows.
This commit is contained in:
1 parent
6518aef5ee
commit
558446b9e4
17 files changed
+251
-40
No files matched your search
Generated
+10
@@ -1767,6 +1767,15 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "egui-phosphor"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "249826f9529c1757d71dbbb3a22ec101b21110ab663c45fb771c2b86121379e4"
|
||||||
|
dependencies = [
|
||||||
|
"egui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-wgpu"
|
name = "egui-wgpu"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
@@ -4399,6 +4408,7 @@ dependencies = [
|
|||||||
"directories",
|
"directories",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
|
"egui-phosphor",
|
||||||
"futures",
|
"futures",
|
||||||
"image",
|
"image",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ eframe = { version = "0.35", default-features = false, features = [
|
|||||||
"x11",
|
"x11",
|
||||||
] }
|
] }
|
||||||
egui = "0.35"
|
egui = "0.35"
|
||||||
|
# The icons on buttons. Glyphs like ⏭ and 🔊 come out of whichever fallback
|
||||||
|
# font has them, at its own size and baseline; an icon font is one set.
|
||||||
|
egui-phosphor = { version = "0.13", default-features = false, features = ["regular"] }
|
||||||
# Poster thumbnails only: Proton renders them as JPEG, and a decoder that also
|
# Poster thumbnails only: Proton renders them as JPEG, and a decoder that also
|
||||||
# pulls in every exotic format is weight this app has no use for.
|
# pulls in every exotic format is weight this app has no use for.
|
||||||
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
|
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
|
||||||
|
|||||||
@@ -115,3 +115,6 @@ see [`docs/ANDROID.md`](docs/ANDROID.md) for exact setup and release signing.
|
|||||||
The shared Rust crates and desktop application are MIT licensed. The Android
|
The shared Rust crates and desktop application are MIT licensed. The Android
|
||||||
application and its combined native distribution are GPL-3.0-or-later; see
|
application and its combined native distribution are GPL-3.0-or-later; see
|
||||||
[`android/LICENSE.md`](android/LICENSE.md) and the bundled-component notices.
|
[`android/LICENSE.md`](android/LICENSE.md) and the bundled-component notices.
|
||||||
|
The desktop application embeds the Inter typeface, under the SIL Open Font
|
||||||
|
License 1.1 ([`crates/pstr-app/assets/fonts/Inter-OFL.txt`](crates/pstr-app/assets/fonts/Inter-OFL.txt)),
|
||||||
|
and the Phosphor icon font, under MIT.
|
||||||
@@ -24,6 +24,7 @@ tracing.workspace = true
|
|||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
eframe.workspace = true
|
eframe.workspace = true
|
||||||
egui.workspace = true
|
egui.workspace = true
|
||||||
|
egui-phosphor.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION AND CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -479,7 +479,7 @@ impl App {
|
|||||||
// before switching is a window that flashes on every launch.
|
// before switching is a window that flashes on every launch.
|
||||||
let window = crate::window::WindowMemory::new(dirs.window_file());
|
let window = crate::window::WindowMemory::new(dirs.window_file());
|
||||||
let (engine, events) = Engine::new(runtime, dirs, cc.egui_ctx.clone())?;
|
let (engine, events) = Engine::new(runtime, dirs, cc.egui_ctx.clone())?;
|
||||||
theme::install_font_fallbacks(&cc.egui_ctx);
|
theme::install_fonts(&cc.egui_ctx);
|
||||||
theme::apply(&cc.egui_ctx, engine.appearance());
|
theme::apply(&cc.egui_ctx, engine.appearance());
|
||||||
|
|
||||||
// Paint from the catalog immediately; the network catches up. A library
|
// Paint from the catalog immediately; the network catches up. A library
|
||||||
@@ -1493,7 +1493,10 @@ impl App {
|
|||||||
ui.add(egui::Spinner::new().size(16.0));
|
ui.add(egui::Spinner::new().size(16.0));
|
||||||
ui.label(ui::muted("connecting"));
|
ui.label(ui::muted("connecting"));
|
||||||
} else if ui
|
} else if ui
|
||||||
.button("Refresh")
|
.button(format!(
|
||||||
|
"{} Refresh",
|
||||||
|
egui_phosphor::regular::ARROW_CLOCKWISE
|
||||||
|
))
|
||||||
.on_hover_text("Re-crawl every share")
|
.on_hover_text("Re-crawl every share")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,8 +32,15 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let dirs = AppDirs::ensure().context("resolve app directories")?;
|
let dirs = AppDirs::ensure().context("resolve app directories")?;
|
||||||
|
|
||||||
let window = pstr_app::window::WindowState::load(&dirs.window_file());
|
let window = pstr_app::window::WindowState::load(&dirs.window_file());
|
||||||
|
let mut viewport = egui::ViewportBuilder::default();
|
||||||
|
// X11 and Windows take the icon from the window. Wayland ignores this and
|
||||||
|
// looks the app id up in the installed desktop entry instead.
|
||||||
|
match eframe::icon_data::from_png_bytes(include_bytes!("../assets/icon-256.png")) {
|
||||||
|
Ok(icon) => viewport = viewport.with_icon(icon),
|
||||||
|
Err(error) => tracing::debug!("window icon: {error}"),
|
||||||
|
}
|
||||||
let mut options = eframe::NativeOptions {
|
let mut options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: viewport
|
||||||
.with_title("proton-stream")
|
.with_title("proton-stream")
|
||||||
.with_inner_size(window.map_or([1180.0, 780.0], |state| [state.width, state.height]))
|
.with_inner_size(window.map_or([1180.0, 780.0], |state| [state.width, state.height]))
|
||||||
.with_maximized(window.is_some_and(|state| state.maximized))
|
.with_maximized(window.is_some_and(|state| state.maximized))
|
||||||
|
|||||||
@@ -84,26 +84,90 @@ impl Role {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Headings are set in the semibold cut; everything below them in the
|
||||||
|
/// regular one.
|
||||||
|
pub fn family(self) -> egui::FontFamily {
|
||||||
|
match self {
|
||||||
|
Self::Display | Self::Title | Self::Heading | Self::Section => semibold(),
|
||||||
|
_ => egui::FontFamily::Proportional,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// For the hand-painted parts, which lay text out through a `Painter` and
|
/// For the hand-painted parts, which lay text out through a `Painter` and
|
||||||
/// never see a `RichText`.
|
/// never see a `RichText`.
|
||||||
pub fn font(self) -> egui::FontId {
|
pub fn font(self) -> egui::FontId {
|
||||||
egui::FontId::proportional(self.size())
|
egui::FontId::new(self.size(), self.family())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For the parts that go through egui's widgets.
|
/// For the parts that go through egui's widgets.
|
||||||
pub fn rich(self, text: impl Into<String>) -> egui::RichText {
|
pub fn rich(self, text: impl Into<String>) -> egui::RichText {
|
||||||
egui::RichText::new(text).size(self.size())
|
egui::RichText::new(text)
|
||||||
|
.size(self.size())
|
||||||
|
.family(self.family())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install a real CJK fallback before the default egui fonts. egui ships a
|
/// The UI typeface, bundled rather than taken from the system so a title
|
||||||
/// compact Latin font, but it intentionally does not bundle the multi-megabyte
|
/// sets the same on every desktop. Inter, under the OFL — the licence is
|
||||||
/// CJK families. Native desktops already provide one, so use it when present.
|
/// beside the files.
|
||||||
|
const INTER: &[u8] = include_bytes!("../assets/fonts/Inter-Regular.ttf");
|
||||||
|
const INTER_SEMIBOLD: &[u8] = include_bytes!("../assets/fonts/Inter-SemiBold.ttf");
|
||||||
|
|
||||||
|
/// The family the headings are set in. egui has no notion of weight, so the
|
||||||
|
/// semibold cut is a family of its own.
|
||||||
|
pub fn semibold() -> egui::FontFamily {
|
||||||
|
egui::FontFamily::Name("semibold".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inter, the icon font, and egui's own fonts behind them for the emoji and
|
||||||
|
/// symbols Inter does not carry. `cjk` goes last, when there is one.
|
||||||
|
fn font_definitions(cjk: Option<egui::FontData>) -> egui::FontDefinitions {
|
||||||
|
use egui::FontFamily::{Monospace, Proportional};
|
||||||
|
|
||||||
|
let mut fonts = egui::FontDefinitions::default();
|
||||||
|
fonts
|
||||||
|
.font_data
|
||||||
|
.insert("inter".into(), Arc::new(egui::FontData::from_static(INTER)));
|
||||||
|
fonts.font_data.insert(
|
||||||
|
"inter-semibold".into(),
|
||||||
|
Arc::new(egui::FontData::from_static(INTER_SEMIBOLD)),
|
||||||
|
);
|
||||||
|
fonts
|
||||||
|
.families
|
||||||
|
.entry(Proportional)
|
||||||
|
.or_default()
|
||||||
|
.insert(0, "inter".into());
|
||||||
|
// Right behind Inter. The icons are in the private use area, so nothing
|
||||||
|
// Inter has is shadowed by them.
|
||||||
|
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
|
||||||
|
if let Some(cjk) = cjk {
|
||||||
|
let name = "system-cjk-fallback".to_owned();
|
||||||
|
fonts.font_data.insert(name.clone(), Arc::new(cjk));
|
||||||
|
for family in [Proportional, Monospace] {
|
||||||
|
fonts.families.entry(family).or_default().push(name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fallbacks = fonts.families[&Proportional]
|
||||||
|
.iter()
|
||||||
|
.filter(|name| *name != "inter")
|
||||||
|
.cloned();
|
||||||
|
let semibold_chain = std::iter::once("inter-semibold".to_owned())
|
||||||
|
.chain(fallbacks)
|
||||||
|
.collect();
|
||||||
|
fonts.families.insert(semibold(), semibold_chain);
|
||||||
|
fonts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install the bundled fonts now, and a CJK fallback when the system has one.
|
||||||
///
|
///
|
||||||
/// On a thread of its own: `fc-match` and reading a font file of tens of
|
/// The bundled ones are compiled in and cost nothing to hand over. The CJK
|
||||||
/// megabytes held the first frame back by a visible moment, and every name
|
/// fallback is on a thread of its own: `fc-match` and reading a font file of
|
||||||
/// that needs the fallback is still loading from the catalog by then anyway.
|
/// tens of megabytes held the first frame back by a visible moment, and every
|
||||||
pub fn install_font_fallbacks(ctx: &egui::Context) {
|
/// name that needs the fallback is still loading from the catalog by then
|
||||||
|
/// anyway. egui deliberately does not bundle the multi-megabyte CJK families,
|
||||||
|
/// and native desktops already provide one.
|
||||||
|
pub fn install_fonts(ctx: &egui::Context) {
|
||||||
|
ctx.set_fonts(font_definitions(None));
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let spawned = std::thread::Builder::new()
|
let spawned = std::thread::Builder::new()
|
||||||
.name("fonts".into())
|
.name("fonts".into())
|
||||||
@@ -120,16 +184,9 @@ fn install_cjk_fallback(ctx: &egui::Context) {
|
|||||||
let Ok(bytes) = std::fs::read(path) else {
|
let Ok(bytes) = std::fs::read(path) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut fonts = egui::FontDefinitions::default();
|
|
||||||
let mut data = egui::FontData::from_owned(bytes);
|
let mut data = egui::FontData::from_owned(bytes);
|
||||||
data.index = index;
|
data.index = index;
|
||||||
let name = "system-cjk-fallback".to_owned();
|
ctx.set_fonts(font_definitions(Some(data)));
|
||||||
fonts.font_data.insert(name.clone(), Arc::new(data));
|
|
||||||
for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] {
|
|
||||||
fonts.families.entry(family).or_default().push(name.clone());
|
|
||||||
}
|
|
||||||
ctx.set_fonts(fonts);
|
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,9 +211,11 @@ fn hero(ui: &mut egui::Ui, art: &mut Art<'_>, title: &Title, actions: &mut Vec<A
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if let Some(episode) = title.next_up() {
|
if let Some(episode) = title.next_up() {
|
||||||
let label = match (title.resume(), episode.numbering()) {
|
let label = match (title.resume(), episode.numbering()) {
|
||||||
(Some(_), Some(numbering)) => format!("▶ Resume {numbering}"),
|
(Some(_), Some(numbering)) => {
|
||||||
(Some(_), None) => "▶ Resume".to_owned(),
|
format!("{} Resume {numbering}", egui_phosphor::regular::PLAY)
|
||||||
(None, _) => "▶ Play".to_owned(),
|
}
|
||||||
|
(Some(_), None) => format!("{} Resume", egui_phosphor::regular::PLAY),
|
||||||
|
(None, _) => format!("{} Play", egui_phosphor::regular::PLAY),
|
||||||
};
|
};
|
||||||
if ui::accent_button(ui, &label).clicked() {
|
if ui::accent_button(ui, &label).clicked() {
|
||||||
actions.push(Action::Play(PlaybackTarget::new(title, episode)));
|
actions.push(Action::Play(PlaybackTarget::new(title, episode)));
|
||||||
@@ -239,7 +241,7 @@ fn hero(ui: &mut egui::Ui, art: &mut Art<'_>, title: &Title, actions: &mut Vec<A
|
|||||||
let mut facts = vec![subtitle(title)];
|
let mut facts = vec![subtitle(title)];
|
||||||
if let Some(found) = &found {
|
if let Some(found) = &found {
|
||||||
if let Some(rating) = found.rating {
|
if let Some(rating) = found.rating {
|
||||||
facts.push(format!("★ {rating:.1}"));
|
facts.push(format!("{} {rating:.1}", egui_phosphor::regular::STAR));
|
||||||
}
|
}
|
||||||
facts.extend(found.genres.iter().take(3).cloned());
|
facts.extend(found.genres.iter().take(3).cloned());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ fn facts(found: &TitleMetadata) -> String {
|
|||||||
parts.push(ui::library::plural(episodes as usize, "episode"));
|
parts.push(ui::library::plural(episodes as usize, "episode"));
|
||||||
}
|
}
|
||||||
if let Some(rating) = found.rating {
|
if let Some(rating) = found.rating {
|
||||||
parts.push(format!("★ {rating:.1}"));
|
parts.push(format!("{} {rating:.1}", egui_phosphor::regular::STAR));
|
||||||
}
|
}
|
||||||
parts.join(" · ")
|
parts.join(" · ")
|
||||||
}
|
}
|
||||||
@@ -657,14 +657,23 @@ fn chrome(
|
|||||||
});
|
});
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
let fullscreen = ui.input(|input| input.viewport().fullscreen.unwrap_or(false));
|
||||||
if ui
|
if ui
|
||||||
.add(
|
.add(
|
||||||
egui::Button::new(theme::Role::Subhead.rich("⛶"))
|
egui::Button::new(theme::Role::Subhead.rich(if fullscreen {
|
||||||
|
egui_phosphor::regular::CORNERS_IN
|
||||||
|
} else {
|
||||||
|
egui_phosphor::regular::CORNERS_OUT
|
||||||
|
}))
|
||||||
.fill(Color32::from_black_alpha(160))
|
.fill(Color32::from_black_alpha(160))
|
||||||
.corner_radius(CornerRadius::same(8))
|
.corner_radius(CornerRadius::same(8))
|
||||||
.min_size(Vec2::new(38.0, 32.0)),
|
.min_size(Vec2::new(38.0, 32.0)),
|
||||||
)
|
)
|
||||||
.on_hover_text("Fullscreen (F)")
|
.on_hover_text(if fullscreen {
|
||||||
|
"Leave fullscreen (F)"
|
||||||
|
} else {
|
||||||
|
"Fullscreen (F)"
|
||||||
|
})
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
actions.push(Action::ToggleFullscreen);
|
actions.push(Action::ToggleFullscreen);
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ fn poster(ui: &mut egui::Ui, texture: &egui::TextureHandle, progress: Option<f64
|
|||||||
/// "← Library", as a link rather than a button: it is navigation, and a
|
/// "← Library", as a link rather than a button: it is navigation, and a
|
||||||
/// filled button beside the page's real actions competed with them.
|
/// filled button beside the page's real actions competed with them.
|
||||||
fn back_link(ui: &mut egui::Ui) -> egui::Response {
|
fn back_link(ui: &mut egui::Ui) -> egui::Response {
|
||||||
let text = theme::Role::Label.rich("← Library");
|
let text = theme::Role::Label.rich(format!("{} Library", egui_phosphor::regular::ARROW_LEFT));
|
||||||
let response = ui.add(egui::Label::new(text.color(theme::muted())).sense(egui::Sense::click()));
|
let response = ui.add(egui::Label::new(text.color(theme::muted())).sense(egui::Sense::click()));
|
||||||
if response.hovered() {
|
if response.hovered() {
|
||||||
ui.painter().hline(
|
ui.painter().hline(
|
||||||
@@ -532,7 +532,7 @@ fn meta_line(title: &Title, found: Option<&TitleMetadata>) -> String {
|
|||||||
|
|
||||||
if let Some(found) = found {
|
if let Some(found) = found {
|
||||||
if let Some(rating) = found.rating {
|
if let Some(rating) = found.rating {
|
||||||
parts.push(format!("★ {rating:.1}"));
|
parts.push(format!("{} {rating:.1}", egui_phosphor::regular::STAR));
|
||||||
}
|
}
|
||||||
// Only when it disagrees with what is on disk: "25 episodes · 25
|
// Only when it disagrees with what is on disk: "25 episodes · 25
|
||||||
// episodes" tells nobody anything, but "12 episodes · 25 on AniList"
|
// episodes" tells nobody anything, but "12 episodes · 25 on AniList"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ pub fn full(
|
|||||||
// hand reaches for it.
|
// hand reaches for it.
|
||||||
step_button(
|
step_button(
|
||||||
ui,
|
ui,
|
||||||
"⏮",
|
egui_phosphor::regular::SKIP_BACK,
|
||||||
"Previous (P)",
|
"Previous (P)",
|
||||||
neighbours.previous,
|
neighbours.previous,
|
||||||
actions,
|
actions,
|
||||||
@@ -76,9 +76,16 @@ pub fn full(
|
|||||||
skip_button(ui, "−10s", "Back ten seconds (←)", -10.0, actions);
|
skip_button(ui, "−10s", "Back ten seconds (←)", -10.0, actions);
|
||||||
play_pause(ui, playback, actions, 44.0);
|
play_pause(ui, playback, actions, 44.0);
|
||||||
skip_button(ui, "+30s", "Forward thirty seconds (→)", 30.0, actions);
|
skip_button(ui, "+30s", "Forward thirty seconds (→)", 30.0, actions);
|
||||||
step_button(ui, "⏭", "Next (N)", neighbours.next, actions, |a| {
|
step_button(
|
||||||
|
ui,
|
||||||
|
egui_phosphor::regular::SKIP_FORWARD,
|
||||||
|
"Next (N)",
|
||||||
|
neighbours.next,
|
||||||
|
actions,
|
||||||
|
|a| {
|
||||||
a.push(Action::PlayAdjacent(Adjacent::Next));
|
a.push(Action::PlayAdjacent(Adjacent::Next));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ui.add_space(12.0);
|
ui.add_space(12.0);
|
||||||
if playback.seeking || playback.buffering || !playback.loaded {
|
if playback.seeking || playback.buffering || !playback.loaded {
|
||||||
@@ -202,13 +209,20 @@ pub fn mini(
|
|||||||
actions.push(Action::Player(Command::Stop));
|
actions.push(Action::Player(Command::Stop));
|
||||||
}
|
}
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
step_button(ui, "⏭", "Next (N)", neighbours.next, actions, |a| {
|
step_button(
|
||||||
|
ui,
|
||||||
|
egui_phosphor::regular::SKIP_FORWARD,
|
||||||
|
"Next (N)",
|
||||||
|
neighbours.next,
|
||||||
|
actions,
|
||||||
|
|a| {
|
||||||
a.push(Action::PlayAdjacent(Adjacent::Next));
|
a.push(Action::PlayAdjacent(Adjacent::Next));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
play_pause(ui, playback, actions, 34.0);
|
play_pause(ui, playback, actions, 34.0);
|
||||||
step_button(
|
step_button(
|
||||||
ui,
|
ui,
|
||||||
"⏮",
|
egui_phosphor::regular::SKIP_BACK,
|
||||||
"Previous (P)",
|
"Previous (P)",
|
||||||
neighbours.previous,
|
neighbours.previous,
|
||||||
actions,
|
actions,
|
||||||
@@ -414,12 +428,14 @@ fn volume(ui: &mut egui::Ui, playback: &Playback, actions: &mut Vec<Action>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let icon = if playback.muted || playback.volume <= 0.0 {
|
let icon = if playback.muted || playback.volume <= 0.0 {
|
||||||
"🔇"
|
egui_phosphor::regular::SPEAKER_SLASH
|
||||||
|
} else if playback.volume < 50.0 {
|
||||||
|
egui_phosphor::regular::SPEAKER_LOW
|
||||||
} else {
|
} else {
|
||||||
"🔊"
|
egui_phosphor::regular::SPEAKER_HIGH
|
||||||
};
|
};
|
||||||
if ui
|
if ui
|
||||||
.button(icon)
|
.button(theme::Role::Subhead.rich(icon))
|
||||||
.on_hover_text(format!("Mute (M) — {:.0}%", playback.volume))
|
.on_hover_text(format!("Mute (M) — {:.0}%", playback.volume))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
|
|||||||
+10
-2
@@ -35,8 +35,16 @@ screen and not for anything in a library grid).
|
|||||||
| Caption | 12 | context rather than content: counts, sizes, errors under a field |
|
| Caption | 12 | context rather than content: counts, sizes, errors under a field |
|
||||||
| Micro | 11 | the smallest thing that stays legible — a badge over artwork |
|
| Micro | 11 | the smallest thing that stays legible — a badge over artwork |
|
||||||
|
|
||||||
Weight and leading are each side's own: egui has one weight, and Compose derives
|
Weight and leading are each side's own. The desktop bundles Inter
|
||||||
line height as 1.35 × size rather than naming it nine more times.
|
(`pstr-app/assets/fonts`, OFL) and sets Display, Title, Heading and Section in
|
||||||
|
its semibold cut, which egui sees as a family of its own (`theme::semibold`);
|
||||||
|
everything below Section is the regular cut. Compose derives line height as
|
||||||
|
1.35 × size rather than naming it nine more times.
|
||||||
|
|
||||||
|
Icons on the desktop come from the Phosphor icon font (`egui-phosphor`,
|
||||||
|
regular weight), installed right behind Inter so an icon can sit inside an
|
||||||
|
ordinary label. Do not use Unicode symbols such as ⏭ or 🔊 as icons: they come
|
||||||
|
out of whichever fallback font has them, at that font's size and baseline.
|
||||||
|
|
||||||
Both clients point their framework's *default* styles at the ramp — egui's five
|
Both clients point their framework's *default* styles at the ramp — egui's five
|
||||||
`TextStyle`s, Compose's fifteen — so a stock widget nobody restyled still lands
|
`TextStyle`s, Compose's fifteen — so a stock widget nobody restyled still lands
|
||||||
|
|||||||
Reference in new issue
Block a user