Skip to content

Commit 2d13a6c

Browse files
authored
Merge pull request #677 from gnmyt/features/multiple-providers
🗃️ Support für LibreSpeed & Cloudflare Speed
2 parents f00f7e8 + fbb891b commit 2d13a6c

File tree

30 files changed

+820
-184
lines changed

30 files changed

+820
-184
lines changed

client/public/assets/locales/en.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@
1515
"wrong": "The password you entered is incorrect",
1616
"unlock": "Unlock"
1717
},
18-
"accept": {
19-
"title": "We need your permission",
20-
"description": "We use services from Ookla. By clicking <Bold>Accept</Bold>, you acknowledge that you have read and agree to Ookla's <EULA>EULA</EULA>, <Privacy>Privacy Statement</Privacy> and <Terms>Terms of Use</Terms>.",
21-
"button": "Accept"
22-
},
2318
"api": {
2419
"title": "API not reachable",
2520
"description": "MySpeed could not reach the API of this instance. Please try again later."
21+
},
22+
"provider": {
23+
"server": "Server",
24+
"server_id": "Server ID",
25+
"choose_automatically": "Choose automatically",
26+
"ookla_license": "I have read and accept the <Eula>EULA</Eula>, <GDPR>privacy policy</GDPR> and <TOS>terms of service</TOS> of Ookla.",
27+
"cloudflare_note": "Cloudflare does not require any additional settings"
2628
}
2729
},
2830
"dropdown": {
@@ -35,7 +37,7 @@
3537
"upload": "Optimal up-speed",
3638
"download": "Optimal down-speed",
3739
"recommendations": "Recommendations",
38-
"server": "Change Server",
40+
"change_provider": "Change provider",
3941
"password": "Change password",
4042
"cron": "Set frequency",
4143
"time": "Set period",
@@ -80,10 +82,8 @@
8082
"download_placeholder": "Down speed (Mbps)",
8183
"recommendations_title": "Optimal recommendations",
8284
"recommendations_set": "Set automatic recommendations?",
83-
"server_title": "Set speedtest server",
85+
"provider_title": "Set speedtest provider",
8486
"manually": "Set manually",
85-
"manual_server_title": "Set speedtest server",
86-
"manual_server_id": "Server ID",
8787
"new_password": "Set a new password",
8888
"password_placeholder": "New password",
8989
"password_removed": "The password lock has been removed and the set password has been removed.",

client/src/common/components/Dropdown/DropdownComponent.jsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ import {
1616
faPause,
1717
faPingPongPaddleBall,
1818
faPlay,
19-
faServer,
2019
faWandMagicSparkles,
2120
faCheck,
22-
faExclamationTriangle
21+
faExclamationTriangle, faSliders
2322
} from "@fortawesome/free-solid-svg-icons";
2423
import {ConfigContext} from "@/common/contexts/Config";
2524
import {StatusContext} from "@/common/contexts/Status";
@@ -36,6 +35,7 @@ import {ToastNotificationContext} from "@/common/contexts/ToastNotification";
3635
import {NodeContext} from "@/common/contexts/Node";
3736
import {IntegrationDialog} from "@/common/components/IntegrationDialog";
3837
import LanguageDialog from "@/common/components/LanguageDialog";
38+
import ProviderDialog from "@/common/components/ProviderDialog";
3939

4040
let icon;
4141

@@ -65,6 +65,7 @@ function DropdownComponent() {
6565
const [showViewDialog, setShowViewDialog] = useState(false);
6666
const [showIntegrationDialog, setShowIntegrationDialog] = useState(false);
6767
const [showLanguageDialog, setShowLanguageDialog] = useState(false);
68+
const [showProviderDialog, setShowProviderDialog] = useState(false);
6869
const ref = useRef();
6970

7071
useEffect(() => {
@@ -130,19 +131,6 @@ function DropdownComponent() {
130131
} else setDialog({title: t("update.recommendations_title"), description: t("info.recommendations_error"), buttonText: t("dialog.okay")});
131132
}
132133

133-
const updateServer = () => patchDialog("serverId", async (value) => ({
134-
title: t("update.server_title"),
135-
select: true,
136-
selectOptions: await jsonRequest("/info/server"),
137-
unsetButton: t("update.manually"),
138-
onClear: updateServerManually,
139-
value
140-
}));
141-
142-
const updateServerManually = () => patchDialog("serverId", (value) => ({
143-
title: t("update.manual_server_title"), placeholder: t("update.manual_server_id"), type: "number", value: value,
144-
}));
145-
146134
const updatePassword = async () => {
147135
const passwordSet = currentNode !== 0 ? findNode(currentNode).password : localStorage.getItem("password") != null;
148136

@@ -239,7 +227,7 @@ function DropdownComponent() {
239227
{run: updateDownload, icon: faArrowDown, text: t("dropdown.download")},
240228
{run: recommendedSettings, icon: faWandMagicSparkles, text: t("dropdown.recommendations")},
241229
{hr: true, key: 1},
242-
{run: updateServer, icon: faServer, text: t("dropdown.server")},
230+
{run: () => setShowProviderDialog(true), icon: faSliders, text: t("dropdown.change_provider")},
243231
{run: updatePassword, icon: faKey, text: t("dropdown.password"), previewHidden: true},
244232
{run: updateCron, icon: faClock, text: t("dropdown.cron")},
245233
{run: exportDialog, icon: faFileExport, text: t("dropdown.export")},
@@ -258,6 +246,7 @@ function DropdownComponent() {
258246
{showViewDialog && <ViewDialog onClose={() => setShowViewDialog(false)}/>}
259247
{showIntegrationDialog && <IntegrationDialog onClose={() => setShowIntegrationDialog(false)}/>}
260248
{showLanguageDialog && <LanguageDialog onClose={() => setShowLanguageDialog(false)}/>}
249+
{showProviderDialog && <ProviderDialog onClose={() => setShowProviderDialog(false)}/>}
261250
<div className="dropdown dropdown-invisible" id="dropdown" ref={ref}>
262251
<div className="dropdown-content">
263252
<h2>{t("dropdown.settings")}</h2>

client/src/common/components/LanguageDialog/styles.sass

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
border-radius: 0.5rem
2323

2424
&:hover
25-
background-color: $light-gray
25+
background-color: $darker-gray
2626

2727
img
2828
width: 2rem
@@ -38,6 +38,9 @@
3838
background-color: $light-gray
3939
color: $white
4040

41+
&:hover
42+
background-color: $light-gray
43+
4144
@media screen and (max-height: 425px)
4245
.language-chooser-dialog
4346
height: 15rem
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {DialogContext, DialogProvider} from "@/common/contexts/Dialog";
2+
import {t} from "i18next";
3+
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
4+
import {faClose} from "@fortawesome/free-solid-svg-icons";
5+
import "./styles.sass";
6+
import React, {useContext, useEffect, useState} from "react";
7+
import OoklaImage from "./assets/img/ookla.webp";
8+
import LibreImage from "./assets/img/libre.webp";
9+
import CloudflareImage from "./assets/img/cloudflare.webp";
10+
import {jsonRequest, patchRequest} from "@/common/utils/RequestUtil";
11+
import {Trans} from "react-i18next";
12+
import {ConfigContext} from "@/common/contexts/Config";
13+
14+
const providers = [
15+
{id: "ookla", name: "Ookla", image: OoklaImage},
16+
{id: "libre", name: "LibreSpeed", image: LibreImage},
17+
{id: "cloudflare", name: "Cloudflare", image: CloudflareImage}
18+
]
19+
20+
21+
export const Dialog = () => {
22+
const close = useContext(DialogContext);
23+
const [config, reloadConfig] = useContext(ConfigContext);
24+
const [provider, setProvider] = useState(config.provider || "ookla");
25+
26+
const [licenseAccepted, setLicenseAccepted] = useState(false);
27+
const [licenseError, setLicenseError] = useState(false);
28+
29+
const [ooklaServers, setOoklaServers] = useState({});
30+
const [libreServers, setLibreServers] = useState({});
31+
32+
const [serverId, setServerId] = useState("none");
33+
34+
useEffect(() => {
35+
jsonRequest("/info/server/ookla").then((response) => {
36+
setOoklaServers(response);
37+
});
38+
jsonRequest("/info/server/libre").then((response) => {
39+
setLibreServers(response);
40+
});
41+
}, []);
42+
43+
useEffect(() => {
44+
if (config[provider + "Id"]) setServerId(config[provider + "Id"]);
45+
}, [provider]);
46+
47+
useEffect(() => {
48+
if (serverId === "") setServerId("none");
49+
}, [serverId]);
50+
51+
const update = async () => {
52+
if (provider === "ookla" && !licenseAccepted) {
53+
setLicenseError(true);
54+
return;
55+
}
56+
57+
await patchRequest("/config/provider", {value: provider});
58+
59+
if (serverId !== config[provider + "Id"] && provider !== "cloudflare") {
60+
await patchRequest("/config/" + provider + "Id", {value: serverId});
61+
}
62+
63+
reloadConfig();
64+
65+
close();
66+
}
67+
68+
return (
69+
<>
70+
<div className="dialog-header">
71+
<h4 className="dialog-text">{t("update.provider_title")}</h4>
72+
<FontAwesomeIcon icon={faClose} className="dialog-text dialog-icon" onClick={() => close()}/>
73+
</div>
74+
<div className="provider-dialog-content">
75+
<div className="provider-header">
76+
{providers.map((current, index) => (
77+
<div className={`provider-item ${current.id === provider ? "provider-item-active" : ""}`}
78+
key={index} onClick={() => setProvider(current.id)}>
79+
<img src={current.image} alt={current.name}/>
80+
<h3>{current.name}</h3>
81+
</div>
82+
))}
83+
</div>
84+
{provider !== "cloudflare" && <div className="provider-content">
85+
<div className="provider-setting">
86+
<h3>{t("dialog.provider.server")}</h3>
87+
<select className="dialog-input provider-input" value={serverId}
88+
onChange={(e) => setServerId(e.target.value)}>
89+
<option value="none">{t("dialog.provider.choose_automatically")}</option>
90+
{provider === "ookla" && Object.keys(ooklaServers).map((current, index) => (
91+
<option key={index} value={current}>{ooklaServers[current]}</option>
92+
))}
93+
{provider === "libre" && Object.keys(libreServers).map((current, index) => (
94+
<option key={index} value={current}>{libreServers[current]}</option>
95+
))}
96+
</select>
97+
</div>
98+
<div className="provider-setting">
99+
<h3>{t("dialog.provider.server_id")}</h3>
100+
<input type="text" className="dialog-input provider-input" value={serverId === "none" ? "" : serverId}
101+
onChange={(e) => setServerId(e.target.value)}/>
102+
</div>
103+
</div>}
104+
{provider === "cloudflare" && <div className="provider-content">
105+
<p className="cloudflare-provider-info">{t("dialog.provider.cloudflare_note")}</p>
106+
</div>}
107+
</div>
108+
<div className="provider-dialog-footer">
109+
<div className="provider-license-box">
110+
{provider === "ookla" && <>
111+
<input type="checkbox" className={licenseError ? "cb-error" : ""} id="license" name="license"
112+
onChange={(e) => setLicenseAccepted(e.target.checked)}/>
113+
<label htmlFor="license"
114+
><Trans components={{
115+
Eula: <a href="https://www.speedtest.net/about/eula" target="_blank"
116+
rel="noreferrer" />,
117+
GDPR: <a href="https://www.speedtest.net/about/privacy" target="_blank"
118+
rel="noreferrer" />,
119+
TOS: <a href="https://www.speedtest.net/about/terms" target="_blank"
120+
rel="noreferrer" />}}>dialog.provider.ookla_license</Trans></label>
121+
</>}
122+
</div>
123+
124+
<button className="dialog-btn" onClick={update}>{t("dialog.update")}</button>
125+
</div>
126+
</>
127+
)
128+
}
129+
130+
export const ProviderDialog = (props) => {
131+
return (
132+
<>
133+
<DialogProvider close={props.onClose}>
134+
<Dialog/>
135+
</DialogProvider>
136+
</>
137+
)
138+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {ProviderDialog as default} from "./ProviderDialog";
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
@import "@/common/styles/colors"
2+
3+
.provider-dialog-content
4+
display: flex
5+
margin: 1rem 0.5rem
6+
user-select: none
7+
flex-direction: column
8+
9+
.provider-header
10+
display: flex
11+
gap: 1rem
12+
13+
.provider-item
14+
display: flex
15+
align-items: center
16+
padding: 0.3rem 0.5rem
17+
gap: 0.5rem
18+
border-radius: 0.8rem
19+
border: 2px solid $light-gray
20+
color: $darker-white
21+
cursor: pointer
22+
23+
img
24+
width: 2.5rem
25+
height: 2.5rem
26+
27+
h3
28+
margin: 0
29+
30+
&:hover
31+
background-color: $darker-gray
32+
33+
.provider-item-active
34+
background-color: $light-gray
35+
36+
&:hover
37+
background-color: $light-gray
38+
39+
.provider-content
40+
display: flex
41+
flex-direction: column
42+
margin-top: 1rem
43+
44+
45+
.provider-setting
46+
display: flex
47+
gap: 1rem
48+
align-items: center
49+
justify-content: space-between
50+
51+
.provider-input
52+
width: 20rem
53+
box-sizing: border-box
54+
margin-top: 0.5rem
55+
margin-bottom: 0.5rem
56+
font-size: 1.3rem
57+
58+
h3
59+
color: $darker-white
60+
61+
.cloudflare-provider-info
62+
color: $subtext
63+
text-align: center
64+
65+
.provider-dialog-footer
66+
display: flex
67+
align-items: center
68+
justify-content: space-between
69+
70+
.provider-license-box
71+
display: flex
72+
align-items: center
73+
gap: 0.5rem
74+
75+
input
76+
border: 2px solid $light-gray
77+
78+
.cb-error
79+
border-color: $red
80+
81+
label
82+
color: $subtext
83+
max-width: 16rem
84+
flex: 1
85+
86+
87+
@media screen and (max-width: 610px)
88+
.provider-dialog-content
89+
.provider-header
90+
flex-direction: column
91+
92+
@media screen and (max-width: 520px)
93+
.provider-dialog-content
94+
.provider-setting .provider-input
95+
width: 60%

client/src/common/contexts/Config/ConfigContext.jsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import React, {createContext, useContext, useEffect, useState} from "react";
22
import {InputDialogContext} from "../InputDialog";
33
import {request} from "@/common/utils/RequestUtil";
4-
import {acceptDialog, apiErrorDialog, passwordRequiredDialog} from "@/common/contexts/Config/dialog";
4+
import {apiErrorDialog, passwordRequiredDialog} from "@/common/contexts/Config/dialog";
55

66
export const ConfigContext = createContext({});
77

88
export const ConfigProvider = (props) => {
99
const [config, setConfig] = useState({});
1010
const [setDialog] = useContext(InputDialogContext);
11-
const [dialogShown, setDialogShown] = useState(false);
1211

1312
const reloadConfig = () => {
1413
request("/config").then(async res => {
@@ -32,13 +31,6 @@ export const ConfigProvider = (props) => {
3231

3332
const checkConfig = async () => (await request("/config")).json();
3433

35-
useEffect(() => {
36-
if (config.acceptOoklaLicense !== undefined && config.acceptOoklaLicense === "false" && !dialogShown) {
37-
setDialogShown(true);
38-
setDialog(acceptDialog());
39-
}
40-
}, [config]);
41-
4234
useEffect(reloadConfig, []);
4335

4436
return (

0 commit comments

Comments
 (0)