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