tangled
alpha
login
or
join now
bwc9876.dev
/
manhunt-app
0
fork
atom
Live location tracking and playback for the game "manhunt"
0
fork
atom
overview
issues
pulls
1
pipelines
Use treefmt
bwc9876.dev
2 weeks ago
6b5163b2
c840c203
verified
This commit was signed with the committer's
known signature
.
bwc9876.dev
SSH Key Fingerprint:
SHA256:DanMEP/RNlSC7pAVbnXO6wzQV00rqyKj053tz4uH5gQ=
+417
-298
10 changed files
expand all
collapse all
unified
split
flake.lock
flake.nix
frontend
.oxlintrc.json
src
components
App.tsx
LobbyScreen.tsx
MenuScreen.tsx
ProfilePicture.tsx
style.css
vite.config.ts
manhunt-app
src
profiles.rs
+44
flake.lock
···
18
"type": "github"
19
}
20
},
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
21
"nixpkgs": {
22
"locked": {
23
"lastModified": 1771008912,
···
53
"root": {
54
"inputs": {
55
"flakelight": "flakelight",
0
56
"nixpkgs": "nixpkgs_2",
57
"rust-overlay": "rust-overlay"
58
}
···
74
"original": {
75
"owner": "oxalica",
76
"repo": "rust-overlay",
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
77
"type": "github"
78
}
79
}
···
18
"type": "github"
19
}
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
+
},
42
"nixpkgs": {
43
"locked": {
44
"lastModified": 1771008912,
···
74
"root": {
75
"inputs": {
76
"flakelight": "flakelight",
77
+
"flakelight-treefmt": "flakelight-treefmt",
78
"nixpkgs": "nixpkgs_2",
79
"rust-overlay": "rust-overlay"
80
}
···
96
"original": {
97
"owner": "oxalica",
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",
121
"type": "github"
122
}
123
}
+18
-17
flake.nix
···
4
flakelight.url = "github:nix-community/flakelight";
5
rust-overlay.url = "github:oxalica/rust-overlay";
6
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
0
0
7
};
8
outputs = {flakelight, ...} @ inputs:
9
flakelight ./. {
10
inherit inputs;
0
11
withOverlays = [inputs.rust-overlay.overlays.default];
12
nixpkgs.config = {
13
allowUnfree = true;
14
android_sdk.accept_license = true;
15
};
16
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;
33
};
34
35
devShell = pkgs: let
···
82
pkg-config
83
gobject-introspection
84
nodePackages.prettier
85
-
(rust-bin.stable.latest.default.override {targets = ["aarch64-linux-android" "armv7-linux-androideabi" "i686-linux-android" "x86_64-linux-android"];})
0
0
0
0
0
0
0
86
cargo-tauri
87
nodejs
88
(android-studio.withSdk androidComposition.androidsdk)
···
4
flakelight.url = "github:nix-community/flakelight";
5
rust-overlay.url = "github:oxalica/rust-overlay";
6
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
7
+
flakelight-treefmt.url = "github:m15a/flakelight-treefmt";
8
+
flakelight-treefmt.inputs.flakelight.follows = "flakelight";
9
};
10
outputs = {flakelight, ...} @ inputs:
11
flakelight ./. {
12
inherit inputs;
13
+
imports = [inputs.flakelight-treefmt.flakelightModules.default];
14
withOverlays = [inputs.rust-overlay.overlays.default];
15
nixpkgs.config = {
16
allowUnfree = true;
17
android_sdk.accept_license = true;
18
};
19
20
+
treefmtConfig = {pkgs, ...}: {
21
+
programs = {
22
+
alejandra.enable = true;
23
+
just.enable = true;
24
+
prettier.enable = true;
25
+
rustfmt.enable = true;
26
+
};
0
0
0
0
0
0
0
0
0
27
};
28
29
devShell = pkgs: let
···
76
pkg-config
77
gobject-introspection
78
nodePackages.prettier
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
+
})
87
cargo-tauri
88
nodejs
89
(android-studio.withSdk androidComposition.androidsdk)
+1
-1
frontend/.oxlintrc.json
···
1
{
2
-
"ignorePatterns": ["src/bindings.ts"]
3
}
···
1
{
2
+
"ignorePatterns": ["src/bindings.ts"]
3
}
+1
-2
frontend/src/components/App.tsx
···
7
import GameScreen from "./GameScreen";
8
9
function ScreenRouter({ screen }: { screen: AppScreen }) {
10
-
11
console.debug(`Render screen ${screen}`);
12
13
switch (screen) {
···
24
}
25
}
26
27
-
export default function App({initialScreen}: {initialScreen: AppScreen}) {
28
const [currentScreen, setScreen] = useState(initialScreen);
29
30
useTauriEvent("changeScreen", (newScreen) => {
···
7
import GameScreen from "./GameScreen";
8
9
function ScreenRouter({ screen }: { screen: AppScreen }) {
0
10
console.debug(`Render screen ${screen}`);
11
12
switch (screen) {
···
23
}
24
}
25
26
+
export default function App({ initialScreen }: { initialScreen: AppScreen }) {
27
const [currentScreen, setScreen] = useState(initialScreen);
28
29
useTauriEvent("changeScreen", (newScreen) => {
+62
-17
frontend/src/components/LobbyScreen.tsx
···
3
import { useTauriEvent } from "@/lib/hooks";
4
import ProfilePicture, { iconForDecor, ProfileDecor } from "./ProfilePicture";
5
import { tempSettings } from "./MenuScreen";
6
-
import { IconArrowBigLeftLinesFilled, IconCircleCheckFilled, IconCircleDashedPlus } from "@tabler/icons-react";
0
0
0
0
7
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>;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
12
}
13
14
-
function TeamButton({ onClick, active, deco, text }: { onClick: () => void, active: boolean, deco: ProfileDecor, text: string }) {
15
-
0
0
0
0
0
0
0
0
0
16
const Icon = iconForDecor(deco);
17
18
-
return <button onClick={onClick} className={`team-button ${deco}`}>
19
-
<Icon />
20
-
{active ? "You're On" : "Join"} {text}
21
-
{active ? <IconCircleCheckFilled /> : <IconCircleDashedPlus />}
22
-
</button>;
0
0
23
}
24
25
const initLobbyState: LobbyState = {
···
28
teams: {},
29
self_id: "",
30
is_host: false,
31
-
settings: tempSettings,
32
};
33
34
export default function LobbyScreen() {
···
52
});
53
});
54
55
-
const profiles = Object.entries(lobbyState.profiles).filter(([_, p]) => p !== undefined) as [string, PlayerProfile][];
0
0
0
56
57
const seekers = profiles.filter(([id, _]) => lobbyState.teams[id] ?? false);
58
const hiders = profiles.filter(([id, _]) => !(lobbyState.teams[id] ?? false));
···
76
</header>
77
<main className="lobby">
78
<ProfileList profiles={seekers} decoration="seeker" />
79
-
<TeamButton onClick={() => commands.switchTeams(true)} active={isSeeker} text="Seekers" deco="seeker" />
0
0
0
0
0
80
<div className="frame">
81
-
<button onClick={onLeaveLobby} aria-label="Leave Lobby" className="fab left"><IconArrowBigLeftLinesFilled size="2em"/></button>
0
0
82
</div>
83
-
<TeamButton onClick={() => commands.switchTeams(false)} active={!isSeeker} text="Hiders" deco="hider" />
0
0
0
0
0
84
<ProfileList profiles={hiders} decoration="hider" />
85
</main>
86
</>
···
3
import { useTauriEvent } from "@/lib/hooks";
4
import ProfilePicture, { iconForDecor, ProfileDecor } from "./ProfilePicture";
5
import { tempSettings } from "./MenuScreen";
6
+
import {
7
+
IconArrowBigLeftLinesFilled,
8
+
IconCircleCheckFilled,
9
+
IconCircleDashedPlus
10
+
} from "@tabler/icons-react";
11
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
+
);
31
}
32
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
+
}) {
44
const Icon = iconForDecor(deco);
45
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
+
);
53
}
54
55
const initLobbyState: LobbyState = {
···
58
teams: {},
59
self_id: "",
60
is_host: false,
61
+
settings: tempSettings
62
};
63
64
export default function LobbyScreen() {
···
82
});
83
});
84
85
+
const profiles = Object.entries(lobbyState.profiles).filter(([_, p]) => p !== undefined) as [
86
+
string,
87
+
PlayerProfile
88
+
][];
89
90
const seekers = profiles.filter(([id, _]) => lobbyState.teams[id] ?? false);
91
const hiders = profiles.filter(([id, _]) => !(lobbyState.teams[id] ?? false));
···
109
</header>
110
<main className="lobby">
111
<ProfileList profiles={seekers} decoration="seeker" />
112
+
<TeamButton
113
+
onClick={() => commands.switchTeams(true)}
114
+
active={isSeeker}
115
+
text="Seekers"
116
+
deco="seeker"
117
+
/>
118
<div className="frame">
119
+
<button onClick={onLeaveLobby} aria-label="Leave Lobby" className="fab left">
120
+
<IconArrowBigLeftLinesFilled size="2em" />
121
+
</button>
122
</div>
123
+
<TeamButton
124
+
onClick={() => commands.switchTeams(false)}
125
+
active={!isSeeker}
126
+
text="Hiders"
127
+
deco="hider"
128
+
/>
129
<ProfileList profiles={hiders} decoration="hider" />
130
</main>
131
</>
+35
-23
frontend/src/components/MenuScreen.tsx
···
1
import React, { useCallback, useEffect, useState } from "react";
2
import "@fontsource/bungee";
3
-
import { IconBuildingBroadcastTowerFilled, IconHexagonPlusFilled, IconClockFilled } from "@tabler/icons-react";
0
0
0
0
4
import { commands, GameSettings, PlayerProfile } from "@/bindings";
5
import ProfilePicture from "./ProfilePicture";
6
···
24
25
const defaultProfile: PlayerProfile = {
26
display_name: "",
27
-
pfp_base64: null,
28
};
29
30
export default function MenuScreen() {
···
73
};
74
75
const onEditPicture = () => {
76
-
commands.createProfilePicture().then(newPic => {
77
if (!newPic) {
78
return;
79
}
···
84
});
85
};
86
87
-
return <>
88
-
<header>
89
-
<ProfilePicture onClick={onEditPicture} fallbackName={profile.display_name} src={profile.pfp_base64} />
90
-
<span className="grow" onClick={onEditName}>Hello, {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
-
</>;
0
0
0
0
0
0
0
0
107
}
···
1
import React, { useCallback, useEffect, useState } from "react";
2
import "@fontsource/bungee";
3
+
import {
4
+
IconBuildingBroadcastTowerFilled,
5
+
IconHexagonPlusFilled,
6
+
IconClockFilled
7
+
} from "@tabler/icons-react";
8
import { commands, GameSettings, PlayerProfile } from "@/bindings";
9
import ProfilePicture from "./ProfilePicture";
10
···
28
29
const defaultProfile: PlayerProfile = {
30
display_name: "",
31
+
pfp_base64: null
32
};
33
34
export default function MenuScreen() {
···
77
};
78
79
const onEditPicture = () => {
80
+
commands.createProfilePicture().then((newPic) => {
81
if (!newPic) {
82
return;
83
}
···
88
});
89
};
90
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, {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
+
);
119
}
+48
-33
frontend/src/components/ProfilePicture.tsx
···
5
export type ProfileDecor = "hider" | "seeker";
6
7
export const iconForDecor = (decor: ProfileDecor) => {
8
-
if (decor === "hider") {
9
-
return IconGhostFilled;
10
-
} else {
11
-
return IconBinocularsFilled;
12
-
};
13
};
14
15
export type ProfilePictureProps = {
16
-
fallbackName: string;
17
-
src: string | null;
18
-
decoration?: ProfileDecor;
19
-
onClick?: () => void;
20
};
21
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);
33
};
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;
0
0
0
0
0
38
39
-
const style = src === null ? {
40
-
filter: `hue-rotate(${hueshift}deg)`,
41
-
} : undefined;
0
0
0
42
43
-
const className = "pfp" + (decoration !== undefined ? ` ${decoration}` : "");
44
45
-
const fallbackInitial = src === null ? (fallbackName[0] ?? "?") : undefined;
46
47
-
const Icon = decoration !== undefined ? iconForDecor(decoration) : null;
48
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>;
0
0
0
0
0
0
0
0
53
}
54
-
···
5
export type ProfileDecor = "hider" | "seeker";
6
7
export const iconForDecor = (decor: ProfileDecor) => {
8
+
if (decor === "hider") {
9
+
return IconGhostFilled;
10
+
} else {
11
+
return IconBinocularsFilled;
12
+
}
13
};
14
15
export type ProfilePictureProps = {
16
+
fallbackName: string;
17
+
src: string | null;
18
+
decoration?: ProfileDecor;
19
+
onClick?: () => void;
20
};
21
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);
33
};
34
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;
43
44
+
const style =
45
+
src === null
46
+
? {
47
+
filter: `hue-rotate(${hueshift}deg)`
48
+
}
49
+
: undefined;
50
51
+
const className = "pfp" + (decoration !== undefined ? ` ${decoration}` : "");
52
53
+
const fallbackInitial = src === null ? (fallbackName[0] ?? "?") : undefined;
54
55
+
const Icon = decoration !== undefined ? iconForDecor(decoration) : null;
56
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
+
);
69
}
0
+184
-185
frontend/src/style.css
···
1
:root {
2
-
font-family: "Bungee";
3
-
user-select: none;
4
-
-webkit-user-select: none;
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;
23
}
24
25
body {
26
-
display: flex;
27
-
flex-direction: column;
28
-
overflow: hidden;
29
-
margin: 0;
30
-
width: 100vw;
31
-
height: 100vh;
32
}
33
34
button {
35
-
font-family: "Bungee";
36
}
37
38
header {
39
-
font-size: 18pt;
40
-
font-weight: bold;
41
-
box-sizing: border-box;
42
-
z-index: 10;
43
44
-
display: flex;
45
-
flex-direction: row;
46
-
align-items: center;
47
48
-
box-shadow: #0001 0 2px 20px;
49
-
background-color: #eee;
50
51
-
padding: var(--1);
52
-
gap: var(--1);
53
54
-
.grow {
55
-
flex-grow: 1;
56
-
display: flex;
57
-
height: 100%;
58
-
align-items: center;
59
-
}
60
}
61
62
main {
63
-
display: flex;
64
-
flex-direction: column;
65
-
flex-grow: 1;
0
0
0
0
0
66
67
-
.map {
68
-
flex-grow: 1;
69
-
background-color: #111;
70
-
}
0
0
0
0
0
0
0
0
71
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
-
}
84
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
-
}
98
99
-
&.seeker {
100
-
background-color: #c67;
101
-
}
102
-
}
103
104
-
&.lobby > div.frame {
105
-
flex-grow: 1;
106
-
background-color: #aaa;
107
-
position: relative;
108
-
overflow-y: scroll;
109
-
width: 100%;
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);
124
125
-
&.left {
126
-
left: var(--small);
127
-
}
128
129
-
&.right {
130
-
right: var(--small);
131
-
}
132
-
}
133
-
}
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%;
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
-
}
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
-
}
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
-
}
175
}
176
-
177
178
span.pfp {
179
-
border-radius: 50%;
180
-
position: relative;
181
-
display: flex;
182
-
align-items: center;
183
-
justify-content: center;
184
185
-
--deco-color: #0000;
186
187
-
box-sizing: border-box;
188
-
border-style: solid;
189
-
border-width: 4px;
190
-
border-color: var(--deco-color);
191
192
-
img {
193
-
width: 2em;
194
-
height: 2em;
195
-
border-radius: 50%;
196
-
}
197
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
-
}
208
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
-
}
218
219
-
&.seeker {
220
-
--deco-color: #c67;
221
-
}
222
223
-
&.hider {
224
-
--deco-color: #67c;
225
-
}
226
}
···
1
:root {
2
+
font-family: "Bungee";
3
+
user-select: none;
4
+
-webkit-user-select: none;
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;
23
}
24
25
body {
26
+
display: flex;
27
+
flex-direction: column;
28
+
overflow: hidden;
29
+
margin: 0;
30
+
width: 100vw;
31
+
height: 100vh;
32
}
33
34
button {
35
+
font-family: "Bungee";
36
}
37
38
header {
39
+
font-size: 18pt;
40
+
font-weight: bold;
41
+
box-sizing: border-box;
42
+
z-index: 10;
43
44
+
display: flex;
45
+
flex-direction: row;
46
+
align-items: center;
47
48
+
box-shadow: #0001 0 2px 20px;
49
+
background-color: #eee;
50
51
+
padding: var(--1);
52
+
gap: var(--1);
53
54
+
.grow {
55
+
flex-grow: 1;
56
+
display: flex;
57
+
height: 100%;
58
+
align-items: center;
59
+
}
60
}
61
62
main {
63
+
display: flex;
64
+
flex-direction: column;
65
+
flex-grow: 1;
66
+
67
+
.map {
68
+
flex-grow: 1;
69
+
background-color: #111;
70
+
}
71
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
+
}
84
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;
0
0
0
94
95
+
&.hider {
96
+
background-color: #67c;
97
+
}
0
0
0
0
0
0
0
0
0
0
98
99
+
&.seeker {
100
+
background-color: #c67;
101
+
}
102
+
}
103
104
+
&.lobby > div.frame {
105
+
flex-grow: 1;
106
+
background-color: #aaa;
107
+
position: relative;
108
+
overflow-y: scroll;
109
+
width: 100%;
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);
124
125
+
&.left {
126
+
left: var(--small);
127
+
}
128
129
+
&.right {
130
+
right: var(--small);
131
+
}
132
+
}
133
+
}
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%;
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
+
}
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
+
}
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
+
}
175
}
0
176
177
span.pfp {
178
+
border-radius: 50%;
179
+
position: relative;
180
+
display: flex;
181
+
align-items: center;
182
+
justify-content: center;
183
184
+
--deco-color: #0000;
185
186
+
box-sizing: border-box;
187
+
border-style: solid;
188
+
border-width: 4px;
189
+
border-color: var(--deco-color);
190
191
+
img {
192
+
width: 2em;
193
+
height: 2em;
194
+
border-radius: 50%;
195
+
}
196
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
+
}
207
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
+
}
217
218
+
&.seeker {
219
+
--deco-color: #c67;
220
+
}
221
222
+
&.hider {
223
+
--deco-color: #67c;
224
+
}
225
}
+16
-14
frontend/vite.config.ts
···
4
import react from "@vitejs/plugin-react";
5
import path from "path";
6
7
-
import browserslist from 'browserslist';
8
-
import {browserslistToTargets} from 'lightningcss';
9
10
const host = process.env.HOST_OVERRIDE || process.env.TAURI_DEV_HOST;
11
12
export default defineConfig(async () => ({
13
-
plugins: [react({
14
-
babel: {
15
-
plugins: ['babel-plugin-react-compiler'],
16
-
},
17
-
})],
0
0
18
clearScreen: false,
19
server: {
20
port: 1420,
···
32
alias: [{ find: "@", replacement: path.resolve(__dirname, "./src") }]
33
},
34
css: {
35
-
transformer: 'lightningcss',
36
-
lightningcss: {
37
-
targets: browserslistToTargets(browserslist('>= 0.25%'))
0
0
0
0
38
}
39
-
},
40
-
build: {
41
-
cssMinify: 'lightningcss'
42
-
},
43
}));
···
4
import react from "@vitejs/plugin-react";
5
import path from "path";
6
7
+
import browserslist from "browserslist";
8
+
import { browserslistToTargets } from "lightningcss";
9
10
const host = process.env.HOST_OVERRIDE || process.env.TAURI_DEV_HOST;
11
12
export default defineConfig(async () => ({
13
+
plugins: [
14
+
react({
15
+
babel: {
16
+
plugins: ["babel-plugin-react-compiler"]
17
+
}
18
+
})
19
+
],
20
clearScreen: false,
21
server: {
22
port: 1420,
···
34
alias: [{ find: "@", replacement: path.resolve(__dirname, "./src") }]
35
},
36
css: {
37
+
transformer: "lightningcss",
38
+
lightningcss: {
39
+
targets: browserslistToTargets(browserslist(">= 0.25%"))
40
+
}
41
+
},
42
+
build: {
43
+
cssMinify: "lightningcss"
44
}
0
0
0
0
45
}));
+8
-6
manhunt-app/src/profiles.rs
···
3
use image::{ImageReader, codecs::webp::WebPEncoder, imageops::FilterType};
4
use log::info;
5
use manhunt_logic::PlayerProfile;
6
-
use tauri_plugin_fs::{FsExt, OpenOptions};
7
use std::io::BufReader;
8
use tauri::AppHandle;
9
use tauri_plugin_dialog::{DialogExt, FileAccessMode, PickerMode};
0
10
use tauri_plugin_store::StoreExt;
11
12
type Result<T = (), E = anyhow::Error> = std::result::Result<T, E>;
···
18
const SUPPORTED_EXTS: [&str; 7] = ["avif", "png", "jpg", "jpeg", "png", "tiff", "webp"];
19
20
fn create_profile_picture(file: std::fs::File) -> Result<String> {
21
-
22
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")?;
0
0
0
0
24
25
let img = img.resize_exact(IMAGE_SIZE, IMAGE_SIZE, FilterType::Lanczos3);
26
···
33
}
34
35
pub fn profile_picture_flow(app: &AppHandle) -> Result<Option<String>> {
36
-
37
let exts = &SUPPORTED_EXTS.as_slice();
38
39
let dialog = app
···
44
.add_filter("Images", exts);
45
46
let file = dialog.blocking_pick_file();
47
-
48
if let Some(file) = file {
49
info!("Picked {file:?}");
50
let fs = app.fs();
51
-
52
let mut opts = OpenOptions::new();
53
opts.read(true);
54
let file = fs.open(file, opts).context("Failed to open file")?;
···
3
use image::{ImageReader, codecs::webp::WebPEncoder, imageops::FilterType};
4
use log::info;
5
use manhunt_logic::PlayerProfile;
0
6
use std::io::BufReader;
7
use tauri::AppHandle;
8
use tauri_plugin_dialog::{DialogExt, FileAccessMode, PickerMode};
9
+
use tauri_plugin_fs::{FsExt, OpenOptions};
10
use tauri_plugin_store::StoreExt;
11
12
type Result<T = (), E = anyhow::Error> = std::result::Result<T, E>;
···
18
const SUPPORTED_EXTS: [&str; 7] = ["avif", "png", "jpg", "jpeg", "png", "tiff", "webp"];
19
20
fn create_profile_picture(file: std::fs::File) -> Result<String> {
0
21
let reader = BufReader::new(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")?;
27
28
let img = img.resize_exact(IMAGE_SIZE, IMAGE_SIZE, FilterType::Lanczos3);
29
···
36
}
37
38
pub fn profile_picture_flow(app: &AppHandle) -> Result<Option<String>> {
0
39
let exts = &SUPPORTED_EXTS.as_slice();
40
41
let dialog = app
···
46
.add_filter("Images", exts);
47
48
let file = dialog.blocking_pick_file();
49
+
50
if let Some(file) = file {
51
info!("Picked {file:?}");
52
let fs = app.fs();
53
+
54
let mut opts = OpenOptions::new();
55
opts.read(true);
56
let file = fs.open(file, opts).context("Failed to open file")?;