Supporters list

This commit is contained in:
Aleksander 2026-08-08 13:52:42 +02:00
parent cae2d7c7e2
commit c85d96c4d9
12 changed files with 223 additions and 64 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M14.94 19.5L12 17.77L9.06 19.5l.78-3.34l-2.59-2.24l3.41-.29L12 10.5l1.34 3.13l3.41.29l-2.59 2.24M20 2H4v2l4.86 3.64a8 8 0 1 0 6.28 0L20 4m-2 11a6 6 0 1 1-7.18-5.88a5.9 5.9 0 0 1 2.36 0A6 6 0 0 1 18 15m-5.37-8h-1.26l-4-3h9.34Z"/></svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M9.175 10.825Q8 9.65 8 8t1.175-2.825T12 4t2.825 1.175T16 8t-1.175 2.825T12 12t-2.825-1.175M4 20v-2.8q0-.85.438-1.562T5.6 14.55q1.55-.775 3.15-1.162T12 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T20 17.2V20z"/></svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -1,5 +1,5 @@
<layout>
<template name="Separator">
<rectangle width="100%" min_height="2" color="#FFFFFF10" />
<rectangle width="100%" min_height="2" color="outline" />
</template>
</layout>

View File

@ -1,47 +1,65 @@
<layout>
<include src="../t_group_box.xml" />
<include src="../t_separator.xml" />
<template name="TierCell">
<div gap="8" align_items="center">
<sprite width="28" height="28" src_builtin="dashboard/medal.svg" color="${color}"/>
<label text="${text}" weight="bold" size="18" color="${color}" shadow="shadow"/>
</div>
</template>
<template name="SupporterCell">
<rectangle padding="8" color="background(transparent)" round="8" border="2" border_color="outline" gap="8" align_items="center" flex_direction="row" >
<sprite width="24" height="24" src_builtin="dashboard/person.svg"/>
<label text="${username}" weight="bold"/>
</rectangle>
</template>
<!-- icon, nickname -->
<template name="Avatar">
<div flex_direction="column" gap="12" align_items="center">
<image src_builtin="${icon}" width="72" height="72" round="100%" position="relative">
<rectangle position="absolute" margin_left="-2" margin_top="-2" width="76" height="76" color="highlight" round="100%"/>
<image src_builtin="${icon}" width="64" height="64" round="100%" position="relative">
<rectangle position="absolute" margin_left="-2" margin_top="-2" width="68" height="68" color="highlight" round="100%"/>
</image>
<label size="14" text="${nickname}"/>
</div>
</template>
<macro name="half"
width="50%" gap="16" flex_direction="column" overflow_y="scroll"
align_items="center"/>
<elements>
<div position="absolute" width="100%" height="100%" padding="4">
<!-- <rectangle width="100%" height="100%" color="#000000" round="6"/> -->
</div>
<div margin="32" flex_direction="column" align_items="center" justify_content="center" gap="16" height="100%">
<div gap="16" align_items="center" width="100%">
<sprite color="#FFAAAA" src_builtin="dashboard/donate.svg" min_width="64" min_height="64"/>
<label wrap="1" size="20" translation="DONATE.DESCRIPTION" color="on_background"/>
<div flex_direction="row" width="100%" min_height="100%" gap="16">
<div macro="half">
<sprite color="#FFAAAA" src_builtin="dashboard/donate.svg" min_width="48" min_height="48"/>
<div gap="16" align_items="center" width="100%">
<label align="justified" wrap="1" size="18" translation="DONATE.DESCRIPTION" color="on_background"/>
</div>
<div flex_direction="column" gap="8" align_items="center">
<Button id="btn_donate" tooltip="DONATE.BUTTON_TOOLTIP" align_self="baseline">
<image src_builtin="dashboard/opencollective_logo.svg" width="200" height="32" margin="8" margin_left="16" margin_right="16"/>
</Button>
<label text="opencollective.com/wayvr-org"/>
</div>
<label weight="bold" size="20" translation="DONATE.THANK_YOU_WAYVR" color="on_background"/>
<div gap="32">
<Avatar icon="dashboard/avatars/galister.png" nickname="galister"/>
<Avatar icon="dashboard/avatars/oo8dev.png" nickname="oo8dev"/>
</div>
</div>
<div flex_direction="column" gap="8" align_items="center" margin_top="16">
<Button id="btn_donate" tooltip="DONATE.BUTTON_TOOLTIP" align_self="baseline">
<image src_builtin="dashboard/opencollective_logo.svg" width="252" height="42" margin="8" margin_left="16" margin_right="16"/>
</Button>
<label text="opencollective.com/wayvr-org"/>
<!-- vertical separator -->
<rectangle height="100%" min_width="2" color="outline"/>
<div macro="half">
<div gap="8" align_items="center">
<label translation="DONATE.CURRENT_SUPPORTERS" size="18" weight="bold" />
<Button round="100%" text="?" tooltip_str="In last 30 days; the list is updated each day" width="24" height="24"/>
</div>
<div id="current_supporters" flex_direction="column" gap="8" width="100%"/>
</div>
<label weight="bold" size="24" translation="DONATE.THANK_YOU_WAYVR" color="on_background"/>
<div gap="32" margin_top="16">
<Avatar icon="dashboard/avatars/galister.png" nickname="galister"/>
<Avatar icon="dashboard/avatars/oo8dev.png" nickname="oo8dev"/>
</div>
<!--
<rectangle color="#FFFFFF33" width="100%" height="2"/>
<rectangle macro="group_box" align_self="start" width="100%">
<label weight="bold" size="18" translation="DONATE.CURRENT_SUPPORTERS"/>
</rectangle>
-->
</div>
</elements>
</layout>

View File

@ -4,19 +4,23 @@ use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
layout::WidgetID,
parser::{Fetchable, ParseDocumentParams, ParserState},
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
taffy::{self, style_helpers::length},
task::Tasks,
widget::div::WidgetDiv,
};
use crate::{
frontend::{Frontend, FrontendTask},
tab::{Tab, TabType},
util::cached_fetcher,
};
#[derive(Clone)]
#[allow(clippy::enum_variant_names)]
enum Task {}
enum Task {
SetSupporters(cached_fetcher::Supporters),
}
pub struct TabDonate<T> {
#[allow(dead_code)]
@ -24,6 +28,8 @@ pub struct TabDonate<T> {
marker: PhantomData<T>,
#[allow(dead_code)]
tasks: Tasks<Task>,
id_current_supporters: WidgetID,
}
impl<T> Tab<T> for TabDonate<T> {
@ -31,10 +37,14 @@ impl<T> Tab<T> for TabDonate<T> {
TabType::Donate
}
fn update(&mut self, _frontend: &mut Frontend<T>, _time_ms: u32, _user_data: &mut T) -> anyhow::Result<()> {
// for task in self.tasks.drain() {
// match task {}
// }
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, _user_data: &mut T) -> anyhow::Result<()> {
while !self.tasks.is_empty() {
for task in self.tasks.drain() {
match task {
Task::SetSupporters(supporters) => self.set_supporters(&mut frontend.layout, supporters)?,
}
}
}
Ok(())
}
@ -48,9 +58,76 @@ fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
}
}
async fn request_supporters(tasks: Tasks<Task>) {
if let Some(supporters) = cached_fetcher::request_supporters().await {
tasks.push(Task::SetSupporters(supporters))
}
}
fn tier_pretty_print(tier: &str) -> String {
format!("{} Tier", wlx_common::locale::capitalize_string(tier))
}
fn tier_color(tier: &str) -> &'static str {
match tier {
"platinum" => "#aaffff",
"gold" => "#ffffaa",
"silver" => "#cccccc",
"bronze" => "#ffaa66",
_ => "on_background",
}
}
impl<T> TabDonate<T> {
fn set_supporters(&mut self, layout: &mut Layout, supporters: cached_fetcher::Supporters) -> anyhow::Result<()> {
let globals = layout.state.globals.clone();
layout.remove_children(self.id_current_supporters);
let mut current_tier = "";
let mut tier_parent = WidgetID::default();
for supporter in &supporters.supporters {
if supporter.tier != current_tier {
current_tier = &supporter.tier;
let mut params = TemplateParams::new();
params.insert_str("text", tier_pretty_print(&supporter.tier));
params.insert("color", tier_color(&supporter.tier));
self.state.realize_template(
&doc_params(&globals),
"TierCell",
layout,
self.id_current_supporters,
params,
)?;
tier_parent = layout
.add_child(
self.id_current_supporters,
WidgetDiv::create(),
taffy::Style {
gap: length(8.0_f32),
flex_wrap: taffy::FlexWrap::Wrap,
..Default::default()
},
)?
.0
.id;
}
let mut params = TemplateParams::new();
params.insert("username", &supporter.username);
self
.state
.realize_template(&doc_params(&globals), "SupporterCell", layout, tier_parent, params)?;
}
Ok(())
}
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, _data: &mut T) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(&doc_params(&frontend.globals), &mut frontend.layout, parent_id)?;
let id_current_supporters = state.get_widget_id("current_supporters")?;
frontend.tasks.handle_button(
&state.fetch_component_as::<ComponentButton>("btn_donate")?,
@ -59,10 +136,13 @@ impl<T> TabDonate<T> {
let tasks = Tasks::<Task>::new();
frontend.executor.spawn(request_supporters(tasks.clone())).detach();
Ok(Self {
state,
marker: PhantomData,
tasks,
id_current_supporters,
})
}
}

View File

@ -30,12 +30,7 @@ impl<T> Tab<T> for TabHome<T> {
}
fn configure_label_hello(common: &mut CallbackDataCommon, label_hello: Widget, config: &GeneralConfig) {
let mut username = various::get_username();
// first character as uppercase
if let Some(first) = username.chars().next() {
let first = first.to_uppercase().to_string();
username.replace_range(0..1, &first);
}
let username = wlx_common::locale::capitalize_string(&various::get_username());
let translated = if !config.hide_username {
common.i18n().translate_and_replace("HELLO_USER", ("{USER}", &username))

View File

@ -24,7 +24,7 @@ pub enum TabType {
impl TabType {
pub fn get_preferred_padding(&self) -> f32 {
match self {
TabType::Welcome | TabType::Donate => 0.0,
TabType::Welcome => 0.0,
_ => 16.0,
}
}

View File

@ -1,14 +1,73 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::util::{networking::http_client, steam_utils::AppID};
use anyhow::Context;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use wlx_common::cache_dir;
fn get_unix_timestamp() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
}
#[derive(Serialize, Deserialize)]
pub struct Supporter {
pub username: String,
pub date: String,
pub tier: String,
pub contribution_count: u32,
}
#[derive(Serialize, Deserialize)]
pub struct Supporters {
pub time_window_days: u32,
pub supporters: Vec<Supporter>,
}
#[derive(Serialize, Deserialize)]
struct SupportersFile {
fetch_timestamp: u64,
supporters: Option<Supporters>, // empty in case of server/network failure
}
pub async fn request_supporters() -> Option<Supporters> {
let cache_file_path = "supporters_v1.json";
const CACHE_DURATION_SECS: u64 = 7200;
let current_timestamp = get_unix_timestamp();
if let Some(data) = cache_dir::get_data(cache_file_path).await
&& let Ok(string) = str::from_utf8(&data)
&& let Ok(file) = serde_json::from_str::<SupportersFile>(string)
&& file.fetch_timestamp + CACHE_DURATION_SECS > current_timestamp
{
return file.supporters;
}
// perform request
let mut supporters_file = SupportersFile {
fetch_timestamp: current_timestamp,
supporters: None,
};
let url = "https://wayvr.org/files/supporters_v1.json";
if let Ok(res) = http_client::get_simple(url).await
&& let Ok(supporters) = res.into_json::<Supporters>()
{
supporters_file.supporters = Some(supporters);
}
let json = serde_json::to_string_pretty(&supporters_file).unwrap() /* safe */;
cache_dir::set_data(cache_file_path, json.as_bytes()).await.ok()?;
supporters_file.supporters
}
pub struct CoverArt {
// can be empty in case if data couldn't be fetched (use a fallback image then)
pub compressed_image_data: Vec<u8>,
}
pub async fn request_image(app_id: AppID) -> anyhow::Result<CoverArt> {
pub async fn request_cover_art(app_id: AppID) -> anyhow::Result<CoverArt> {
let cache_file_path = format!("cover_arts/{}.bin", app_id);
// check if file already exists in cache directory

View File

@ -46,14 +46,7 @@ impl BindingsDropdown for XrInputSubpathKind {
self
.get_str("Translation")
.map(Translation::from_translation_key)
.unwrap_or_else(|| {
let mut chars = self.as_ref().chars();
let capitalized = match chars.next() {
None => String::new(),
Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
};
Translation::from_raw_text(&capitalized)
})
.unwrap_or_else(|| Translation::from_raw_text(&wlx_common::locale::capitalize_string(self.as_ref())))
}
fn action_str(&self, action: &str, side: XrInputSide) -> Rc<str> {
let value = self.as_ref();

View File

@ -62,7 +62,7 @@ const GAME_COVER_SIZE_Y: f32 = 210.0;
impl View {
async fn request_cover_image(manifest: steam_utils::AppManifest, on_loaded: Box<dyn FnOnce(CoverArt)>) {
let cover_art = match cached_fetcher::request_image(manifest.app_id.clone()).await {
let cover_art = match cached_fetcher::request_cover_art(manifest.app_id.clone()).await {
Ok(cover_art) => cover_art,
Err(e) => {
log::error!("request_cover_image failed: {:?}", e);
@ -75,7 +75,7 @@ impl View {
fn mount_image(&self, layout: &mut Layout, glyph: &CustomGlyphData) -> anyhow::Result<()> {
let image = WidgetImage::create(WidgetImageParams {
round: WLength::Units(10.0),
round: WLength::Units(6.0),
glyph_data: Some(glyph.clone()),
..Default::default()
});
@ -173,7 +173,7 @@ impl View {
color: Some(drawing::Color::new(1.0, 1.0, 1.0, 0.0).into()),
border_color: Some(BORDER_COLOR_DEFAULT.into()),
hover_border_color: Some(BORDER_COLOR_HOVERED.into()),
round: WLength::Units(12.0),
round: WLength::Units(8.0),
border: 2.0,
tooltip: Some(TooltipInfo {
side: TooltipSide::Bottom,
@ -209,11 +209,11 @@ impl View {
},
)?;
let rect_gradient = |color: drawing::Color, color2: drawing::Color| {
let rect_gradient = |color: drawing::Color, color2: drawing::Color, round: f32| {
rectangle::WidgetRectangle::create(rectangle::WidgetRectangleParams {
color: color.into(),
color2: color2.into(),
round: WLength::Units(12.0),
round: WLength::Units(round),
gradient: GradientMode::Vertical,
..Default::default()
})
@ -234,7 +234,8 @@ impl View {
widget_button.id,
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.2),
drawing::Color::new(1.0, 1.0, 1.0, 0.02),
drawing::Color::new(1.0, 1.0, 1.0, 0.01),
8.0,
),
rect_gradient_style(taffy::AlignSelf::BASELINE, 0.05),
)?;
@ -248,6 +249,7 @@ impl View {
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.15),
drawing::Color::new(1.0, 1.0, 1.0, 0.0),
8.0,
),
rect_gradient_style(taffy::AlignSelf::BASELINE, 0.5),
)?;
@ -258,6 +260,7 @@ impl View {
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.0),
drawing::Color::new(0.0, 0.0, 0.0, 0.25),
8.0,
),
rect_gradient_style(taffy::AlignSelf::END, 0.5),
)?;
@ -268,6 +271,7 @@ impl View {
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.1),
drawing::Color::new(0.0, 0.0, 0.0, 0.9),
8.0,
),
rect_gradient_style(taffy::AlignSelf::END, 0.05),
)?;

View File

@ -128,7 +128,7 @@ static DEFAULT: &WguiColorPalette = &WguiColorPalette {
hex("#ffebf5"), // OnDanger
hex("#002e43"), // Background
hex("#e4f5f6"), // OnBackground
hex("#0c5170"), // OackgroundVariant
hex("#0c5170"), // OnBackgroundVariant
hex("#b5cacc"), // OnBackgroundVariant
hex("#00131c"), // BackgroundContrast
hex("#e4edf6"), // OnBackgroundContrast

View File

@ -89,3 +89,11 @@ impl WayVRLangProvider {
Self::default()
}
}
pub fn capitalize_string(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
}
}