Live location tracking and playback for the game "manhunt"

Loading screen for pfp processing

bwc9876.dev cba2b381 76afdd5a

verified
+83 -17
-6
TODO.md
··· 1 1 # TODO 2 2 3 - ## 02-19-26 4 - 5 - - [ ] Get pictures working 6 - - [ ] Set up oxlint 7 - - [ ] treefmt 8 - 9 3 ## Ben 10 4 11 5 - [x] Transport : Packet splitting
+15
frontend/src/components/LoadingCover.tsx
··· 1 + import React from "react"; 2 + import LoadingSpinner from "./LoadingSpinner"; 3 + 4 + export type LoadingCoverProps = { 5 + text?: string; 6 + show: boolean; 7 + }; 8 + 9 + export default function LoadingCover({show, text}: LoadingCoverProps) { 10 + return <div className="screen-cover" style={{display: show ? undefined : "none"}}> 11 + <LoadingSpinner /> 12 + {text && <strong>{text}</strong>} 13 + </div> 14 + } 15 +
+10
frontend/src/components/LoadingSpinner.tsx
··· 1 + import React from "react"; 2 + 3 + export type LoadingSpinnerProps = { 4 + className?: string; 5 + }; 6 + 7 + export default function LoadingSpinner({className}: LoadingSpinnerProps) { 8 + return <span className={`spinner ${className ?? ""}`}></span>; 9 + } 10 +
+9 -7
frontend/src/components/MenuScreen.tsx
··· 7 7 } from "@tabler/icons-react"; 8 8 import { commands, GameSettings, PlayerProfile } from "@/bindings"; 9 9 import ProfilePicture from "./ProfilePicture"; 10 + import LoadingCover from "./LoadingCover"; 10 11 11 12 // Temp settings for now. 12 13 export const tempSettings: GameSettings = { ··· 33 34 34 35 export default function MenuScreen() { 35 36 const [profile, setProfile] = useState<PlayerProfile>(defaultProfile); 37 + const [loadingCover, setLoadingCover] = useState<boolean>(false); 36 38 37 39 useEffect(() => { 38 40 let cancel = false; ··· 77 79 }; 78 80 79 81 const onEditPicture = () => { 82 + setLoadingCover(true); 80 83 commands.createProfilePicture().then((newPic) => { 81 - if (!newPic) { 82 - return; 84 + if (newPic) { 85 + const newProfile = { ...profile, pfp_base64: newPic }; 86 + commands.updateProfile(newProfile); 87 + setProfile(newProfile); 83 88 } 84 - 85 - const newProfile = { ...profile, pfp_base64: newPic }; 86 - commands.updateProfile(newProfile); 87 - setProfile(newProfile); 88 - }); 89 + }).finally(() => { setLoadingCover(false); }); 89 90 }; 90 91 91 92 return ( ··· 114 115 Past Games 115 116 </button> 116 117 </main> 118 + <LoadingCover text="Processing Picture..." show={loadingCover} /> 117 119 </> 118 120 ); 119 121 }
+2 -2
frontend/src/components/ProfilePicture.tsx
··· 1 1 import React from "react"; 2 - import fallbackPicture from "@/default-pfp.png"; 2 + import fallbackPicture from "@/default-pfp.webp"; 3 3 import { IconBinocularsFilled, IconGhostFilled } from "@tabler/icons-react"; 4 4 5 5 export type ProfileDecor = "hider" | "seeker"; ··· 62 62 data-initial={fallbackInitial} 63 63 className={className} 64 64 > 65 - <img width={256} height={256} style={style} alt="Profile Picture" src={fallback} /> 65 + <img width={64} height={64} style={style} alt="Profile Picture" src={fallback} /> 66 66 {Icon && <Icon size="1em" />} 67 67 </span> 68 68 );
frontend/src/default-pfp.png

This is a binary file and will not be displayed.

frontend/src/default-pfp.webp

This is a binary file and will not be displayed.

frontend/src/evil-cow.png

This is a binary file and will not be displayed.

+46 -1
frontend/src/style.css
··· 101 101 } 102 102 } 103 103 104 - &.lobby > div.frame { 104 + &.lobby>div.frame { 105 105 flex-grow: 1; 106 106 background-color: #aaa; 107 107 position: relative; ··· 172 172 transform: rotateZ(1deg) translateY(5px); 173 173 } 174 174 } 175 + } 176 + 177 + div.screen-cover { 178 + font-size: 24pt; 179 + position: fixed; 180 + inset: 0; 181 + background-color: #0009; 182 + display: flex; 183 + align-items: center; 184 + justify-content: center; 185 + z-index: 500; 186 + flex-direction: column; 187 + gap: var(--2); 188 + 189 + strong { 190 + color: white; 191 + filter: drop-shadow(0 0 10px #000a); 192 + } 193 + 194 + .spinner { 195 + font-size: 48pt; 196 + } 197 + } 198 + 199 + @keyframes rotation { 200 + 0% { 201 + transform: rotate(0deg); 202 + } 203 + 204 + 100% { 205 + transform: rotate(360deg); 206 + } 207 + } 208 + 209 + 210 + span.spinner { 211 + width: 1em; 212 + height: 1em; 213 + filter: drop-shadow(0 0 10px #000a); 214 + border: solid .1em gray; 215 + border-bottom-color: white; 216 + border-radius: 50%; 217 + display: inline-block; 218 + box-sizing: border-box; 219 + animation: rotation 1s linear infinite; 175 220 } 176 221 177 222 span.pfp {
+1 -1
manhunt-app/src/profiles.rs
··· 25 25 .decode() 26 26 .context("Failed to read image file")?; 27 27 28 - let img = img.resize_exact(IMAGE_SIZE, IMAGE_SIZE, FilterType::Lanczos3); 28 + let img = img.resize_to_fill(IMAGE_SIZE, IMAGE_SIZE, FilterType::Lanczos3); 29 29 30 30 let mut buf = Vec::<u8>::new(); 31 31 let encoder = WebPEncoder::new_lossless(&mut buf);