mirror of
https://github.com/p-stream/p-stream.git
synced 2026-05-11 21:40:54 +00:00
add manual region picker
This commit is contained in:
parent
8262e5ba12
commit
35a90264e6
4 changed files with 74 additions and 7 deletions
|
|
@ -13,9 +13,12 @@ interface DropdownProps {
|
||||||
selectedItem: OptionItem;
|
selectedItem: OptionItem;
|
||||||
setSelectedItem: (value: OptionItem) => void;
|
setSelectedItem: (value: OptionItem) => void;
|
||||||
options: Array<OptionItem>;
|
options: Array<OptionItem>;
|
||||||
|
direction?: "up" | "down";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dropdown(props: DropdownProps) {
|
export function Dropdown(props: DropdownProps) {
|
||||||
|
const { direction = "down" } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative my-4 max-w-[25rem]">
|
<div className="relative my-4 max-w-[25rem]">
|
||||||
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
|
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
|
||||||
|
|
@ -31,7 +34,7 @@ export function Dropdown(props: DropdownProps) {
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<Icon
|
<Icon
|
||||||
icon={Icons.UP_DOWN_ARROW}
|
icon={Icons.UP_DOWN_ARROW}
|
||||||
className="transform transition-transform text-xl text-dropdown-secondary"
|
className={`transform transition-transform text-xl text-dropdown-secondary ${direction === "up" ? "rotate-180" : ""}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
@ -41,7 +44,9 @@ export function Dropdown(props: DropdownProps) {
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="absolute left-0 right-0 top-10 z-[100] mt-4 max-h-60 overflow-auto rounded-md bg-dropdown-background py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-background-secondary scrollbar-thumb-type-secondary focus:outline-none sm:top-10">
|
<Listbox.Options
|
||||||
|
className={`absolute left-0 right-0 z-[100] mt-4 max-h-60 overflow-auto rounded-md bg-dropdown-background py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-background-secondary scrollbar-thumb-type-secondary focus:outline-none ${direction === "up" ? "bottom-full mb-4" : "top-full"}`}
|
||||||
|
>
|
||||||
{props.options.map((opt) => (
|
{props.options.map((opt) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Toggle } from "@/components/buttons/Toggle";
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||||
import { Stepper } from "@/components/layout/Stepper";
|
import { Stepper } from "@/components/layout/Stepper";
|
||||||
|
|
@ -36,6 +37,7 @@ import {
|
||||||
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { Region, useRegionStore } from "@/utils/detectRegion";
|
||||||
import { getProxyUrls } from "@/utils/proxyUrls";
|
import { getProxyUrls } from "@/utils/proxyUrls";
|
||||||
|
|
||||||
import { Status, testFebboxToken } from "../parts/settings/SetupPart";
|
import { Status, testFebboxToken } from "../parts/settings/SetupPart";
|
||||||
|
|
@ -53,6 +55,7 @@ export function FEDAPISetup() {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const febboxToken = useAuthStore((s) => s.febboxToken);
|
const febboxToken = useAuthStore((s) => s.febboxToken);
|
||||||
const setFebboxToken = useAuthStore((s) => s.setFebboxToken);
|
const setFebboxToken = useAuthStore((s) => s.setFebboxToken);
|
||||||
|
const { region, setRegion } = useRegionStore();
|
||||||
|
|
||||||
const [status, setStatus] = useState<Status>("unset");
|
const [status, setStatus] = useState<Status>("unset");
|
||||||
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
||||||
|
|
@ -61,6 +64,14 @@ export function FEDAPISetup() {
|
||||||
unset: "noresult",
|
unset: "noresult",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const regionOptions = [
|
||||||
|
{ id: "us-east", name: "US East" },
|
||||||
|
{ id: "us-west", name: "US West" },
|
||||||
|
{ id: "south-america", name: "South America" },
|
||||||
|
{ id: "asia", name: "Asia" },
|
||||||
|
{ id: "europe", name: "Europe" },
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkTokenStatus = async () => {
|
const checkTokenStatus = async () => {
|
||||||
const result = await getFebboxTokenStatus(febboxToken);
|
const result = await getFebboxTokenStatus(febboxToken);
|
||||||
|
|
@ -165,6 +176,21 @@ export function FEDAPISetup() {
|
||||||
passwordToggleable
|
passwordToggleable
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
/>
|
/>
|
||||||
|
<div className="ml-4 w-40">
|
||||||
|
<Dropdown
|
||||||
|
options={regionOptions}
|
||||||
|
selectedItem={{
|
||||||
|
id: region || "us-east",
|
||||||
|
name:
|
||||||
|
regionOptions.find((r) => r.id === region)?.name ||
|
||||||
|
"US East",
|
||||||
|
}}
|
||||||
|
setSelectedItem={(item) =>
|
||||||
|
setRegion(item.id as Region, true)
|
||||||
|
}
|
||||||
|
direction="up"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{status === "error" && (
|
{status === "error" && (
|
||||||
<p className="text-type-danger mt-4">
|
<p className="text-type-danger mt-4">
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Toggle } from "@/components/buttons/Toggle";
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||||
import {
|
import {
|
||||||
|
|
@ -26,6 +27,7 @@ import {
|
||||||
} from "@/pages/parts/settings/SetupPart";
|
} from "@/pages/parts/settings/SetupPart";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
import { Region, useRegionStore } from "@/utils/detectRegion";
|
||||||
|
|
||||||
interface ProxyEditProps {
|
interface ProxyEditProps {
|
||||||
proxyUrls: string[] | null;
|
proxyUrls: string[] | null;
|
||||||
|
|
@ -229,6 +231,7 @@ async function getFebboxTokenStatus(febboxToken: string | null) {
|
||||||
function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
|
function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showVideo, setShowVideo] = useState(false);
|
const [showVideo, setShowVideo] = useState(false);
|
||||||
|
const { region, setRegion } = useRegionStore();
|
||||||
|
|
||||||
const [status, setStatus] = useState<Status>("unset");
|
const [status, setStatus] = useState<Status>("unset");
|
||||||
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
const statusMap: Record<Status, StatusCircleProps["type"]> = {
|
||||||
|
|
@ -237,6 +240,14 @@ function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
|
||||||
unset: "noresult",
|
unset: "noresult",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const regionOptions = [
|
||||||
|
{ id: "us-east", name: "US East" },
|
||||||
|
{ id: "us-west", name: "US West" },
|
||||||
|
{ id: "south-america", name: "South America" },
|
||||||
|
{ id: "asia", name: "Asia" },
|
||||||
|
{ id: "europe", name: "Europe" },
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkTokenStatus = async () => {
|
const checkTokenStatus = async () => {
|
||||||
const result = await getFebboxTokenStatus(febboxToken);
|
const result = await getFebboxTokenStatus(febboxToken);
|
||||||
|
|
@ -337,6 +348,19 @@ function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
|
||||||
passwordToggleable
|
passwordToggleable
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
/>
|
/>
|
||||||
|
<div className="ml-4 w-40">
|
||||||
|
<Dropdown
|
||||||
|
options={regionOptions}
|
||||||
|
selectedItem={{
|
||||||
|
id: region || "us-east",
|
||||||
|
name:
|
||||||
|
regionOptions.find((r) => r.id === region)?.name ||
|
||||||
|
"US East",
|
||||||
|
}}
|
||||||
|
setSelectedItem={(item) => setRegion(item.id as Region, true)}
|
||||||
|
direction="up"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{status === "error" && (
|
{status === "error" && (
|
||||||
<p className="text-type-danger mt-4">
|
<p className="text-type-danger mt-4">
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,22 @@ export type Region =
|
||||||
interface RegionStore {
|
interface RegionStore {
|
||||||
region: Region | null;
|
region: Region | null;
|
||||||
lastChecked: number | null;
|
lastChecked: number | null;
|
||||||
setRegion: (region: Region) => void;
|
userPicked: boolean;
|
||||||
|
setRegion: (region: Region, userPicked?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000;
|
// const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000;
|
||||||
const THIRTY_MINUTES_MS = 30 * 60 * 1000;
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
// const THIRTY_MINUTES_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
export const useRegionStore = create<RegionStore>()(
|
export const useRegionStore = create<RegionStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
region: null,
|
region: null,
|
||||||
lastChecked: null,
|
lastChecked: null,
|
||||||
setRegion: (region) => set({ region, lastChecked: Date.now() }),
|
userPicked: false,
|
||||||
|
setRegion: (region, userPicked = false) =>
|
||||||
|
set({ region, lastChecked: Date.now(), userPicked }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "__MW::region",
|
name: "__MW::region",
|
||||||
|
|
@ -48,10 +52,16 @@ function determineRegion(data: {
|
||||||
export async function detectRegion(): Promise<Region> {
|
export async function detectRegion(): Promise<Region> {
|
||||||
const store = useRegionStore.getState();
|
const store = useRegionStore.getState();
|
||||||
|
|
||||||
|
// If user picked a region, always return that
|
||||||
|
if (store.userPicked && store.region) {
|
||||||
|
return store.region;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a recent detection, return that
|
||||||
if (
|
if (
|
||||||
store.region &&
|
store.region &&
|
||||||
store.lastChecked &&
|
store.lastChecked &&
|
||||||
Date.now() - store.lastChecked < THIRTY_MINUTES_MS
|
Date.now() - store.lastChecked < ONE_DAY_MS
|
||||||
) {
|
) {
|
||||||
return store.region;
|
return store.region;
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +71,9 @@ export async function detectRegion(): Promise<Region> {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const detectedRegion = determineRegion(data);
|
const detectedRegion = determineRegion(data);
|
||||||
store.setRegion(detectedRegion); // Persist the detected region
|
if (!store.userPicked) {
|
||||||
|
store.setRegion(detectedRegion); // Only update if not user picked
|
||||||
|
}
|
||||||
return detectedRegion;
|
return detectedRegion;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to detect region:", error);
|
console.warn("Failed to detect region:", error);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue