Browse Source
* Channel config rework Add staged channel config with tabbed UI, import/export workflow, and global form state refactor * Improve import dialog config comparison and UI labels * Review fixes * Improve state handling * Fix default filter behaviour --------- Co-authored-by: philon- <[email protected]>pull/824/head
committed by
GitHub
42 changed files with 598 additions and 457 deletions
@ -1,86 +0,0 @@ |
|||
import { Channel } from "@components/PageComponents/Channel.tsx"; |
|||
import { PageLayout } from "@components/PageLayout.tsx"; |
|||
import { Sidebar } from "@components/Sidebar.tsx"; |
|||
import { |
|||
Tabs, |
|||
TabsContent, |
|||
TabsList, |
|||
TabsTrigger, |
|||
} from "@components/UI/Tabs.tsx"; |
|||
import { useDevice } from "@core/stores"; |
|||
import type { Protobuf } from "@meshtastic/core"; |
|||
import { Types } from "@meshtastic/core"; |
|||
import i18next from "i18next"; |
|||
import { QrCodeIcon, UploadIcon } from "lucide-react"; |
|||
import { useState } from "react"; |
|||
import { useTranslation } from "react-i18next"; |
|||
|
|||
export const getChannelName = (channel: Protobuf.Channel.Channel) => { |
|||
return channel.settings?.name.length |
|||
? channel.settings?.name |
|||
: channel.index === 0 |
|||
? i18next.t("page.broadcastLabel") |
|||
: i18next.t("page.channelIndex", { |
|||
ns: "channels", |
|||
index: channel.index, |
|||
}); |
|||
}; |
|||
|
|||
const ChannelsPage = () => { |
|||
const { t } = useTranslation("channels"); |
|||
const { channels, setDialogOpen } = useDevice(); |
|||
const [activeChannel] = useState<Types.ChannelNumber>( |
|||
Types.ChannelNumber.Primary, |
|||
); |
|||
|
|||
const currentChannel = channels.get(activeChannel); |
|||
const allChannels = Array.from(channels.values()); |
|||
|
|||
return ( |
|||
<PageLayout |
|||
contentClassName="overflow-auto" |
|||
leftBar={<Sidebar />} |
|||
label={ |
|||
currentChannel |
|||
? getChannelName(currentChannel) |
|||
: t("loading", { ns: "common" }) |
|||
} |
|||
actions={[ |
|||
{ |
|||
key: "import", |
|||
icon: UploadIcon, |
|||
onClick() { |
|||
setDialogOpen("import", true); |
|||
}, |
|||
}, |
|||
{ |
|||
key: "qr", |
|||
icon: QrCodeIcon, |
|||
onClick() { |
|||
setDialogOpen("QR", true); |
|||
}, |
|||
}, |
|||
]} |
|||
> |
|||
<Tabs defaultValue="0"> |
|||
<TabsList className="dark:bg-slate-800"> |
|||
{allChannels.map((channel) => ( |
|||
<TabsTrigger |
|||
key={channel.index} |
|||
value={channel.index.toString()} |
|||
className="dark:text-white" |
|||
> |
|||
{getChannelName(channel)} |
|||
</TabsTrigger> |
|||
))} |
|||
</TabsList> |
|||
{allChannels.map((channel) => ( |
|||
<TabsContent key={channel.index} value={channel.index.toString()}> |
|||
<Channel key={channel.index} channel={channel} /> |
|||
</TabsContent> |
|||
))} |
|||
</Tabs> |
|||
</PageLayout> |
|||
); |
|||
}; |
|||
export default ChannelsPage; |
|||
@ -0,0 +1,95 @@ |
|||
import { Channel } from "@app/components/PageComponents/ChannelConfig/Channel"; |
|||
import { Button } from "@components/UI/Button.tsx"; |
|||
import { Spinner } from "@components/UI/Spinner.tsx"; |
|||
import { |
|||
Tabs, |
|||
TabsContent, |
|||
TabsList, |
|||
TabsTrigger, |
|||
} from "@components/UI/Tabs.tsx"; |
|||
import { useDevice } from "@core/stores"; |
|||
import type { Protobuf } from "@meshtastic/core"; |
|||
import i18next from "i18next"; |
|||
import { QrCodeIcon, UploadIcon } from "lucide-react"; |
|||
import { Suspense, useMemo } from "react"; |
|||
import type { UseFormReturn } from "react-hook-form"; |
|||
import { useTranslation } from "react-i18next"; |
|||
|
|||
interface ConfigProps { |
|||
onFormInit: <T extends object>(methods: UseFormReturn<T>) => void; |
|||
} |
|||
|
|||
export const getChannelName = (channel: Protobuf.Channel.Channel) => { |
|||
return channel.settings?.name.length |
|||
? channel.settings?.name |
|||
: channel.index === 0 |
|||
? i18next.t("page.broadcastLabel") |
|||
: i18next.t("page.channelIndex", { |
|||
ns: "channels", |
|||
index: channel.index, |
|||
}); |
|||
}; |
|||
|
|||
export const ChannelConfig = ({ onFormInit }: ConfigProps) => { |
|||
const { channels, getWorkingChannelConfig, setDialogOpen } = useDevice(); |
|||
const { t } = useTranslation("channels"); |
|||
|
|||
const allChannels = Array.from(channels.values()); |
|||
const flags = useMemo( |
|||
() => |
|||
new Map( |
|||
allChannels.map((channel) => [ |
|||
channel.index, |
|||
getWorkingChannelConfig(channel.index), |
|||
]), |
|||
), |
|||
[allChannels, getWorkingChannelConfig], |
|||
); |
|||
|
|||
return ( |
|||
<Tabs defaultValue="channel_0"> |
|||
<TabsList className="w-full dark:bg-slate-700"> |
|||
{allChannels.map((channel) => ( |
|||
<TabsTrigger |
|||
key={`channel_${channel.index}`} |
|||
value={`channel_${channel.index}`} |
|||
className="dark:text-white relative" |
|||
> |
|||
{getChannelName(channel)} |
|||
{flags.get(channel.index) && ( |
|||
<span className="absolute -top-0.5 -right-0.5 z-50 flex size-3"> |
|||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-500 opacity-25" /> |
|||
<span className="relative inline-flex size-3 rounded-full bg-sky-500" /> |
|||
</span> |
|||
)} |
|||
</TabsTrigger> |
|||
))} |
|||
<Button |
|||
className="ml-auto mr-1 h-8" |
|||
onClick={() => setDialogOpen("import", true)} |
|||
> |
|||
<UploadIcon className="mr-2" size={14} /> |
|||
{t("page.import")} |
|||
</Button> |
|||
<Button className=" h-8" onClick={() => setDialogOpen("QR", true)}> |
|||
<QrCodeIcon className="mr-2" size={14} /> |
|||
{t("page.export")} |
|||
</Button> |
|||
</TabsList> |
|||
{allChannels.map((channel) => ( |
|||
<TabsContent |
|||
key={`channel_${channel.index}`} |
|||
value={`channel_${channel.index}`} |
|||
> |
|||
<Suspense fallback={<Spinner size="lg" className="my-5" />}> |
|||
<Channel |
|||
key={channel.index} |
|||
onFormInit={onFormInit} |
|||
channel={channel} |
|||
/> |
|||
</Suspense> |
|||
</TabsContent> |
|||
))} |
|||
</Tabs> |
|||
); |
|||
}; |
|||
Loading…
Reference in new issue