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