tangled
alpha
login
or
join now
stream.place
/
streamplace
74
fork
atom
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
Add build info and misc settings UI improvements
Natalie B.
3 months ago
392813a0
5415e457
+314
-197
17 changed files
expand all
collapse all
unified
split
.gitignore
js
app
components
settings
about-category-settings.tsx
advanced-category-settings.tsx
components
setting-toggle.tsx
settings-navigation-item.tsx
danmu-category-settings.tsx
privacy-category-settings.tsx
settings-item-link.tsx
settings.tsx
streaming-category-settings.tsx
updates.native.tsx
updates.tsx
webhook-manager.tsx
package.json
scripts
generate-build-info.js
src
router.tsx
components
src
components
ui
button.tsx
+1
.gitignore
···
23
23
target
24
24
my-release-key.keystore
25
25
test.xml
26
26
+
js/app/src/build-info.json
+47
-5
js/app/components/settings/about-category-settings.tsx
···
1
1
import { Text, View, zero } from "@streamplace/components";
2
2
import { useTranslation } from "react-i18next";
3
3
import { ScrollView } from "react-native";
4
4
-
import pkg from "../../package.json";
4
4
+
import { Updates } from "./updates";
5
5
+
6
6
+
let buildInfo: {
7
7
+
hash: string;
8
8
+
shortHash: string;
9
9
+
branch: string;
10
10
+
tag: string;
11
11
+
isDirty: boolean;
12
12
+
buildTime: string;
13
13
+
} | null = null;
14
14
+
15
15
+
try {
16
16
+
buildInfo = require("../../src/build-info.json");
17
17
+
} catch {
18
18
+
// build-info.json doesn't exist in dev mode
19
19
+
}
5
20
6
21
export function AboutCategorySettings() {
7
22
const { t } = useTranslation("settings");
8
23
24
24
+
const getBuildStatus = () => {
25
25
+
if (!buildInfo) {
26
26
+
return "dev";
27
27
+
}
28
28
+
return buildInfo.isDirty || process?.env.NODE_ENV === "development"
29
29
+
? "dev"
30
30
+
: "prod";
31
31
+
};
32
32
+
33
33
+
const buildLabel = buildInfo ? buildInfo.tag : "development";
34
34
+
const buildStatus = getBuildStatus();
35
35
+
9
36
return (
10
37
<ScrollView>
11
38
<View style={[zero.layout.flex.align.center, zero.px[4], zero.py[4]]}>
···
16
43
]}
17
44
>
18
45
<View>
19
19
-
<Text size="xl">{t("app-version", { version: pkg.version })}</Text>
20
20
-
<Text size="lg" color="muted">
21
21
-
{t("app-version-description")}
22
22
-
</Text>
46
46
+
<Text>This version is </Text>
47
47
+
<Updates />
48
48
+
</View>
49
49
+
50
50
+
<View
51
51
+
style={[
52
52
+
{ flexDirection: "row" },
53
53
+
{ alignItems: "flex-start" },
54
54
+
{ justifyContent: "flex-start" },
55
55
+
]}
56
56
+
>
57
57
+
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
58
58
+
<Text size="lg">Build</Text>
59
59
+
</View>
60
60
+
<View style={{ alignItems: "flex-end" }}>
61
61
+
<Text size="lg" color="muted">
62
62
+
{buildLabel} ({buildStatus})
63
63
+
</Text>
64
64
+
</View>
23
65
</View>
24
66
</View>
25
67
</View>
+8
-19
js/app/components/settings/advanced-category-settings.tsx
···
1
1
import { Button, Input, Text, View, zero } from "@streamplace/components";
2
2
import { useEffect, useState } from "react";
3
3
import { useTranslation } from "react-i18next";
4
4
-
import { ScrollView, Switch } from "react-native";
4
4
+
import { ScrollView } from "react-native";
5
5
import { useStore } from "store";
6
6
import { DEFAULT_URL } from "store/slices/streamplaceSlice";
7
7
+
import { SettingToggle } from "./components/setting-toggle";
7
8
8
9
export function AdvancedCategorySettings() {
9
10
const url = useStore((state) => state.url);
···
56
57
zero.gap.all[4],
57
58
]}
58
59
>
59
59
-
<View
60
60
-
style={[
61
61
-
{ flexDirection: "row" },
62
62
-
{ alignItems: "flex-start" },
63
63
-
{ justifyContent: "flex-start" },
64
64
-
]}
65
65
-
>
66
66
-
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
67
67
-
<Text size="xl">{t("use-custom-node")}</Text>
68
68
-
<Text size="lg" color="muted">
69
69
-
{t("default-url", { url: defaultUrl })}
70
70
-
</Text>
71
71
-
</View>
72
72
-
<Switch
73
73
-
value={overrideEnabled}
74
74
-
onValueChange={handleToggleOverride}
75
75
-
/>
76
76
-
</View>
60
60
+
<SettingToggle
61
61
+
title={t("use-custom-node")}
62
62
+
description={t("default-url", { url: defaultUrl })}
63
63
+
value={overrideEnabled}
64
64
+
onValueChange={handleToggleOverride}
65
65
+
/>
77
66
78
67
{overrideEnabled && (
79
68
<View
+40
js/app/components/settings/components/setting-toggle.tsx
···
1
1
+
import { Text, View } from "@streamplace/components";
2
2
+
import { mergeStyles } from "@streamplace/components/src/ui";
3
3
+
import { Switch, ViewStyle } from "react-native";
4
4
+
5
5
+
export interface SettingToggleProps {
6
6
+
title: string;
7
7
+
description?: string;
8
8
+
value: boolean;
9
9
+
onValueChange: (value: boolean) => void;
10
10
+
style?: ViewStyle;
11
11
+
}
12
12
+
13
13
+
export function SettingToggle({
14
14
+
title,
15
15
+
description,
16
16
+
value,
17
17
+
onValueChange,
18
18
+
style,
19
19
+
}: SettingToggleProps) {
20
20
+
return (
21
21
+
<View
22
22
+
style={mergeStyles(
23
23
+
{ flexDirection: "row" },
24
24
+
{ alignItems: "flex-start" },
25
25
+
{ justifyContent: "flex-start" },
26
26
+
style,
27
27
+
)}
28
28
+
>
29
29
+
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
30
30
+
<Text size="xl">{title}</Text>
31
31
+
{description && (
32
32
+
<Text size="lg" color="muted">
33
33
+
{description}
34
34
+
</Text>
35
35
+
)}
36
36
+
</View>
37
37
+
<Switch value={value} onValueChange={onValueChange} />
38
38
+
</View>
39
39
+
);
40
40
+
}
+8
-21
js/app/components/settings/danmu-category-settings.tsx
···
8
8
} from "@streamplace/components";
9
9
import { useDanmuSettings } from "@streamplace/components/src/streamplace-store";
10
10
import { useTranslation } from "react-i18next";
11
11
-
import {
12
12
-
Platform,
13
13
-
ScrollView,
14
14
-
Switch,
15
15
-
useWindowDimensions,
16
16
-
} from "react-native";
11
11
+
import { Platform, ScrollView, useWindowDimensions } from "react-native";
12
12
+
import { SettingToggle } from "./components/setting-toggle";
17
13
18
14
export function DanmuCategorySettings() {
19
15
const { t } = useTranslation("settings");
···
44
40
>
45
41
<View style={[{ alignItems: "stretch" }, zero.gap.all[4]]}>
46
42
{/* Enable/Disable Danmu */}
47
47
-
<View
48
48
-
style={[
49
49
-
{ flexDirection: "row" },
50
50
-
{ alignItems: "flex-start" },
51
51
-
{ justifyContent: "flex-start" },
52
52
-
]}
53
53
-
>
54
54
-
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
55
55
-
<Text size="xl">{t("danmu-enabled")}</Text>
56
56
-
<Text size="lg" color="muted">
57
57
-
{t("danmu-enabled-description")}
58
58
-
</Text>
59
59
-
</View>
60
60
-
<Switch value={danmuEnabled} onValueChange={setDanmuEnabled} />
61
61
-
</View>
43
43
+
<SettingToggle
44
44
+
title={t("danmu-enabled")}
45
45
+
description={t("danmu-enabled-description")}
46
46
+
value={danmuEnabled}
47
47
+
onValueChange={setDanmuEnabled}
48
48
+
/>
62
49
63
50
{/* Opacity */}
64
51
<View style={[zero.gap.all[6]]}>
+15
-29
js/app/components/settings/privacy-category-settings.tsx
···
1
1
-
import { Text, View, zero } from "@streamplace/components";
1
1
+
import { View, zero } from "@streamplace/components";
2
2
import { useEffect } from "react";
3
3
import { useTranslation } from "react-i18next";
4
4
-
import { ScrollView, Switch } from "react-native";
4
4
+
import { ScrollView } from "react-native";
5
5
import { useStore } from "store";
6
6
import { useIsReady, useServerSettings, useStreamplaceUrl } from "store/hooks";
7
7
+
import { SettingToggle } from "./components/setting-toggle";
7
8
8
9
export function PrivacyCategorySettings() {
9
10
const { t } = useTranslation("settings");
···
35
36
{ paddingVertical: 24, maxWidth: 500, width: "100%" },
36
37
]}
37
38
>
38
38
-
<View
39
39
-
style={[
40
40
-
zero.layout.flex.row,
41
41
-
zero.layout.flex.align.center,
42
42
-
zero.layout.flex.justify.between,
43
43
-
{ width: "100%" },
44
44
-
]}
45
45
-
>
46
46
-
<View style={[zero.flex.values[1], { paddingRight: 12 }]}>
47
47
-
<Text size="xl">
48
48
-
{t("debug-recording-title", { host: u.host })}
49
49
-
</Text>
50
50
-
<Text size="lg" color="muted">
51
51
-
{t("debug-recording-description")}
52
52
-
</Text>
53
53
-
</View>
54
54
-
<Switch
55
55
-
value={debugRecordingOn}
56
56
-
onValueChange={(value) => {
57
57
-
if (value === true) {
58
58
-
createServerSettingsRecord(true);
59
59
-
} else {
60
60
-
createServerSettingsRecord(false);
61
61
-
}
62
62
-
}}
63
63
-
/>
64
64
-
</View>
39
39
+
<SettingToggle
40
40
+
title={t("debug-recording-title", { host: u.host })}
41
41
+
description={t("debug-recording-description")}
42
42
+
value={debugRecordingOn}
43
43
+
onValueChange={(value) => {
44
44
+
if (value === true) {
45
45
+
createServerSettingsRecord(true);
46
46
+
} else {
47
47
+
createServerSettingsRecord(false);
48
48
+
}
49
49
+
}}
50
50
+
/>
65
51
</View>
66
52
</View>
67
53
</ScrollView>
-55
js/app/components/settings/settings-item-link.tsx
···
1
1
-
import { useNavigation } from "@react-navigation/native";
2
2
-
import { Text, View } from "@streamplace/components";
3
3
-
import { ChevronRight, LucideIcon } from "lucide-react-native";
4
4
-
import { Pressable } from "react-native";
5
5
-
6
6
-
interface SettingsItemLinkProps {
7
7
-
title: string;
8
8
-
screen: string;
9
9
-
icon: LucideIcon;
10
10
-
rootScreen?: boolean; // if true, navigates to root stack instead of Settings stack
11
11
-
}
12
12
-
13
13
-
export function SettingsItemLink({
14
14
-
title,
15
15
-
screen,
16
16
-
icon: Icon,
17
17
-
rootScreen = false,
18
18
-
}: SettingsItemLinkProps) {
19
19
-
const navigation = useNavigation();
20
20
-
21
21
-
const handlePress = () => {
22
22
-
if (rootScreen) {
23
23
-
// Navigate to root stack screen
24
24
-
navigation.navigate(screen as never);
25
25
-
} else {
26
26
-
// Navigate within Settings stack
27
27
-
navigation.navigate(screen as never);
28
28
-
}
29
29
-
};
30
30
-
31
31
-
return (
32
32
-
<Pressable onPress={handlePress}>
33
33
-
{({ pressed }) => (
34
34
-
<View
35
35
-
style={[
36
36
-
{
37
37
-
flexDirection: "row",
38
38
-
alignItems: "center",
39
39
-
justifyContent: "space-between",
40
40
-
paddingVertical: 12,
41
41
-
paddingHorizontal: 16,
42
42
-
backgroundColor: pressed ? "#ffffff08" : "transparent",
43
43
-
},
44
44
-
]}
45
45
-
>
46
46
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
47
47
-
<Icon size={20} color="#999" />
48
48
-
<Text size="lg">{title}</Text>
49
49
-
</View>
50
50
-
<ChevronRight size={20} color="#666" />
51
51
-
</View>
52
52
-
)}
53
53
-
</Pressable>
54
54
-
);
55
55
-
}
js/app/components/settings/settings-navigation-item.tsx
js/app/components/settings/components/settings-navigation-item.tsx
+1
-1
js/app/components/settings/settings.tsx
···
6
6
zero,
7
7
} from "@streamplace/components";
8
8
import AQLink from "components/aqlink";
9
9
-
import { SettingsNavigationItem } from "components/settings/settings-navigation-item";
9
9
+
import { SettingsNavigationItem } from "components/settings/components/settings-navigation-item";
10
10
import { Code, Info, Lock, LogIn, Shield, Video } from "lucide-react-native";
11
11
import { ImageBackground, Pressable, ScrollView } from "react-native";
12
12
+3
-4
js/app/components/settings/streaming-category-settings.tsx
···
1
1
import { View, zero } from "@streamplace/components";
2
2
-
import { SettingsItemLink } from "components/settings/settings-item-link";
3
2
import { Key, Webhook } from "lucide-react-native";
4
3
import { useTranslation } from "react-i18next";
5
4
import { ScrollView } from "react-native";
5
5
+
import { SettingsNavigationItem } from "./components/settings-navigation-item";
6
6
import { HorizontalBar } from "./settings";
7
7
8
8
export function StreamingCategorySettings() {
···
11
11
<ScrollView>
12
12
<View style={[zero.layout.flex.align.center, zero.px[2], zero.py[2]]}>
13
13
<View style={[{ paddingVertical: 0, maxWidth: 500, width: "100%" }]}>
14
14
-
<SettingsItemLink
14
14
+
<SettingsNavigationItem
15
15
title={t("key-management")}
16
16
screen="KeyManagement"
17
17
icon={Key}
18
18
-
rootScreen
19
18
/>
20
19
<HorizontalBar />
21
21
-
<SettingsItemLink
20
20
+
<SettingsNavigationItem
22
21
title={t("webhooks")}
23
22
screen="WebhooksSettings"
24
23
icon={Webhook}
+54
-49
js/app/components/settings/updates.native.tsx
···
10
10
import * as ExpoUpdates from "expo-updates";
11
11
import { useEffect, useState } from "react";
12
12
import { useTranslation } from "react-i18next";
13
13
-
import { Platform, TouchableOpacity, View } from "react-native";
13
13
+
import { Platform, Pressable, TouchableOpacity, View } from "react-native";
14
14
import pkg from "../../package.json";
15
15
16
16
const UNLOCK_TAP_COUNT = 5;
···
85
85
<Text size="2xl" style={[{ fontWeight: "bold", color: "#fff" }]}>
86
86
Streamplace v{version}
87
87
</Text>
88
88
-
<View
88
88
+
<Pressable
89
89
+
onPress={handleVersionPress}
89
90
style={[
90
91
{ alignSelf: "flex-start" },
91
92
theme.zero.bg.muted,
···
97
98
<Text size="base" center>
98
99
{runTypeMessage}
99
100
</Text>
100
100
-
</View>
101
101
+
</Pressable>
101
102
</View>
102
102
-
<Button
103
103
-
onPress={async () => {
104
104
-
try {
105
105
-
setChecked(true);
106
106
-
const res = await ExpoUpdates.checkForUpdateAsync();
107
107
-
if (!res.isAvailable) {
103
103
+
<View>
104
104
+
<Button
105
105
+
variant="secondary"
106
106
+
width="full"
107
107
+
onPress={async () => {
108
108
+
try {
109
109
+
setChecked(true);
110
110
+
const res = await ExpoUpdates.checkForUpdateAsync();
111
111
+
if (!res.isAvailable) {
112
112
+
toast.show(
113
113
+
t("modal-latest-version"),
114
114
+
t("modal-no-update-available"),
115
115
+
{ duration: 2000 },
116
116
+
);
117
117
+
} else {
118
118
+
toast.show(
119
119
+
t("modal-update-available-title"),
120
120
+
t("modal-update-available-description"),
121
121
+
{ duration: 2000 },
122
122
+
);
123
123
+
}
124
124
+
} catch (e) {
108
125
toast.show(
109
109
-
t("modal-latest-version"),
110
110
-
t("modal-no-update-available"),
111
111
-
{ duration: 2000 },
112
112
-
);
113
113
-
} else {
114
114
-
toast.show(
115
115
-
t("modal-update-available-title"),
116
116
-
t("modal-update-available-description"),
117
117
-
{ duration: 2000 },
126
126
+
t("modal-update-failed-title"),
127
127
+
t("modal-update-failed-description", {
128
128
+
store: Platform.OS === "ios" ? "App Store" : "Play Store",
129
129
+
}),
118
130
);
119
131
}
120
120
-
} catch (e) {
121
121
-
toast.show(
122
122
-
t("modal-update-failed-title"),
123
123
-
t("modal-update-failed-description", {
124
124
-
store: Platform.OS === "ios" ? "App Store" : "Play Store",
125
125
-
}),
126
126
-
);
127
127
-
}
128
128
-
}}
129
129
-
>
130
130
-
<Text style={[{ color: "#fff", fontWeight: "600" }]}>{buttonText}</Text>
131
131
-
</Button>
132
132
-
{isUpdatePending && (
133
133
-
<TouchableOpacity
134
134
-
style={[
135
135
-
{
136
136
-
marginTop: 8,
137
137
-
backgroundColor: "#007AFF",
138
138
-
borderRadius: 8,
139
139
-
paddingHorizontal: 16,
140
140
-
paddingVertical: 12,
141
141
-
alignItems: "center",
142
142
-
},
143
143
-
]}
144
144
-
onPress={() => {
145
145
-
ExpoUpdates.reloadAsync();
146
132
}}
147
133
>
148
148
-
<Text style={[{ color: "#fff", fontWeight: "600" }]}>
149
149
-
{t("button-reload-app-on-update")}
150
150
-
</Text>
151
151
-
</TouchableOpacity>
152
152
-
)}
134
134
+
{buttonText}
135
135
+
</Button>
136
136
+
{isUpdatePending && (
137
137
+
<TouchableOpacity
138
138
+
style={[
139
139
+
{
140
140
+
marginTop: 8,
141
141
+
backgroundColor: "#007AFF",
142
142
+
borderRadius: 8,
143
143
+
paddingHorizontal: 16,
144
144
+
paddingVertical: 12,
145
145
+
alignItems: "center",
146
146
+
},
147
147
+
]}
148
148
+
onPress={() => {
149
149
+
ExpoUpdates.reloadAsync();
150
150
+
}}
151
151
+
>
152
152
+
<Text style={[{ color: "#fff", fontWeight: "600" }]}>
153
153
+
{t("button-reload-app-on-update")}
154
154
+
</Text>
155
155
+
</TouchableOpacity>
156
156
+
)}
157
157
+
</View>
153
158
</View>
154
159
);
155
160
}
+48
-4
js/app/components/settings/updates.tsx
···
1
1
-
import { Text } from "@streamplace/components";
1
1
+
import {
2
2
+
Text,
3
3
+
useDanmuUnlocked,
4
4
+
useSetDanmuUnlocked,
5
5
+
useToast,
6
6
+
} from "@streamplace/components";
7
7
+
import { useState } from "react";
2
8
import { useTranslation } from "react-i18next";
3
3
-
import { View } from "react-native";
9
9
+
import { Pressable } from "react-native-gesture-handler";
4
10
import pkg from "../../package.json";
5
11
12
12
+
const UNLOCK_TAP_COUNT = 5;
13
13
+
6
14
// maybe someday some PWA update stuff will live here
7
15
export function Updates() {
8
16
const { t } = useTranslation("settings");
17
17
+
const toast = useToast();
18
18
+
const [checked, setChecked] = useState(false);
19
19
+
const [tapCount, setTapCount] = useState(0);
20
20
+
const danmuUnlocked = useDanmuUnlocked();
21
21
+
const setDanmuUnlocked = useSetDanmuUnlocked();
22
22
+
23
23
+
const handleVersionPress = () => {
24
24
+
if (danmuUnlocked) {
25
25
+
toast.show("You are already a developer", undefined, {
26
26
+
duration: 2,
27
27
+
variant: "info",
28
28
+
actionLabel: "Stop being a developer",
29
29
+
onAction: () => {
30
30
+
setDanmuUnlocked(false);
31
31
+
toast.show("You are no longer a developer", undefined, {
32
32
+
duration: 2,
33
33
+
variant: "info",
34
34
+
});
35
35
+
},
36
36
+
});
37
37
+
return;
38
38
+
}
39
39
+
40
40
+
const newCount = tapCount + 1;
41
41
+
setTapCount(newCount);
42
42
+
43
43
+
if (newCount >= UNLOCK_TAP_COUNT) {
44
44
+
setDanmuUnlocked(true);
45
45
+
toast.show("You are now a developer", "have fun! lol", {
46
46
+
duration: 20,
47
47
+
variant: "success",
48
48
+
});
49
49
+
setTapCount(0);
50
50
+
}
51
51
+
};
52
52
+
9
53
return (
10
10
-
<View>
54
54
+
<Pressable onPress={handleVersionPress}>
11
55
<Text size="xl">{t("app-version", { version: pkg.version })}</Text>
12
12
-
</View>
56
56
+
</Pressable>
13
57
);
14
58
}
+11
-2
js/app/components/settings/webhook-manager.tsx
···
508
508
<Text style={[text.gray[300], { fontSize: 14, fontWeight: "500" }]}>
509
509
Text Replacements
510
510
</Text>
511
511
-
<Button size="pill" onPress={addReplacement}>
511
511
+
<Button width="min" size="pill" onPress={addReplacement}>
512
512
<Text style={[text.white, { fontSize: 12 }]}>+ Add</Text>
513
513
</Button>
514
514
</View>
···
830
830
</Text>
831
831
832
832
<View
833
833
-
style={[layout.flex.row, layout.flex.justify.between, gap.all[3]]}
833
833
+
style={[
834
834
+
layout.flex.row,
835
835
+
layout.flex.justify.between,
836
836
+
gap.all[3],
837
837
+
w.percent[100],
838
838
+
]}
834
839
>
835
840
<Button
836
841
onPress={handleCreate}
837
842
size="pill"
843
843
+
width="min"
838
844
leftIcon={<Plus color={theme.colors.text} />}
839
845
>
840
846
<Text>{t("create-webhook")}</Text>
···
845
851
disabled={loading}
846
852
leftIcon={<RefreshCw color={theme.colors.text} />}
847
853
size="pill"
854
854
+
width="min"
848
855
variant="secondary"
849
856
>
850
857
<Text>{t("refresh")}</Text>
···
938
945
<View style={[layout.flex.row, layout.flex.justify.end, gap.all[3]]}>
939
946
<Button
940
947
variant="secondary"
948
948
+
width="full"
941
949
onPress={() => setDeleteDialog({ isVisible: false, webhook: null })}
942
950
disabled={
943
951
deleteDialog.webhook
···
949
957
</Button>
950
958
<Button
951
959
variant="destructive"
960
960
+
width="full"
952
961
onPress={confirmDelete}
953
962
disabled={
954
963
deleteDialog.webhook
+1
-1
js/app/package.json
···
10
10
"web": "npx expo start --web --port 38081",
11
11
"test": "jest --watchAll",
12
12
"build": "pnpm run build:web && pnpm run prebuild",
13
13
-
"build:web": "pnpm run export && node exportClientExpoConfig.js > dist/expoConfig.json",
13
13
+
"build:web": "node scripts/generate-build-info.js && pnpm run export && node exportClientExpoConfig.js > dist/expoConfig.json",
14
14
"export": "expo export --dump-sourcemap || expo export --dump-sourcemap",
15
15
"check": "bash -c 'set -euo pipefail;export OUT=$(mktemp -d); npx tsc -p . --outDir $OUT; rm -rf $OUT'",
16
16
"prebuild": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean && sed -i.bak 's/org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m/org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m/' android/gradle.properties && pnpm run find-node",
+48
js/app/scripts/generate-build-info.js
···
1
1
+
#!/usr/bin/env node
2
2
+
3
3
+
const { execSync } = require("child_process");
4
4
+
const fs = require("fs");
5
5
+
const path = require("path");
6
6
+
7
7
+
function getGitInfo() {
8
8
+
try {
9
9
+
const hash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
10
10
+
const shortHash = execSync("git rev-parse --short HEAD", {
11
11
+
encoding: "utf-8",
12
12
+
}).trim();
13
13
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
14
14
+
encoding: "utf-8",
15
15
+
}).trim();
16
16
+
const tag = execSync("git describe --tags --always --dirty", {
17
17
+
encoding: "utf-8",
18
18
+
}).trim();
19
19
+
const isDirty = tag.endsWith("-dirty");
20
20
+
21
21
+
return {
22
22
+
hash,
23
23
+
shortHash,
24
24
+
branch,
25
25
+
tag,
26
26
+
isDirty,
27
27
+
};
28
28
+
} catch (error) {
29
29
+
console.warn("Could not get git info:", error.message);
30
30
+
return {
31
31
+
hash: "unknown",
32
32
+
shortHash: "unknown",
33
33
+
branch: "unknown",
34
34
+
tag: "unknown",
35
35
+
isDirty: false,
36
36
+
};
37
37
+
}
38
38
+
}
39
39
+
40
40
+
const buildInfo = {
41
41
+
...getGitInfo(),
42
42
+
buildTime: new Date().toISOString(),
43
43
+
};
44
44
+
45
45
+
const outputPath = path.join(__dirname, "..", "src", "build-info.json");
46
46
+
fs.writeFileSync(outputPath, JSON.stringify(buildInfo, null, 2));
47
47
+
48
48
+
console.log("Generated build-info.json:", buildInfo);
+16
-6
js/app/src/router.tsx
···
235
235
]}
236
236
>
237
237
{sidebar?.isActive ? (
238
238
-
<Pressable style={{ padding: 5 }} onPress={handlePress}>
239
239
-
{sidebar.isCollapsed ? (
240
240
-
<PanelLeftOpen size={24} color={theme.colors.accentForeground} />
241
241
-
) : (
242
242
-
<PanelLeftClose size={24} color={theme.colors.accentForeground} />
238
238
+
<>
239
239
+
<Pressable style={{ padding: 5 }} onPress={handlePress}>
240
240
+
{sidebar.isCollapsed ? (
241
241
+
<PanelLeftOpen size={24} color={theme.colors.accentForeground} />
242
242
+
) : (
243
243
+
<PanelLeftClose size={24} color={theme.colors.accentForeground} />
244
244
+
)}
245
245
+
</Pressable>
246
246
+
{canGoBack && (
247
247
+
<Pressable
248
248
+
style={{ marginLeft: 10, paddingVertical: 5 }}
249
249
+
onPress={handleGoBackPress}
250
250
+
>
251
251
+
<ArrowLeft size={24} color={theme.colors.accentForeground} />
252
252
+
</Pressable>
243
253
)}
244
244
-
</Pressable>
254
254
+
</>
245
255
) : (
246
256
<Pressable style={{ padding: 5 }} onPress={handleGoBackPress}>
247
257
{canGoBack ? (
+13
-1
js/components/src/components/ui/button.tsx
···
39
39
rightIcon?: React.ReactNode;
40
40
loading?: boolean;
41
41
loadingText?: string;
42
42
+
width?: "full" | "min" | number;
42
43
}
43
44
44
45
export const Button = forwardRef<any, ButtonProps>(
···
53
54
loadingText,
54
55
disabled,
55
56
style,
57
57
+
width = "full",
56
58
...props
57
59
},
58
60
ref,
···
198
200
}
199
201
}, [variant, icons]);
200
202
203
203
+
const widthStyle = useMemo(() => {
204
204
+
if (width === "full") {
205
205
+
return { width: "100%" };
206
206
+
} else if (width === "min") {
207
207
+
return { alignSelf: "flex-start" as const };
208
208
+
} else {
209
209
+
return { width };
210
210
+
}
211
211
+
}, [width]);
212
212
+
201
213
return (
202
214
<ButtonPrimitive.Root
203
215
ref={ref}
204
216
disabled={disabled || loading}
205
205
-
style={[buttonStyle, sizeStyles.button, style]}
217
217
+
style={[buttonStyle, sizeStyles.button, widthStyle, style]}
206
218
{...props}
207
219
>
208
220
<ButtonPrimitive.Content style={sizeStyles.inner}>