Supporters list (#622)

* Supporters list

* dash-frontend: Random supporter on main screen, wgui: prevent unnecessary redraws

* more creative sponsor messages

---------

Co-authored-by: galister <22305755+galister@users.noreply.github.com>
This commit is contained in:
oo8dev 2026-08-09 10:48:52 +02:00 committed by GitHub
parent cae2d7c7e2
commit 1a44b54d98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 412 additions and 96 deletions

1
Cargo.lock generated
View File

@ -1406,6 +1406,7 @@ dependencies = [
"keyvalues-parser",
"log",
"lz4_flex",
"rand 0.10.2",
"rust-embed",
"serde",
"serde_json",

View File

@ -27,6 +27,7 @@ xdg.workspace = true
keyvalues-parser = "0.2.4"
lz4_flex = { version = "0.13.1", features = ["frame"] }
ureq = "3.3.0"
rand = "0.10.2"
[features]
default = ["monado"]

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="15" 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

@ -7,12 +7,17 @@
justify_content="center"
align_items="center"
flex_grow="1"
gap="24">
gap="8"
>
<image src_builtin="dashboard/splash.png" width="300" height="89" />
<label id="label_hello" size="32" weight="bold" color="on_background" />
<!-- supporters -->
<Button id="btn_supporter" width="fit_content" height="24" text=" " border="0" color="background_variant(transparent)"/>
<label id="label_hello" margin_top="24" size="32" weight="bold" color="on_background" />
<!-- main button list -->
<div flex_direction="row" gap="8">
<div flex_direction="row" gap="8" margin_top="32">
<MenuButton id="btn_apps" icon="dashboard/apps.svg" translation="APPLICATIONS" />
<MenuButton id="btn_games" icon="dashboard/games.svg" translation="GAMES" />
<MenuButton id="btn_monado" icon="dashboard/monado.svg" text="Monado" />

View File

@ -271,7 +271,10 @@
"DESCRIPTION": "Wir sind ein kleines Team aus 2 Entwicklern, die WayVR in ihrer Freizeit bauen. Wenn dir die Software gefällt, unterstütze uns bitte jede Spende hilft uns dabei, weiterzumachen.",
"THANK_YOU_WAYVR": "Danke vom WayVR-Team!",
"BUTTON_TOOLTIP": "Via Open Collective spenden",
"CURRENT_SUPPORTERS": "Aktuelle Unterstützer"
"CURRENT_SUPPORTERS": "Aktuelle Unterstützer",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Unterstützt von {USER}|Besonderer Dank an {USER}|Gesponsert von {USER}|Ermöglicht durch {USER}|Getragen von Community-Mitgliedern wie {USER}|Mit Hilfe von {USER}|Aufrechterhalten durch {USER}|Ein Beitrag von {USER}|Finanziert durch {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Kaffee spendiert von {USER}|Angetrieben von {USER}|Ein großes Shoutout an {USER}!|{USER} hält uns am Laufen|Mit {USER} wachsen wir weiter|{USER} hält die Server stabil|In voller Dankbarkeit gegenüber {USER}|Power-up durch {USER}|Unterstützt von Freunden wie {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Unterstützt von Legenden wie {USER}|Koffein-Zufuhr gesichert durch {USER}|Eine echte Legende: {USER}|Unser Held des Tages: {USER}|Ein absoluter Superstar: {USER}|Bezahlt gerade die Stromrechnung: {USER}|Der Retter unserer Nerven: {USER}|Der Ruhepol im Chaos: {USER}|Kaffee-zu-Code-Konverter: {USER}|Hält die Production stabil: {USER}|Die eine und einzige(r): {USER}|Bezwinger des dmabuf-Buffers: {USER}"
},
"DOWNLOAD": "Herunterladen",
"DOWNLOADER": "Downloader",
@ -343,4 +346,4 @@
"ADD_TO_FAVOURITES_HINT": "Voreinstellung speichern und an die App-Schublade anheften",
"REMOVE_FROM_FAVOURITES": "Aus angehefteten Anwendungen entfernen",
"EMPTY": "Leer"
}
}

View File

@ -272,6 +272,9 @@ DONATE.DESCRIPTION,"We're a small team of 2 developers building WayVR in our fre
DONATE.THANK_YOU_WAYVR,Thank you from the WayVR Team!,Thank you message displayed on the donate tab.
DONATE.BUTTON_TOOLTIP,Donate via Open Collective,Tooltip on the donate button on the donate tab.
DONATE.CURRENT_SUPPORTERS,Current Supporters,Section header on the donate tab listing supporters.
DONATE.BROUGHT_TO_YOU_VARIANTS_OFTEN,Supported by {USER}|Special thanks to {USER}|Sponsored by {USER}|Backed by {USER}|Driven by community members like {USER}|Made possible by {USER}|Sustained by {USER}|Contributed by {USER}|Funded by {USER},This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it.
DONATE.BROUGHT_TO_YOU_VARIANTS_RARE,Coffee provided by {USER}|Fuelled by {USER}|Shoutout to {USER}!|Keeping us going: {USER}|Helping us grow: {USER}|Keeping the servers running: {USER}|With gratitude to {USER}|Powered by {USER}|Supported by friends like {USER},This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it (rare versions).
DONATE.BROUGHT_TO_YOU_VARIANTS_VERY_RARE,Supported by legends like {USER}|Caffeine supplied by {USER}|A true legend: {USER}|Our hero of the day: {USER}|An absolute superstar: {USER}|Paying for the electricity bill: {USER}|The savior of our sanity: {USER}|Mental stability sponsored by: {USER}|Caffeine-to-code converter provided by {USER}|Keeping the production stable: {USER}|The one and only: {USER}|Passing dmabuf buffers: {USER},This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it (rare versions).
DOWNLOAD,Download,Button in the Whisper model download dialog to confirm downloading a model.
DOWNLOADER,Downloader,Window title for the file download popup.
WELCOME.ALL_SET,You're all set!,Main heading on the final welcome/tutorial page.

1 DEFINITIONS.ANCHORED Anchored A movement option that makes this window move together with the rest of the workspace. Emphasize the "moving together" part as opposed to "not moving at all".
272 FAILED_TO_LAUNCH_APPLICATION WELCOME.WORKING_SET Failed to launch a application: Working Set Error prefix in the toast shown when app launch fails. Page title on the working set tutorial page. "Set" was previously translated as "${DEFINITIONS.SET}". Working as in "Current".
273 GAME_LAUNCHED DOWNLOAD_AGAIN Game launched Download again Toast notification shown after successfully launching a game via Steam. Button in the skymap already downloaded dialog to re-download a skymap.
274 GAME_LIST.NO_GAMES_FOUND DOWNLOADING_FILE No games found Downloading file... Empty state text shown in the game list when no games are installed. Status label in the download file popup showing download progress.
275 FAILED_TO_LAUNCH_APPLICATION Failed to launch a application: Error prefix in the toast shown when app launch fails.
276 GAME_LAUNCHED Game launched Toast notification shown after successfully launching a game via Steam.
277 GAME_LIST.NO_GAMES_FOUND No games found Empty state text shown in the game list when no games are installed.
278 GAME_LIST.RUNNING_GAMES_LIST List of running games Section label in the running games list view.
279 GAMES Games Tab label in the dashboard sidebar for the Games tab.
280 GENERAL_SETTINGS General settings Tab name and group box title in the Monado runtime settings.

View File

@ -271,7 +271,10 @@
"DESCRIPTION": "We're a small team of 2 developers building WayVR in our free time. If you enjoy using this software, please consider supporting us - every donation helps us keep going.",
"THANK_YOU_WAYVR": "Thank you from the WayVR Team!",
"BUTTON_TOOLTIP": "Donate via Open Collective",
"CURRENT_SUPPORTERS": "Current Supporters"
"CURRENT_SUPPORTERS": "Current Supporters",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Supported by {USER}|Special thanks to {USER}|Sponsored by {USER}|Backed by {USER}|Driven by community members like {USER}|Made possible by {USER}|Sustained by {USER}|Contributed by {USER}|Funded by {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Coffee provided by {USER}|Fuelled by {USER}|Shoutout to {USER}!|Keeping us going: {USER}|Helping us grow: {USER}|Keeping the servers running: {USER}|With gratitude to {USER}|Powered by {USER}|Supported by friends like {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Supported by legends like {USER}|Caffeine supplied by {USER}|A true legend: {USER}|Our hero of the day: {USER}|An absolute superstar: {USER}|Paying for the electricity bill: {USER}|The savior of our sanity: {USER}|Leading the way: {USER}|Caffeine-to-code converter provided by {USER}|Keeping the production stable: {USER}|The one and only: {USER}|Passing dmabuf buffers: {USER}"
},
"DOWNLOAD": "Download",
"DOWNLOADER": "Downloader",
@ -343,4 +346,4 @@
"ADD_TO_FAVOURITES_HINT": "Save preset and pin it to the app drawer",
"REMOVE_FROM_FAVOURITES": "Remove from pinned apps",
"EMPTY": "Empty"
}
}

View File

@ -271,7 +271,10 @@
"DESCRIPTION": "Somos un pequeño equipo de 2 desarrolladores creando WayVR en nuestro tiempo libre. Si disfrutas usando este software, considera apoyarnos; cada donación nos ayuda a seguir adelante.",
"THANK_YOU_WAYVR": "¡Gracias del equipo de WayVR!",
"BUTTON_TOOLTIP": "Dona a través de Open Collective",
"CURRENT_SUPPORTERS": "Colaboradores actuales"
"CURRENT_SUPPORTERS": "Colaboradores actuales",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Apoyado por {USER}|Gracias a {USER}|Con el apoyo de {USER}|Patrocinado por {USER}|Un aporte especial de {USER}|Impulsado por {USER}|Haciendo esto posible: {USER}|Sostenible gracias a {USER}|Un agradecimiento a {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Un gran impulso de {USER}|Todo es gracias a {USER}|Con todo el cariño de {USER}|El motor de hoy: {USER}|Sumando valor con {USER}|Haciendo crecer el proyecto: {USER}|De corazón, gracias a {USER}|Con el respaldo de {USER}|La chispa del día: {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "¡Un nivel leyenda: {USER}!|Energía pura gracias a {USER}|Nuestra estrella del día: {USER}|El MVP de hoy: {USER}|Rescatando el día: {USER}|Sosteniendo la aplicación: {USER}|El combustible secreto: {USER}|Manteniendo la cordura: {USER}|Conversor de café en código: {USER}|Salvando la producción: {USER}|¡Simplemente increíble: {USER}!|El corazón de todo: {USER}"
},
"DOWNLOAD": "Descargar",
"DOWNLOADER": "Descargador",
@ -343,4 +346,4 @@
"ADD_TO_FAVOURITES_HINT": "Guardar ajuste y fijarlo al cajón de aplicaciones",
"REMOVE_FROM_FAVOURITES": "Quitar de aplicaciones fijadas",
"EMPTY": "Vacío"
}
}

View File

@ -271,7 +271,10 @@
"DESCRIPTION": "Siamo un piccolo team di 2 sviluppatori che realizza WayVR nel tempo libero. Se ti piace usare questo software, considera di supportarci: ogni donazione ci aiuta a continuare.",
"THANK_YOU_WAYVR": "Grazie dal team di WayVR!",
"BUTTON_TOOLTIP": "Donazione tramite Open Collective",
"CURRENT_SUPPORTERS": "Supporter attuali"
"CURRENT_SUPPORTERS": "Supporter attuali",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Supportato da {USER}|Grazie speciali a {USER}|Sponsorizzato da {USER}|Contributo di {USER}|Sostenuto da membri come {USER}|Reso possibile da {USER}|Sostenuto da {USER}|Grazie a {USER}|Alimentato da {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Caffè offerto da {USER}|Energia pura grazie a {USER}|Un grande applauso a {USER}!|Ci tiene in piedi: {USER}|Ci aiuta a crescere: {USER}|Mantiene i server accesi: {USER}|Con immensa gratitudine a {USER}|Spinto dalla forza di {USER}|Supportato da amici come {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Supportato da leggende come {USER}|Caffeina fornita da {USER}|Una vera leggenda: {USER}|Il nostro eroe del giorno: {USER}|Una superstar assoluta: {USER}|Paga la bolletta della luce: {USER}|Il motore della nostra creatività: {USER}|Il navigatore del nostro percorso: {USER}|Convertitore caffeina-codice fornito da {USER}|Mantiene la produzione stabile: {USER}|L'unico e inimitabile: {USER}|Maestro dei buffer dmabuf: {USER}"
},
"DOWNLOAD": "Scarica",
"DOWNLOADER": "Download",
@ -343,4 +346,4 @@
"ADD_TO_FAVOURITES_HINT": "Salva il preset e fissalo al cassetto delle app",
"REMOVE_FROM_FAVOURITES": "Rimuovi dalle app fissate",
"EMPTY": "Vuoto"
}
}

View File

@ -271,7 +271,10 @@
"DESCRIPTION": "WayVRは、2人の開発者が空き時間を利用して作っている小規模なプロジェクトです。もしこのソフトを気に入っていただけたら、ぜひサポートをお願いします。皆様の寄付が開発の大きな励みになります。",
"THANK_YOU_WAYVR": "WayVRチームより、感謝を込めて",
"BUTTON_TOOLTIP": "Open Collectiveで寄付する",
"CURRENT_SUPPORTERS": "現在のサポーター"
"CURRENT_SUPPORTERS": "現在のサポーター",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "{USER}さんにサポートされています|{USER}さんに感謝!|{USER}さんのご支援で成り立っています|{USER}さんが支えてくれています|{USER}さんの後押しのおかげです|{USER}さんが後援しています|{USER}さんのご協力に感謝|{USER}さんが応援してくれています|本日のサポーターは{USER}さんです",
"BROUGHT_TO_YOU_VARIANTS_RARE": "{USER}さんの優しさに感謝|{USER}さんが背中を押してくれました|{USER}さんの心強いサポート|{USER}さんが彩りを添えてくれました|{USER}さんのおかげで前進しています|{USER}さんの温かいご支援|{USER}さんが笑顔を届けてくれました|{USER}さんが道を照らしてくれました|{USER}さんに心から感謝して",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "今日のMVPは{USER}さん!|{USER}さんが神降臨!|{USER}さんが限界突破させてくれた!|{USER}さんのパワーで爆走中!|救世主、現る:{USER}さん!|{USER}さんが文明を支えています|{USER}さんの情熱が燃料です!|本日のスーパーヒーロー:{USER}さん!|{USER}さんがバグをなぎ倒してくれた!|{USER}さんの応援でテンションMAX|全宇宙から{USER}さんに拍手を!"
},
"DOWNLOAD": "ダウンロード",
"DOWNLOADER": "ダウンローダー",
@ -343,4 +346,4 @@
"ADD_TO_FAVOURITES_HINT": "プリセットを保存し、アプリドロワーに固定する",
"REMOVE_FROM_FAVOURITES": "固定済みアプリから外す",
"EMPTY": "なし"
}
}

View File

@ -271,7 +271,10 @@
"DESCRIPTION": "Jesteśmy małym zespołem dwóch programistów, którzy tworzą WayVR w wolnym czasie. Jeśli lubisz korzystać z tego programu, rozważ wsparcie nas finansowo każda darowizna pomaga nam działać dalej.",
"THANK_YOU_WAYVR": "Dzięki od zespołu WayVR!",
"BUTTON_TOOLTIP": "Wesprzyj projekt przez Open Collective",
"CURRENT_SUPPORTERS": "Obecni darczyńcy"
"CURRENT_SUPPORTERS": "Obecni darczyńcy",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Wspierane przez {USER}|Dzięki dla {USER}|Sponsor: {USER}|Z pomocą {USER}|Wkład od {USER}|Umożliwione przez {USER}|Dzięki wsparciu {USER}|Patron: {USER}|Działamy dzięki {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Kawa od {USER}|Energia dostarczona przez {USER}|Wielkie brawa dla {USER}!|Dzięki {USER} idziemy do przodu|Razem z {USER} rośniemy w siłę|Serwery napędzane przez {USER}|Z ogromną wdzięcznością dla {USER}|Napędzane przez {USER}|Wsparcie od przyjaciół takich jak {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Wspierane przez legendę: {USER}|Dostawa kofeiny od {USER}|Prawdziwa legenda: {USER}|Gwiazda dnia: {USER}|Absolutna supergwiazda: {USER}|Opłaca rachunki za prąd: {USER}|Ludzie, którzy czynią to możliwe: {USER}|Sponsor spokoju ducha: {USER}|Konwerter kawy na kod: {USER}|Strażnik stabilności produkcji: {USER}|Jedyny w swoim rodzaju: {USER}|Przesyła bufory dmabuf: {USER}"
},
"DOWNLOAD": "Pobierz",
"DOWNLOADER": "Menedżer pobierania",

View File

@ -271,7 +271,10 @@
"DESCRIPTION": "我们是由两名开发者组成的小团队,在业余时间构建 WayVR。如果你喜欢这款软件请考虑支持我们 —— 每一份捐赠都能帮助我们坚持下去。",
"THANK_YOU_WAYVR": "WayVR 团队向您致谢!",
"BUTTON_TOOLTIP": "通过 Open Collective 捐赠",
"CURRENT_SUPPORTERS": "当前支持者"
"CURRENT_SUPPORTERS": "当前支持者",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "由 {USER} 赞助|感谢 {USER} 的支持|由 {USER} 助力|来自 {USER} 的贡献|{USER} 让这一切成为可能|由 {USER} 维持运行|特别感谢 {USER}|{USER} 给予了支持|由 {USER} 驱动",
"BROUGHT_TO_YOU_VARIANTS_RARE": "感谢 {USER} 的温暖支持|{USER} 为我们注入了能量|由 {USER} 助力成长|感谢 {USER} 的无私奉献|{USER} 让今天变得更美好|今日的动力源泉:{USER}|由 {USER} 悉心支持|感恩 {USER} 的陪伴|{USER} 是今天的闪光点",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "今日之神:{USER}|全场 MVP{USER}|{USER} 拯救了世界|给 {USER} 递上一朵小红花|由 {USER} 承包了今天的快乐|{USER} 承包了今日的电费|代码之光:{USER}|{USER} 拯救了我们的发际线|简直是超级英雄:{USER}|{USER} 让生产环境稳如老狗|唯一且无可替代的:{USER}|{USER} 是今天的顶梁柱"
},
"DOWNLOAD": "下载",
"DOWNLOADER": "下载管理器",
@ -343,4 +346,4 @@
"ADD_TO_FAVOURITES_HINT": "保存预设并将其固定到应用抽屉",
"REMOVE_FROM_FAVOURITES": "从固定应用中移除",
"EMPTY": "暂无"
}
}

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

@ -1,11 +1,13 @@
use std::marker::PhantomData;
use std::{marker::PhantomData, rc::Rc};
use wgui::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
components::button::ComponentButton,
color::{WguiColor, WguiColorName, WguiNamedColor},
components::{ComponentTrait, button::ComponentButton},
event::CallbackDataCommon,
i18n::Translation,
layout::{Widget, WidgetID},
layout::{LayoutTask, LayoutTasks, Widget, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
widget::label::WidgetLabel,
};
@ -14,6 +16,7 @@ use wlx_common::config::GeneralConfig;
use crate::{
frontend::{Frontend, FrontendTask},
tab::{Tab, TabType},
util::cached_fetcher,
various,
};
@ -29,14 +32,91 @@ 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);
fn get_supporter_anim(
btn: Rc<ComponentButton>,
tasks: LayoutTasks,
supporters: Rc<cached_fetcher::Supporters>,
prev_supporter_name: String,
) -> Animation {
let username = loop {
let random_supporter = &supporters.supporters[rand::random_range(0..supporters.supporters.len() - 1)];
let username = random_supporter.username.clone();
if username != prev_supporter_name {
break username;
}
};
const FADE_SPEED: f32 = 10.0;
Animation::new_ex(
btn.base().get_id(),
1234,
480,
AnimationEasing::Linear,
Box::new(move |common, data| {
let opacity_in = f32::clamp(data.pos * FADE_SPEED, 0.0, 1.0);
let opacity_out = f32::clamp((1.0 - data.pos) * FADE_SPEED, 0.0, 1.0);
let opacity = f32::min(opacity_in, opacity_out);
btn.set_text_color(
common,
WguiColor::Named(WguiNamedColor::with_alpha(WguiColorName::OnPrimary, opacity)),
);
// first iter
if data.pos == 0.0 {
let translation_key = match rand::random::<u8>() {
0..=15 => "DONATE.BROUGHT_TO_YOU_VARIANTS_VERY_RARE",
16..=70 => "DONATE.BROUGHT_TO_YOU_VARIANTS_RARE",
71..=u8::MAX => "DONATE.BROUGHT_TO_YOU_VARIANTS_OFTEN",
};
let translated = common.globals().i18n_builtin.translate(translation_key);
let variants = translated.split('|').collect::<Vec<&str>>();
let variant = variants[rand::random_range(0..variants.len() - 1)];
btn.set_text(
common,
Translation::from_raw_text_string(variant.replace("{USER}", &username)),
);
}
// last iter
if data.pos == 1.0 {
// infinitely looped animation, re-trigger it.
tasks.push(LayoutTask::PlayAnimation(get_supporter_anim(
btn.clone(),
tasks.clone(),
supporters.clone(),
username.clone(), /* prev supporter name */
)))
}
}),
)
}
async fn config_supporters(btn: Rc<ComponentButton>, tasks: LayoutTasks) {
let Some(supporters) = cached_fetcher::request_supporters().await else {
return;
};
if supporters.supporters.len() <= 2 {
return; // Welp.
}
let supporters = Rc::new(supporters);
tasks.push(LayoutTask::PlayAnimation(get_supporter_anim(
btn,
tasks.clone(),
supporters,
String::new(),
)));
}
fn configure_label_hello(common: &mut CallbackDataCommon, label_hello: Widget, config: &GeneralConfig) {
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))
} else {
@ -66,6 +146,8 @@ impl<T> TabHome<T> {
frontend.interface.general_config(data),
);
let btn_supporter = state.fetch_component_as::<ComponentButton>("btn_supporter")?;
let btn_apps = state.fetch_component_as::<ComponentButton>("btn_apps")?;
let btn_games = state.fetch_component_as::<ComponentButton>("btn_games")?;
let btn_monado = state.fetch_component_as::<ComponentButton>("btn_monado")?;
@ -74,14 +156,20 @@ impl<T> TabHome<T> {
let btn_donate = state.fetch_component_as::<ComponentButton>("btn_donate")?;
let btn_website = state.fetch_component_as::<ComponentButton>("btn_website")?;
let tasks = &mut frontend.tasks;
tasks.handle_button(&btn_apps, FrontendTask::SetTab(TabType::Apps));
tasks.handle_button(&btn_games, FrontendTask::SetTab(TabType::Games));
tasks.handle_button(&btn_monado, FrontendTask::SetTab(TabType::Monado));
tasks.handle_button(&btn_settings, FrontendTask::SetTab(TabType::Settings));
tasks.handle_button(&btn_welcome_screen, FrontendTask::SetTab(TabType::Welcome));
tasks.handle_button(&btn_donate, FrontendTask::SetTab(TabType::Donate));
tasks.handle_button(&btn_website, FrontendTask::OpenURL("https://wayvr.org".into()));
let f_tasks = &mut frontend.tasks;
f_tasks.handle_button(&btn_apps, FrontendTask::SetTab(TabType::Apps));
f_tasks.handle_button(&btn_games, FrontendTask::SetTab(TabType::Games));
f_tasks.handle_button(&btn_monado, FrontendTask::SetTab(TabType::Monado));
f_tasks.handle_button(&btn_settings, FrontendTask::SetTab(TabType::Settings));
f_tasks.handle_button(&btn_welcome_screen, FrontendTask::SetTab(TabType::Welcome));
f_tasks.handle_button(&btn_donate, FrontendTask::SetTab(TabType::Donate));
f_tasks.handle_button(&btn_supporter, FrontendTask::SetTab(TabType::Donate));
f_tasks.handle_button(&btn_website, FrontendTask::OpenURL("https://wayvr.org".into()));
frontend
.executor
.spawn(config_supporters(btn_supporter, frontend.layout.tasks.clone()))
.detach();
Ok(Self {
state,

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

@ -3,7 +3,7 @@ use strum::{EnumCount, EnumString};
// Primary: button color
// OnPrimary: text color placed on the Primary-colored button
#[derive(Debug, Copy, Clone, EnumCount, EnumString)]
#[derive(Debug, Copy, Clone, PartialEq, EnumCount, EnumString)]
#[repr(usize)]
#[strum(serialize_all = "snake_case")]
pub enum WguiColorName {
@ -26,7 +26,7 @@ pub enum WguiColorName {
Highlight,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct WguiNamedColor {
name: WguiColorName,
rgb_multiplier: f32,
@ -43,7 +43,7 @@ pub enum ParentColor {
Ignore,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WguiColor {
Raw(drawing::Color),
Named(WguiNamedColor),
@ -203,6 +203,15 @@ impl Default for WguiColor {
}
impl WguiNamedColor {
pub const fn with_alpha(name: WguiColorName, alpha: f32) -> WguiNamedColor {
WguiNamedColor {
name,
rgb_multiplier: 1.0,
rgb_addition: 0.0,
alpha,
}
}
pub fn resolve(&self, palette: &WguiColorPalette) -> drawing::Color {
let mut color = palette[self.name];
if self.alpha != 1.0 {

View File

@ -211,6 +211,14 @@ impl ComponentButton {
label.set_text(common, text);
}
pub fn set_text_color(&self, common: &mut CallbackDataCommon, color: WguiColor) {
let state = self.state.borrow();
let Some(mut label) = common.state.widgets.get_as::<WidgetLabel>(state.id_label) else {
return;
};
label.set_color(common, color, true);
}
pub fn set_color(&self, common: &mut CallbackDataCommon, color: WguiColor) {
let gradient_intensity = common.state.theme.gradient_intensity;

View File

@ -6,7 +6,7 @@ use std::{
};
use crate::{
animation::Animations,
animation::{Animation, Animations},
components::{self, Component, ComponentWeak, DestroyData, FocusChangeData, RefreshData},
drawing::{
self, ANSI_BOLD_CODE, ANSI_RESET_CODE, Boundary, PushScissorStackResult, push_scissor_stack, push_transform_stack,
@ -153,6 +153,7 @@ pub enum LayoutTask {
SetWidgetVisible(WidgetID, bool), // if true, sets Display to Flex; None, otherwise
ModifyLayoutState(LayoutModifyStateFunc),
PlaySound(WguiSoundType),
PlayAnimation(Animation),
Dispatch(LayoutDispatchFunc),
SetFocus(ComponentWeak),
RefreshPalette,
@ -653,7 +654,10 @@ impl Layout {
}
}
fn try_recompute_layout(&mut self, size: Vec2) -> anyhow::Result<()> {
fn try_recompute_layout(&mut self, mut size: Vec2) -> anyhow::Result<()> {
size.x = size.x.round();
size.y = size.y.round();
if !self.state.tree.dirty(self.tree_root_node)? && self.prev_size == size {
// Nothing to do
return Ok(());
@ -749,6 +753,9 @@ impl Layout {
let mut tasks = self.tasks.drain();
while let Some(task) = tasks.pop_front() {
match task {
LayoutTask::PlayAnimation(animation) => {
self.animations.add(animation);
}
LayoutTask::RefreshPalette => {
let root = self.tree_root_widget;
LayoutState::refresh_palette_recur(&mut self.common(), root);

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

@ -122,6 +122,12 @@ impl WidgetLabel {
}
pub fn set_color(&mut self, common: &mut CallbackDataCommon, color: WguiColor, apply_to_existing_text: bool) {
if let Some(current_color) = &self.params.style.color
&& *current_color == color
{
// not changed
return;
}
self.params.style.color = Some(color);
if apply_to_existing_text {
self.update_attrs(&common.globals().palette);

View File

@ -440,12 +440,15 @@ impl WidgetState {
&& (scrolling_cur.y - scrolling_target.y).abs() < epsilon
{
*scrolling_cur = *scrolling_target;
perform_next_tick = false;
}
}
if perform_next_tick {
alterables.mark_tick(this_widget_id);
alterables.mark_redraw();
} else {
self.data.scrolling_cur_prev = self.data.scrolling_cur;
}
}

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(),
}
}