Live location tracking and playback for the game "manhunt"

Use treefmt

bwc9876.dev 6b5163b2 c840c203

verified
+417 -298
+44
flake.lock
··· 18 18 "type": "github" 19 19 } 20 20 }, 21 + "flakelight-treefmt": { 22 + "inputs": { 23 + "flakelight": [ 24 + "flakelight" 25 + ], 26 + "treefmt-nix": "treefmt-nix" 27 + }, 28 + "locked": { 29 + "lastModified": 1771333462, 30 + "narHash": "sha256-nkE5hR6+JCu6mhxDv+GxJmLmCf0vl/HvRapzF9LNUcI=", 31 + "owner": "m15a", 32 + "repo": "flakelight-treefmt", 33 + "rev": "3eb38fcc804c4434e8fe96aad659f8aed2b1e01a", 34 + "type": "github" 35 + }, 36 + "original": { 37 + "owner": "m15a", 38 + "repo": "flakelight-treefmt", 39 + "type": "github" 40 + } 41 + }, 21 42 "nixpkgs": { 22 43 "locked": { 23 44 "lastModified": 1771008912, ··· 53 74 "root": { 54 75 "inputs": { 55 76 "flakelight": "flakelight", 77 + "flakelight-treefmt": "flakelight-treefmt", 56 78 "nixpkgs": "nixpkgs_2", 57 79 "rust-overlay": "rust-overlay" 58 80 } ··· 74 96 "original": { 75 97 "owner": "oxalica", 76 98 "repo": "rust-overlay", 99 + "type": "github" 100 + } 101 + }, 102 + "treefmt-nix": { 103 + "inputs": { 104 + "nixpkgs": [ 105 + "flakelight-treefmt", 106 + "flakelight", 107 + "nixpkgs" 108 + ] 109 + }, 110 + "locked": { 111 + "lastModified": 1770228511, 112 + "narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=", 113 + "owner": "numtide", 114 + "repo": "treefmt-nix", 115 + "rev": "337a4fe074be1042a35086f15481d763b8ddc0e7", 116 + "type": "github" 117 + }, 118 + "original": { 119 + "owner": "numtide", 120 + "repo": "treefmt-nix", 77 121 "type": "github" 78 122 } 79 123 }
+18 -17
flake.nix
··· 4 4 flakelight.url = "github:nix-community/flakelight"; 5 5 rust-overlay.url = "github:oxalica/rust-overlay"; 6 6 rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; 7 + flakelight-treefmt.url = "github:m15a/flakelight-treefmt"; 8 + flakelight-treefmt.inputs.flakelight.follows = "flakelight"; 7 9 }; 8 10 outputs = {flakelight, ...} @ inputs: 9 11 flakelight ./. { 10 12 inherit inputs; 13 + imports = [inputs.flakelight-treefmt.flakelightModules.default]; 11 14 withOverlays = [inputs.rust-overlay.overlays.default]; 12 15 nixpkgs.config = { 13 16 allowUnfree = true; 14 17 android_sdk.accept_license = true; 15 18 }; 16 19 17 - flakelight.builtinFormatters = false; 18 - formatters = pkgs: let 19 - prettier = "${pkgs.prettier}/bin/prettier --write ."; 20 - alejandra = "${pkgs.alejandra}/bin/alejandra ."; 21 - rustfmt = "${pkgs.rustfmt}/bin/rustfmt fmt"; 22 - just = "${pkgs.just}/bin/just --fmt --unstable"; 23 - in { 24 - "justfile" = just; 25 - "*.nix" = alejandra; 26 - "*.js" = prettier; 27 - "*.ts" = prettier; 28 - "*.jsx" = prettier; 29 - "*.tsx" = prettier; 30 - "*.md" = prettier; 31 - "*.json" = prettier; 32 - "*.rs" = rustfmt; 20 + treefmtConfig = {pkgs, ...}: { 21 + programs = { 22 + alejandra.enable = true; 23 + just.enable = true; 24 + prettier.enable = true; 25 + rustfmt.enable = true; 26 + }; 33 27 }; 34 28 35 29 devShell = pkgs: let ··· 82 76 pkg-config 83 77 gobject-introspection 84 78 nodePackages.prettier 85 - (rust-bin.stable.latest.default.override {targets = ["aarch64-linux-android" "armv7-linux-androideabi" "i686-linux-android" "x86_64-linux-android"];}) 79 + (rust-bin.stable.latest.default.override { 80 + targets = [ 81 + "aarch64-linux-android" 82 + "armv7-linux-androideabi" 83 + "i686-linux-android" 84 + "x86_64-linux-android" 85 + ]; 86 + }) 86 87 cargo-tauri 87 88 nodejs 88 89 (android-studio.withSdk androidComposition.androidsdk)
+1 -1
frontend/.oxlintrc.json
··· 1 1 { 2 - "ignorePatterns": ["src/bindings.ts"] 2 + "ignorePatterns": ["src/bindings.ts"] 3 3 }
+1 -2
frontend/src/components/App.tsx
··· 7 7 import GameScreen from "./GameScreen"; 8 8 9 9 function ScreenRouter({ screen }: { screen: AppScreen }) { 10 - 11 10 console.debug(`Render screen ${screen}`); 12 11 13 12 switch (screen) { ··· 24 23 } 25 24 } 26 25 27 - export default function App({initialScreen}: {initialScreen: AppScreen}) { 26 + export default function App({ initialScreen }: { initialScreen: AppScreen }) { 28 27 const [currentScreen, setScreen] = useState(initialScreen); 29 28 30 29 useTauriEvent("changeScreen", (newScreen) => {
+62 -17
frontend/src/components/LobbyScreen.tsx
··· 3 3 import { useTauriEvent } from "@/lib/hooks"; 4 4 import ProfilePicture, { iconForDecor, ProfileDecor } from "./ProfilePicture"; 5 5 import { tempSettings } from "./MenuScreen"; 6 - import { IconArrowBigLeftLinesFilled, IconCircleCheckFilled, IconCircleDashedPlus } from "@tabler/icons-react"; 6 + import { 7 + IconArrowBigLeftLinesFilled, 8 + IconCircleCheckFilled, 9 + IconCircleDashedPlus 10 + } from "@tabler/icons-react"; 7 11 8 - function ProfileList({ profiles, decoration }: { profiles: [string, PlayerProfile][], decoration: ProfileDecor }) { 9 - return <div className="lobby-pfps"> 10 - {profiles.map(([k, p]) => <ProfilePicture key={k} decoration={decoration} src={p.pfp_base64} fallbackName={p.display_name} />)} 11 - </div>; 12 + function ProfileList({ 13 + profiles, 14 + decoration 15 + }: { 16 + profiles: [string, PlayerProfile][]; 17 + decoration: ProfileDecor; 18 + }) { 19 + return ( 20 + <div className="lobby-pfps"> 21 + {profiles.map(([k, p]) => ( 22 + <ProfilePicture 23 + key={k} 24 + decoration={decoration} 25 + src={p.pfp_base64} 26 + fallbackName={p.display_name} 27 + /> 28 + ))} 29 + </div> 30 + ); 12 31 } 13 32 14 - function TeamButton({ onClick, active, deco, text }: { onClick: () => void, active: boolean, deco: ProfileDecor, text: string }) { 15 - 33 + function TeamButton({ 34 + onClick, 35 + active, 36 + deco, 37 + text 38 + }: { 39 + onClick: () => void; 40 + active: boolean; 41 + deco: ProfileDecor; 42 + text: string; 43 + }) { 16 44 const Icon = iconForDecor(deco); 17 45 18 - return <button onClick={onClick} className={`team-button ${deco}`}> 19 - <Icon /> 20 - {active ? "You're On" : "Join"} {text} 21 - {active ? <IconCircleCheckFilled /> : <IconCircleDashedPlus />} 22 - </button>; 46 + return ( 47 + <button onClick={onClick} className={`team-button ${deco}`}> 48 + <Icon /> 49 + {active ? "You're On" : "Join"} {text} 50 + {active ? <IconCircleCheckFilled /> : <IconCircleDashedPlus />} 51 + </button> 52 + ); 23 53 } 24 54 25 55 const initLobbyState: LobbyState = { ··· 28 58 teams: {}, 29 59 self_id: "", 30 60 is_host: false, 31 - settings: tempSettings, 61 + settings: tempSettings 32 62 }; 33 63 34 64 export default function LobbyScreen() { ··· 52 82 }); 53 83 }); 54 84 55 - const profiles = Object.entries(lobbyState.profiles).filter(([_, p]) => p !== undefined) as [string, PlayerProfile][]; 85 + const profiles = Object.entries(lobbyState.profiles).filter(([_, p]) => p !== undefined) as [ 86 + string, 87 + PlayerProfile 88 + ][]; 56 89 57 90 const seekers = profiles.filter(([id, _]) => lobbyState.teams[id] ?? false); 58 91 const hiders = profiles.filter(([id, _]) => !(lobbyState.teams[id] ?? false)); ··· 76 109 </header> 77 110 <main className="lobby"> 78 111 <ProfileList profiles={seekers} decoration="seeker" /> 79 - <TeamButton onClick={() => commands.switchTeams(true)} active={isSeeker} text="Seekers" deco="seeker" /> 112 + <TeamButton 113 + onClick={() => commands.switchTeams(true)} 114 + active={isSeeker} 115 + text="Seekers" 116 + deco="seeker" 117 + /> 80 118 <div className="frame"> 81 - <button onClick={onLeaveLobby} aria-label="Leave Lobby" className="fab left"><IconArrowBigLeftLinesFilled size="2em"/></button> 119 + <button onClick={onLeaveLobby} aria-label="Leave Lobby" className="fab left"> 120 + <IconArrowBigLeftLinesFilled size="2em" /> 121 + </button> 82 122 </div> 83 - <TeamButton onClick={() => commands.switchTeams(false)} active={!isSeeker} text="Hiders" deco="hider" /> 123 + <TeamButton 124 + onClick={() => commands.switchTeams(false)} 125 + active={!isSeeker} 126 + text="Hiders" 127 + deco="hider" 128 + /> 84 129 <ProfileList profiles={hiders} decoration="hider" /> 85 130 </main> 86 131 </>
+35 -23
frontend/src/components/MenuScreen.tsx
··· 1 1 import React, { useCallback, useEffect, useState } from "react"; 2 2 import "@fontsource/bungee"; 3 - import { IconBuildingBroadcastTowerFilled, IconHexagonPlusFilled, IconClockFilled } from "@tabler/icons-react"; 3 + import { 4 + IconBuildingBroadcastTowerFilled, 5 + IconHexagonPlusFilled, 6 + IconClockFilled 7 + } from "@tabler/icons-react"; 4 8 import { commands, GameSettings, PlayerProfile } from "@/bindings"; 5 9 import ProfilePicture from "./ProfilePicture"; 6 10 ··· 24 28 25 29 const defaultProfile: PlayerProfile = { 26 30 display_name: "", 27 - pfp_base64: null, 31 + pfp_base64: null 28 32 }; 29 33 30 34 export default function MenuScreen() { ··· 73 77 }; 74 78 75 79 const onEditPicture = () => { 76 - commands.createProfilePicture().then(newPic => { 80 + commands.createProfilePicture().then((newPic) => { 77 81 if (!newPic) { 78 82 return; 79 83 } ··· 84 88 }); 85 89 }; 86 90 87 - return <> 88 - <header> 89 - <ProfilePicture onClick={onEditPicture} fallbackName={profile.display_name} src={profile.pfp_base64} /> 90 - <span className="grow" onClick={onEditName}>Hello,&nbsp;&nbsp;{profile.display_name}</span> 91 - </header> 92 - <main className="menu"> 93 - <button onClick={startLobby}> 94 - <IconBuildingBroadcastTowerFilled size="5em" /> 95 - Start Lobby 96 - </button> 97 - <button onClick={joinLobby}> 98 - <IconHexagonPlusFilled size="2.5em" /> 99 - Join Lobby 100 - </button> 101 - <button> 102 - <IconClockFilled size="1.5em" /> 103 - Past Games 104 - </button> 105 - </main> 106 - </>; 91 + return ( 92 + <> 93 + <header> 94 + <ProfilePicture 95 + onClick={onEditPicture} 96 + fallbackName={profile.display_name} 97 + src={profile.pfp_base64} 98 + /> 99 + <span className="grow" onClick={onEditName}> 100 + Hello,&nbsp;&nbsp;{profile.display_name} 101 + </span> 102 + </header> 103 + <main className="menu"> 104 + <button onClick={startLobby}> 105 + <IconBuildingBroadcastTowerFilled size="5em" /> 106 + Start Lobby 107 + </button> 108 + <button onClick={joinLobby}> 109 + <IconHexagonPlusFilled size="2.5em" /> 110 + Join Lobby 111 + </button> 112 + <button> 113 + <IconClockFilled size="1.5em" /> 114 + Past Games 115 + </button> 116 + </main> 117 + </> 118 + ); 107 119 }
+48 -33
frontend/src/components/ProfilePicture.tsx
··· 5 5 export type ProfileDecor = "hider" | "seeker"; 6 6 7 7 export const iconForDecor = (decor: ProfileDecor) => { 8 - if (decor === "hider") { 9 - return IconGhostFilled; 10 - } else { 11 - return IconBinocularsFilled; 12 - }; 8 + if (decor === "hider") { 9 + return IconGhostFilled; 10 + } else { 11 + return IconBinocularsFilled; 12 + } 13 13 }; 14 14 15 15 export type ProfilePictureProps = { 16 - fallbackName: string; 17 - src: string | null; 18 - decoration?: ProfileDecor; 19 - onClick?: () => void; 16 + fallbackName: string; 17 + src: string | null; 18 + decoration?: ProfileDecor; 19 + onClick?: () => void; 20 20 }; 21 21 22 22 const hashName = (str: string, seed = 3) => { 23 - let h1 = 0xdeadbeef ^ seed, 24 - h2 = 0x41c6ce57 ^ seed; 25 - for (let i = 0, ch; i < str.length; i++) { 26 - ch = str.charCodeAt(i); 27 - h1 = Math.imul(h1 ^ ch, 2654435761); 28 - h2 = Math.imul(h2 ^ ch, 1597334677); 29 - } 30 - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); 31 - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); 32 - return 4294967296 * (2097151 & h2) + (h1 >>> 0); 23 + let h1 = 0xdeadbeef ^ seed, 24 + h2 = 0x41c6ce57 ^ seed; 25 + for (let i = 0, ch; i < str.length; i++) { 26 + ch = str.charCodeAt(i); 27 + h1 = Math.imul(h1 ^ ch, 2654435761); 28 + h2 = Math.imul(h2 ^ ch, 1597334677); 29 + } 30 + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); 31 + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); 32 + return 4294967296 * (2097151 & h2) + (h1 >>> 0); 33 33 }; 34 34 35 - export default function ProfilePicture({ onClick, fallbackName, src, decoration }: ProfilePictureProps) { 36 - const fallback = src ? `data:image/webp;base64,${src}` : fallbackPicture; 37 - const hueshift = hashName(fallbackName) % 360; 35 + export default function ProfilePicture({ 36 + onClick, 37 + fallbackName, 38 + src, 39 + decoration 40 + }: ProfilePictureProps) { 41 + const fallback = src ? `data:image/webp;base64,${src}` : fallbackPicture; 42 + const hueshift = hashName(fallbackName) % 360; 38 43 39 - const style = src === null ? { 40 - filter: `hue-rotate(${hueshift}deg)`, 41 - } : undefined; 44 + const style = 45 + src === null 46 + ? { 47 + filter: `hue-rotate(${hueshift}deg)` 48 + } 49 + : undefined; 42 50 43 - const className = "pfp" + (decoration !== undefined ? ` ${decoration}` : ""); 51 + const className = "pfp" + (decoration !== undefined ? ` ${decoration}` : ""); 44 52 45 - const fallbackInitial = src === null ? (fallbackName[0] ?? "?") : undefined; 53 + const fallbackInitial = src === null ? (fallbackName[0] ?? "?") : undefined; 46 54 47 - const Icon = decoration !== undefined ? iconForDecor(decoration) : null; 55 + const Icon = decoration !== undefined ? iconForDecor(decoration) : null; 48 56 49 - return <span onClick={() => { onClick?.(); }} data-initial={fallbackInitial} className={className}> 50 - <img width={256} height={256} style={style} alt="Profile Picture" src={fallback} /> 51 - {Icon && <Icon size="1em"/>} 52 - </span>; 57 + return ( 58 + <span 59 + onClick={() => { 60 + onClick?.(); 61 + }} 62 + data-initial={fallbackInitial} 63 + className={className} 64 + > 65 + <img width={256} height={256} style={style} alt="Profile Picture" src={fallback} /> 66 + {Icon && <Icon size="1em" />} 67 + </span> 68 + ); 53 69 } 54 -
+184 -185
frontend/src/style.css
··· 1 1 :root { 2 - font-family: "Bungee"; 3 - user-select: none; 4 - -webkit-user-select: none; 2 + font-family: "Bungee"; 3 + user-select: none; 4 + -webkit-user-select: none; 5 5 6 - --scale: 1.25; 7 - --small: calc(1rem * pow(var(--scale), -1)); 8 - --half: calc(1rem * pow(var(--scale), -0.5)); 9 - --1: calc(1rem * pow(var(--scale), 0)); 10 - --2: calc(1rem * pow(var(--scale), 1)); 11 - --3: calc(1rem * pow(var(--scale), 2)); 12 - --4: calc(1rem * pow(var(--scale), 3)); 13 - --5: calc(1rem * pow(var(--scale), 4)); 14 - --6: calc(1rem * pow(var(--scale), 5)); 15 - --7: calc(1rem * pow(var(--scale), 6)); 16 - --8: calc(1rem * pow(var(--scale), 7)); 17 - --9: calc(1rem * pow(var(--scale), 8)); 18 - --10: calc(1rem * pow(var(--scale), 9)); 19 - --11: calc(1rem * pow(var(--scale), 10)); 20 - --12: calc(1rem * pow(var(--scale), 11)); 21 - --14: calc(1rem * pow(var(--scale), 13)); 22 - overflow: hidden; 6 + --scale: 1.25; 7 + --small: calc(1rem * pow(var(--scale), -1)); 8 + --half: calc(1rem * pow(var(--scale), -0.5)); 9 + --1: calc(1rem * pow(var(--scale), 0)); 10 + --2: calc(1rem * pow(var(--scale), 1)); 11 + --3: calc(1rem * pow(var(--scale), 2)); 12 + --4: calc(1rem * pow(var(--scale), 3)); 13 + --5: calc(1rem * pow(var(--scale), 4)); 14 + --6: calc(1rem * pow(var(--scale), 5)); 15 + --7: calc(1rem * pow(var(--scale), 6)); 16 + --8: calc(1rem * pow(var(--scale), 7)); 17 + --9: calc(1rem * pow(var(--scale), 8)); 18 + --10: calc(1rem * pow(var(--scale), 9)); 19 + --11: calc(1rem * pow(var(--scale), 10)); 20 + --12: calc(1rem * pow(var(--scale), 11)); 21 + --14: calc(1rem * pow(var(--scale), 13)); 22 + overflow: hidden; 23 23 } 24 24 25 25 body { 26 - display: flex; 27 - flex-direction: column; 28 - overflow: hidden; 29 - margin: 0; 30 - width: 100vw; 31 - height: 100vh; 26 + display: flex; 27 + flex-direction: column; 28 + overflow: hidden; 29 + margin: 0; 30 + width: 100vw; 31 + height: 100vh; 32 32 } 33 33 34 34 button { 35 - font-family: "Bungee"; 35 + font-family: "Bungee"; 36 36 } 37 37 38 38 header { 39 - font-size: 18pt; 40 - font-weight: bold; 41 - box-sizing: border-box; 42 - z-index: 10; 39 + font-size: 18pt; 40 + font-weight: bold; 41 + box-sizing: border-box; 42 + z-index: 10; 43 43 44 - display: flex; 45 - flex-direction: row; 46 - align-items: center; 44 + display: flex; 45 + flex-direction: row; 46 + align-items: center; 47 47 48 - box-shadow: #0001 0 2px 20px; 49 - background-color: #eee; 48 + box-shadow: #0001 0 2px 20px; 49 + background-color: #eee; 50 50 51 - padding: var(--1); 52 - gap: var(--1); 51 + padding: var(--1); 52 + gap: var(--1); 53 53 54 - .grow { 55 - flex-grow: 1; 56 - display: flex; 57 - height: 100%; 58 - align-items: center; 59 - } 54 + .grow { 55 + flex-grow: 1; 56 + display: flex; 57 + height: 100%; 58 + align-items: center; 59 + } 60 60 } 61 61 62 62 main { 63 - display: flex; 64 - flex-direction: column; 65 - flex-grow: 1; 63 + display: flex; 64 + flex-direction: column; 65 + flex-grow: 1; 66 + 67 + .map { 68 + flex-grow: 1; 69 + background-color: #111; 70 + } 66 71 67 - .map { 68 - flex-grow: 1; 69 - background-color: #111; 70 - } 72 + .lobby-pfps { 73 + z-index: 2; 74 + overflow-x: auto; 75 + box-shadow: 0 0 10px #0004; 76 + display: flex; 77 + flex-direction: row; 78 + gap: var(--2); 79 + width: 100%; 80 + font-size: 20pt; 81 + min-height: calc(20pt + var(--2)); 82 + padding: var(--small); 83 + } 71 84 72 - .lobby-pfps { 73 - z-index: 2; 74 - overflow-x: auto; 75 - box-shadow: 0 0 10px #0004; 76 - display: flex; 77 - flex-direction: row; 78 - gap: var(--2); 79 - width: 100%; 80 - font-size: 20pt; 81 - min-height: calc(20pt + var(--2)); 82 - padding: var(--small); 83 - } 85 + .team-button { 86 + display: flex; 87 + flex-direction: row; 88 + align-items: center; 89 + justify-content: center; 90 + font-size: 16pt; 91 + gap: var(--small); 92 + padding: var(--1); 93 + border: none; 84 94 85 - .team-button { 86 - display: flex; 87 - flex-direction: row; 88 - align-items: center; 89 - justify-content: center; 90 - font-size: 16pt; 91 - gap: var(--small); 92 - padding: var(--1); 93 - border: none; 94 - 95 - &.hider { 96 - background-color: #67c; 97 - } 95 + &.hider { 96 + background-color: #67c; 97 + } 98 98 99 - &.seeker { 100 - background-color: #c67; 101 - } 102 - } 99 + &.seeker { 100 + background-color: #c67; 101 + } 102 + } 103 103 104 - &.lobby > div.frame { 105 - flex-grow: 1; 106 - background-color: #aaa; 107 - position: relative; 108 - overflow-y: scroll; 109 - width: 100%; 104 + &.lobby > div.frame { 105 + flex-grow: 1; 106 + background-color: #aaa; 107 + position: relative; 108 + overflow-y: scroll; 109 + width: 100%; 110 110 111 - button.fab { 112 - background-color: black; 113 - color: white; 114 - border-radius: 50%; 115 - box-shadow: 0 0 5px black; 116 - border: none; 117 - display: flex; 118 - align-items: center; 119 - justify-content: center; 120 - font-size: 12pt; 121 - padding: var(--2); 122 - position: absolute; 123 - bottom: var(--small); 111 + button.fab { 112 + background-color: black; 113 + color: white; 114 + border-radius: 50%; 115 + box-shadow: 0 0 5px black; 116 + border: none; 117 + display: flex; 118 + align-items: center; 119 + justify-content: center; 120 + font-size: 12pt; 121 + padding: var(--2); 122 + position: absolute; 123 + bottom: var(--small); 124 124 125 - &.left { 126 - left: var(--small); 127 - } 125 + &.left { 126 + left: var(--small); 127 + } 128 128 129 - &.right { 130 - right: var(--small); 131 - } 132 - } 133 - } 129 + &.right { 130 + right: var(--small); 131 + } 132 + } 133 + } 134 134 135 - &.menu button { 136 - display: flex; 137 - flex-direction: row; 138 - align-items: center; 139 - justify-content: center; 140 - border-radius: 0; 141 - border: none; 142 - margin: 0; 143 - box-shadow: 0 0 25px black; 144 - width: 105%; 135 + &.menu button { 136 + display: flex; 137 + flex-direction: row; 138 + align-items: center; 139 + justify-content: center; 140 + border-radius: 0; 141 + border: none; 142 + margin: 0; 143 + box-shadow: 0 0 25px black; 144 + width: 105%; 145 145 146 - &:first-child { 147 - font-size: 35pt; 148 - flex-grow: 1; 149 - justify-content: safe; 150 - flex-direction: column; 151 - background-color: #6c6; 152 - margin-top: calc(-1 * var(--4)); 153 - transform: rotateZ(2deg) translateX(-7px) translateY(-30px); 154 - } 146 + &:first-child { 147 + font-size: 35pt; 148 + flex-grow: 1; 149 + justify-content: safe; 150 + flex-direction: column; 151 + background-color: #6c6; 152 + margin-top: calc(-1 * var(--4)); 153 + transform: rotateZ(2deg) translateX(-7px) translateY(-30px); 154 + } 155 155 156 - &:nth-child(2) { 157 - font-size: 28pt; 158 - min-height: 32%; 159 - flex-direction: column; 160 - background-color: #67c; 161 - margin-top: calc(-1 * var(--8)); 162 - padding-bottom: var(--5); 163 - transform: rotateZ(-2deg) translateX(-6px); 164 - } 156 + &:nth-child(2) { 157 + font-size: 28pt; 158 + min-height: 32%; 159 + flex-direction: column; 160 + background-color: #67c; 161 + margin-top: calc(-1 * var(--8)); 162 + padding-bottom: var(--5); 163 + transform: rotateZ(-2deg) translateX(-6px); 164 + } 165 165 166 - &:nth-child(3) { 167 - font-size: 25pt; 168 - min-height: 20%; 169 - margin-top: calc(-1 * var(--4)); 170 - gap: 4px; 171 - background-color: #c67; 172 - transform: rotateZ(1deg) translateY(5px); 173 - } 174 - } 166 + &:nth-child(3) { 167 + font-size: 25pt; 168 + min-height: 20%; 169 + margin-top: calc(-1 * var(--4)); 170 + gap: 4px; 171 + background-color: #c67; 172 + transform: rotateZ(1deg) translateY(5px); 173 + } 174 + } 175 175 } 176 - 177 176 178 177 span.pfp { 179 - border-radius: 50%; 180 - position: relative; 181 - display: flex; 182 - align-items: center; 183 - justify-content: center; 178 + border-radius: 50%; 179 + position: relative; 180 + display: flex; 181 + align-items: center; 182 + justify-content: center; 184 183 185 - --deco-color: #0000; 184 + --deco-color: #0000; 186 185 187 - box-sizing: border-box; 188 - border-style: solid; 189 - border-width: 4px; 190 - border-color: var(--deco-color); 186 + box-sizing: border-box; 187 + border-style: solid; 188 + border-width: 4px; 189 + border-color: var(--deco-color); 191 190 192 - img { 193 - width: 2em; 194 - height: 2em; 195 - border-radius: 50%; 196 - } 191 + img { 192 + width: 2em; 193 + height: 2em; 194 + border-radius: 50%; 195 + } 197 196 198 - svg { 199 - position: absolute; 200 - bottom: -10%; 201 - right: -15%; 202 - color: var(--deco-color); 203 - filter: drop-shadow(0 0 2px black); 204 - border-radius: 50%; 205 - padding: 1px; 206 - text-align: center; 207 - } 197 + svg { 198 + position: absolute; 199 + bottom: -10%; 200 + right: -15%; 201 + color: var(--deco-color); 202 + filter: drop-shadow(0 0 2px black); 203 + border-radius: 50%; 204 + padding: 1px; 205 + text-align: center; 206 + } 208 207 209 - &[data-initial]::after { 210 - content: attr(data-initial); 211 - color: white; 212 - filter: drop-shadow(0 0 4px black); 213 - position: absolute; 214 - display: flex; 215 - align-items: center; 216 - justify-content: center; 217 - } 208 + &[data-initial]::after { 209 + content: attr(data-initial); 210 + color: white; 211 + filter: drop-shadow(0 0 4px black); 212 + position: absolute; 213 + display: flex; 214 + align-items: center; 215 + justify-content: center; 216 + } 218 217 219 - &.seeker { 220 - --deco-color: #c67; 221 - } 218 + &.seeker { 219 + --deco-color: #c67; 220 + } 222 221 223 - &.hider { 224 - --deco-color: #67c; 225 - } 222 + &.hider { 223 + --deco-color: #67c; 224 + } 226 225 }
+16 -14
frontend/vite.config.ts
··· 4 4 import react from "@vitejs/plugin-react"; 5 5 import path from "path"; 6 6 7 - import browserslist from 'browserslist'; 8 - import {browserslistToTargets} from 'lightningcss'; 7 + import browserslist from "browserslist"; 8 + import { browserslistToTargets } from "lightningcss"; 9 9 10 10 const host = process.env.HOST_OVERRIDE || process.env.TAURI_DEV_HOST; 11 11 12 12 export default defineConfig(async () => ({ 13 - plugins: [react({ 14 - babel: { 15 - plugins: ['babel-plugin-react-compiler'], 16 - }, 17 - })], 13 + plugins: [ 14 + react({ 15 + babel: { 16 + plugins: ["babel-plugin-react-compiler"] 17 + } 18 + }) 19 + ], 18 20 clearScreen: false, 19 21 server: { 20 22 port: 1420, ··· 32 34 alias: [{ find: "@", replacement: path.resolve(__dirname, "./src") }] 33 35 }, 34 36 css: { 35 - transformer: 'lightningcss', 36 - lightningcss: { 37 - targets: browserslistToTargets(browserslist('>= 0.25%')) 37 + transformer: "lightningcss", 38 + lightningcss: { 39 + targets: browserslistToTargets(browserslist(">= 0.25%")) 40 + } 41 + }, 42 + build: { 43 + cssMinify: "lightningcss" 38 44 } 39 - }, 40 - build: { 41 - cssMinify: 'lightningcss' 42 - }, 43 45 }));
+8 -6
manhunt-app/src/profiles.rs
··· 3 3 use image::{ImageReader, codecs::webp::WebPEncoder, imageops::FilterType}; 4 4 use log::info; 5 5 use manhunt_logic::PlayerProfile; 6 - use tauri_plugin_fs::{FsExt, OpenOptions}; 7 6 use std::io::BufReader; 8 7 use tauri::AppHandle; 9 8 use tauri_plugin_dialog::{DialogExt, FileAccessMode, PickerMode}; 9 + use tauri_plugin_fs::{FsExt, OpenOptions}; 10 10 use tauri_plugin_store::StoreExt; 11 11 12 12 type Result<T = (), E = anyhow::Error> = std::result::Result<T, E>; ··· 18 18 const SUPPORTED_EXTS: [&str; 7] = ["avif", "png", "jpg", "jpeg", "png", "tiff", "webp"]; 19 19 20 20 fn create_profile_picture(file: std::fs::File) -> Result<String> { 21 - 22 21 let reader = BufReader::new(file); 23 - let img = ImageReader::new(reader).with_guessed_format().context("Failed to guess format")?.decode().context("Failed to read image file")?; 22 + let img = ImageReader::new(reader) 23 + .with_guessed_format() 24 + .context("Failed to guess format")? 25 + .decode() 26 + .context("Failed to read image file")?; 24 27 25 28 let img = img.resize_exact(IMAGE_SIZE, IMAGE_SIZE, FilterType::Lanczos3); 26 29 ··· 33 36 } 34 37 35 38 pub fn profile_picture_flow(app: &AppHandle) -> Result<Option<String>> { 36 - 37 39 let exts = &SUPPORTED_EXTS.as_slice(); 38 40 39 41 let dialog = app ··· 44 46 .add_filter("Images", exts); 45 47 46 48 let file = dialog.blocking_pick_file(); 47 - 49 + 48 50 if let Some(file) = file { 49 51 info!("Picked {file:?}"); 50 52 let fs = app.fs(); 51 - 53 + 52 54 let mut opts = OpenOptions::new(); 53 55 opts.read(true); 54 56 let file = fs.open(file, opts).context("Failed to open file")?;