29 changed files with 9025 additions and 113 deletions
@ -0,0 +1,6 @@ |
|||
[submodule "design"] |
|||
path = design |
|||
url = https://github.com/meshtastic/meshtastic-design/ |
|||
[submodule "public/design"] |
|||
path = public/design |
|||
url = https://github.com/meshtastic/meshtastic-design/ |
|||
@ -0,0 +1,6 @@ |
|||
module.exports = { |
|||
plugins: { |
|||
tailwindcss: {}, |
|||
autoprefixer: {}, |
|||
}, |
|||
} |
|||
|
Before Width: | Height: | Size: 3.1 KiB |
@ -1,46 +0,0 @@ |
|||
.App { |
|||
text-align: center; |
|||
} |
|||
.App code { |
|||
background: #FFF3; |
|||
padding: 4px 8px; |
|||
border-radius: 4px; |
|||
} |
|||
.App p { |
|||
margin: 0.4rem; |
|||
} |
|||
|
|||
.App-logo { |
|||
height: 40vmin; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
@media (prefers-reduced-motion: no-preference) { |
|||
.App-logo { |
|||
animation: App-logo-spin infinite 20s linear; |
|||
} |
|||
} |
|||
|
|||
.App-header { |
|||
background-color: #282c34; |
|||
min-height: 100vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: calc(10px + 2vmin); |
|||
color: white; |
|||
} |
|||
|
|||
.App-link { |
|||
color: #61dafb; |
|||
} |
|||
|
|||
@keyframes App-logo-spin { |
|||
from { |
|||
transform: rotate(0deg); |
|||
} |
|||
to { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import { render } from '@testing-library/react'; |
|||
import { expect } from 'chai'; |
|||
import App from './App'; |
|||
|
|||
describe('<App>', () => { |
|||
it('renders learn react link', () => { |
|||
const { getByText } = render(<App />); |
|||
const linkElement = getByText(/learn react/i); |
|||
expect(document.body.contains(linkElement)); |
|||
}); |
|||
}); |
|||
@ -1,41 +1,206 @@ |
|||
import React, { useState, useEffect } from 'react'; |
|||
import logo from './logo.svg'; |
|||
import './App.css'; |
|||
import React, { useEffect, useState } from 'react'; |
|||
|
|||
interface AppProps {} |
|||
import { |
|||
Client, |
|||
IHTTPConnection, |
|||
Protobuf, |
|||
SettingsManager, |
|||
Types, |
|||
} from '@meshtastic/meshtasticjs'; |
|||
|
|||
import Header from './components/Header'; |
|||
import Main from './Main'; |
|||
import Translations_English from './translations/en'; |
|||
import Translations_Japanese from './translations/jp'; |
|||
|
|||
export enum LanguageEnum { |
|||
ENGLISH, |
|||
JAPANESE, |
|||
} |
|||
|
|||
export interface languageTemplate { |
|||
no_messages_message: string; |
|||
ui_settings_title: string; |
|||
nodes_title: string; |
|||
color_scheme_title: string; |
|||
language_title: string; |
|||
device_settings_title: string; |
|||
device_channels_title: string; |
|||
device_region_title: string; |
|||
device_wifi_ssid: string; |
|||
device_wifi_psk: string; |
|||
save_changes_button: string; |
|||
no_nodes_message: string; |
|||
no_message_placeholder: string; |
|||
} |
|||
|
|||
const App = () => { |
|||
const [deviceStatus, setDeviceStatus] = useState( |
|||
{} as Types.DeviceStatusEnum, |
|||
); |
|||
const [myNodeInfo, setMyNodeInfo] = useState({} as Protobuf.MyNodeInfo); |
|||
const [messages, setMessages] = useState( |
|||
[] as { message: Types.TextPacket; ack: false }[], |
|||
); |
|||
const [channels, setChannels] = useState([] as Protobuf.Channel[]); |
|||
const [nodes, setNodes] = useState([] as Types.NodeInfoPacket[]); |
|||
const [connection, setConnection] = useState({} as IHTTPConnection); |
|||
const [isReady, setIsReady] = useState(false); |
|||
const [lastMeshInterraction, setLastMeshInterraction] = useState(0); |
|||
const [preferences, setPreferences] = useState( |
|||
{} as Protobuf.RadioConfig_UserPreferences, |
|||
); |
|||
const [language, setLanguage] = useState(LanguageEnum.ENGLISH); |
|||
const [translations, setTranslations] = useState(Translations_English); |
|||
|
|||
function App({}: AppProps) { |
|||
// Create the count state.
|
|||
const [count, setCount] = useState(0); |
|||
// Create the counter (+1 every second).
|
|||
useEffect(() => { |
|||
const timer = setTimeout(() => setCount(count + 1), 1000); |
|||
return () => clearTimeout(timer); |
|||
}, [count, setCount]); |
|||
// Return the App component.
|
|||
switch (language) { |
|||
case LanguageEnum.ENGLISH: |
|||
setTranslations(Translations_English); |
|||
break; |
|||
case LanguageEnum.JAPANESE: |
|||
setTranslations(Translations_Japanese); |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
}, [language]); |
|||
|
|||
useEffect(() => { |
|||
const client = new Client(); |
|||
const connection = client.createHTTPConnection(); |
|||
// connection.connect(window.location.hostname, undefined, true);
|
|||
connection.connect({ |
|||
address: '192.168.105.71', |
|||
receiveBatchRequests: false, |
|||
tls: false, |
|||
fetchInterval: 2000, |
|||
}); |
|||
setConnection(connection); |
|||
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE; |
|||
connection.onDeviceStatusEvent.subscribe((status) => { |
|||
setDeviceStatus(status); |
|||
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) { |
|||
setIsReady(true); |
|||
} |
|||
}); |
|||
connection.onMyNodeInfoEvent.subscribe(setMyNodeInfo); |
|||
connection.onTextPacketEvent.subscribe((message) => { |
|||
setMessages((messages) => [ |
|||
...messages, |
|||
{ message: message, ack: false }, |
|||
]); |
|||
}); |
|||
connection.onNodeInfoPacketEvent.subscribe((node) => { |
|||
if ( |
|||
nodes.findIndex( |
|||
(currentNode) => currentNode.data.num === node.data.num, |
|||
) >= 0 |
|||
) { |
|||
setNodes( |
|||
nodes.map((currentNode) => |
|||
currentNode.data.num === node.data.num ? node : currentNode, |
|||
), |
|||
); |
|||
} else { |
|||
setNodes((nodes) => [...nodes, node]); |
|||
} |
|||
}); |
|||
|
|||
connection.onAdminPacketEvent.subscribe((adminMessage) => { |
|||
switch (adminMessage.data.variant.oneofKind) { |
|||
case 'getRadioResponse': |
|||
if (adminMessage.data.variant.getRadioResponse.preferences) { |
|||
setPreferences( |
|||
adminMessage.data.variant.getRadioResponse.preferences, |
|||
); |
|||
} |
|||
|
|||
break; |
|||
// case 'getChannelResponse':
|
|||
// if (adminMessage.data.variant.getChannelResponse) {
|
|||
// setChannels((channels) => [
|
|||
// ...channels,
|
|||
// adminMessage.data.variant.getChannelResponse,
|
|||
// ]);
|
|||
// }
|
|||
|
|||
default: |
|||
break; |
|||
} |
|||
}); |
|||
|
|||
connection.onMeshHeartbeat.subscribe(setLastMeshInterraction); |
|||
|
|||
connection.onRoutingPacketEvent.subscribe((routingPacket) => { |
|||
console.log(routingPacket); |
|||
|
|||
messages.map((message) => { |
|||
console.log( |
|||
`${ |
|||
routingPacket.payloadVariant.oneofKind === 'decoded' |
|||
? routingPacket.payloadVariant.decoded.requestId |
|||
: null |
|||
} === ${message.message.packet.id}: ${ |
|||
routingPacket.payloadVariant.oneofKind === 'decoded' |
|||
? routingPacket.payloadVariant.decoded.requestId |
|||
: null === message.message.packet.id |
|||
}`,
|
|||
); |
|||
}); |
|||
// messages.find((message) => {
|
|||
// message.message.packet.id === routingPacket.decoded.requestId;
|
|||
// });
|
|||
}); |
|||
}, []); |
|||
return ( |
|||
<div className="App"> |
|||
<header className="App-header"> |
|||
<img src={logo} className="App-logo" alt="logo" /> |
|||
<p> |
|||
Edit <code>src/App.tsx</code> and save to reload. |
|||
</p> |
|||
<p> |
|||
Page has been open for <code>{count}</code> seconds. |
|||
</p> |
|||
<p> |
|||
<a |
|||
className="App-link" |
|||
href="https://reactjs.org" |
|||
target="_blank" |
|||
rel="noopener noreferrer" |
|||
> |
|||
Learn React |
|||
</a> |
|||
</p> |
|||
</header> |
|||
<div className="flex flex-col h-screen w-screen"> |
|||
{/* <Head> |
|||
<link |
|||
rel="apple-touch-icon" |
|||
sizes="180x180" |
|||
href="/design/web/apple-touch-icon.png" |
|||
/> |
|||
<link |
|||
rel="icon" |
|||
type="image/png" |
|||
sizes="32x32" |
|||
href="/design/web/favicon-32x32.png" |
|||
/> |
|||
<link |
|||
rel="icon" |
|||
type="image/png" |
|||
sizes="16x16" |
|||
href="/design/web/favicon-16x16.png" |
|||
/> |
|||
<link rel="manifest" href="/design/web/site.webmanifest" /> |
|||
<link |
|||
rel="mask-icon" |
|||
href="/design/web/safari-pinned-tab.svg" |
|||
color="#67ea94" |
|||
/> |
|||
<meta name="theme-color" content="#67ea94" /> |
|||
</Head> */} |
|||
<Header |
|||
status={deviceStatus} |
|||
IsReady={isReady} |
|||
LastMeshInterraction={lastMeshInterraction} |
|||
/> |
|||
<Main |
|||
IsReady={isReady} |
|||
Messages={messages} |
|||
MyNodeInfo={myNodeInfo} |
|||
Connection={connection} |
|||
Nodes={nodes} |
|||
Channels={channels} |
|||
Preferences={preferences} |
|||
Language={language} |
|||
SetLanguage={setLanguage} |
|||
Translations={translations} |
|||
/> |
|||
</div> |
|||
); |
|||
} |
|||
}; |
|||
|
|||
export default App; |
|||
|
|||
@ -0,0 +1,113 @@ |
|||
import React, { useState } from 'react'; |
|||
|
|||
import { FaBars, FaPaperPlane } from 'react-icons/fa'; |
|||
|
|||
import type { |
|||
IHTTPConnection, |
|||
Protobuf, |
|||
Types, |
|||
} from '@meshtastic/meshtasticjs'; |
|||
|
|||
import type { LanguageEnum, languageTemplate } from './App'; |
|||
import ChatMessage from './components/ChatMessage'; |
|||
import Sidebar from './components/Sidebar'; |
|||
|
|||
interface MainProps { |
|||
Messages: { message: Types.TextPacket; ack: boolean }[]; |
|||
Connection: IHTTPConnection; |
|||
MyNodeInfo: Protobuf.MyNodeInfo; |
|||
Nodes: Types.NodeInfoPacket[]; |
|||
Channels: Protobuf.Channel[]; |
|||
IsReady: boolean; |
|||
Preferences: Protobuf.RadioConfig_UserPreferences; |
|||
Language: LanguageEnum; |
|||
SetLanguage: Function; |
|||
Translations: languageTemplate; |
|||
} |
|||
|
|||
const Main = (props: MainProps) => { |
|||
const [currentMessage, setCurrentMessage] = useState(''); |
|||
const [mobileNavOpen, setMobileNavOpen] = useState(true); |
|||
|
|||
const sendMessage = () => { |
|||
if (props.IsReady) { |
|||
props.Connection.sendText(currentMessage, undefined, true); |
|||
setCurrentMessage(''); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<div className="flex flex-col md:flex-row flex-grow space-2"> |
|||
<div className="flex flex-col flex-grow container mx-auto"> |
|||
<div className="flex flex-col flex-grow py-6 px-3 space-y-2"> |
|||
{props.Messages.length ? ( |
|||
props.Messages.map((message, Main) => ( |
|||
<ChatMessage |
|||
nodes={props.Nodes} |
|||
key={Main} |
|||
message={message} |
|||
myId={props.MyNodeInfo.myNodeNum} |
|||
/> |
|||
)) |
|||
) : ( |
|||
<div className="m-auto text-2xl text-gray-500"> |
|||
{props.Translations.no_messages_message} |
|||
</div> |
|||
)} |
|||
</div> |
|||
<div className="flex space-x-2 w-full p-3"> |
|||
<form |
|||
className="flex flex-wrap relative w-full" |
|||
onSubmit={(e) => { |
|||
e.preventDefault(); |
|||
sendMessage(); |
|||
}} |
|||
> |
|||
{props.IsReady} |
|||
<input |
|||
type="text" |
|||
placeholder={`${props.Translations.no_message_placeholder}...`} |
|||
disabled={!props.IsReady} |
|||
value={currentMessage} |
|||
onChange={(e) => { |
|||
setCurrentMessage(e.target.value); |
|||
}} |
|||
className={`p-3 placeholder-gray-400 text-gray-700 relative rounded-md shadow-md focus:outline-none w-full pr-10 ${ |
|||
props.IsReady ? 'cursor-text' : 'cursor-not-allowed' |
|||
}`}
|
|||
/> |
|||
<span className="z-10 h-full text-gray-400 absolute w-8 right-0 py-4"> |
|||
<FaPaperPlane |
|||
onClick={sendMessage} |
|||
className={`text-xl hover:text-gray-500 ${ |
|||
props.IsReady ? 'cursor-pointer' : 'cursor-not-allowed' |
|||
}`}
|
|||
/> |
|||
</span> |
|||
</form> |
|||
<div |
|||
className="flex p-3 text-xl hover:text-gray-500 text-gray-400 rounded-md shadow-md focus:outline-none cursor-pointer md:hidden" |
|||
onClick={() => { |
|||
setMobileNavOpen(!mobileNavOpen); |
|||
}} |
|||
> |
|||
<FaBars className="m-auto" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<Sidebar |
|||
IsReady={props.IsReady} |
|||
Nodes={props.Nodes} |
|||
Channels={props.Channels} |
|||
Preferences={props.Preferences} |
|||
Connection={props.Connection} |
|||
MobileNavOpen={mobileNavOpen} |
|||
Language={props.Language} |
|||
SetLanguage={props.SetLanguage} |
|||
Translations={props.Translations} |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default Main; |
|||
@ -0,0 +1,64 @@ |
|||
import React from 'react'; |
|||
|
|||
import { FaCheckCircle, FaCircle, FaUser } from 'react-icons/fa'; |
|||
|
|||
import type { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
interface ChatMessageProps { |
|||
message: { message: Types.TextPacket; ack: boolean }; |
|||
myId: number; |
|||
nodes: Types.NodeInfoPacket[]; |
|||
} |
|||
|
|||
const ChatMessage = (props: ChatMessageProps) => { |
|||
return ( |
|||
<div className="flex items-end"> |
|||
<div |
|||
className={`flex p-3 rounded-full shadow-md ${ |
|||
props.message.message.packet.from !== props.myId |
|||
? 'bg-gray-300' |
|||
: 'bg-green-200' |
|||
}`}
|
|||
> |
|||
<FaUser className="m-auto" /> |
|||
</div> |
|||
<div className="flex flex-col container px-2 items-start"> |
|||
<div |
|||
className={`px-4 py-2 rounded-md shadow-md ${ |
|||
props.message.message.packet.from !== props.myId |
|||
? 'bg-gray-300' |
|||
: 'bg-green-200' |
|||
}`}
|
|||
> |
|||
<div className="flex text-xs text-gray-500 space-x-1"> |
|||
<div className="font-medium"> |
|||
{/* { |
|||
props.nodes.find( |
|||
(node) => node.data.num === props.message.message.packet.from, |
|||
).data.user.longName |
|||
} */} |
|||
</div> |
|||
<p>-</p> |
|||
<div className="underline"> |
|||
{new Date( |
|||
props.message.message.packet.rxTime > 0 |
|||
? props.message.message.packet.rxTime |
|||
: Date.now(), |
|||
).toLocaleString()} |
|||
</div> |
|||
</div> |
|||
<div className="flex justify-between text-gray-600"> |
|||
<span className="inline-block">{props.message.message.data}</span> |
|||
{props.message.ack ? ( |
|||
<FaCheckCircle className="my-auto" /> |
|||
) : ( |
|||
<FaCircle className=" text-lg my-auto animate-pulse" /> |
|||
)} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ChatMessage; |
|||
@ -0,0 +1,56 @@ |
|||
import React from 'react'; |
|||
|
|||
import { FaBroadcastTower, FaMobileAlt } from 'react-icons/fa'; |
|||
|
|||
import { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
interface HeaderProps { |
|||
status: Types.DeviceStatusEnum; |
|||
IsReady: boolean; |
|||
LastMeshInterraction: number; |
|||
} |
|||
|
|||
const Header = (props: HeaderProps) => { |
|||
return ( |
|||
<nav className="w-full shadow-md"> |
|||
<div className="flex w-full container mx-auto justify-between px-6 py-4"> |
|||
<img src="/design/typelogo/typelogo.svg" height="30" width="200" /> |
|||
|
|||
<div className="flex items-center"> |
|||
<div className="flex pl-4"> |
|||
<div |
|||
className={`w-5 h-5 rounded-full ${ |
|||
new Date(props.LastMeshInterraction) < |
|||
new Date(Date.now() - 40000) |
|||
? 'bg-red-400' |
|||
: new Date(props.LastMeshInterraction) < |
|||
new Date(Date.now() - 20000) |
|||
? 'bg-yellow-400' |
|||
: 'bg-green-400' |
|||
}`}
|
|||
></div> |
|||
<FaBroadcastTower className="m-auto ml-1 text-xl" /> |
|||
</div> |
|||
|
|||
<div className="flex pl-4"> |
|||
<div |
|||
className={`w-5 h-5 rounded-full ${ |
|||
props.status <= Types.DeviceStatusEnum.DEVICE_DISCONNECTED |
|||
? 'bg-red-400' |
|||
: props.status <= Types.DeviceStatusEnum.DEVICE_CONFIGURING && |
|||
!props.IsReady |
|||
? 'bg-yellow-400' |
|||
: props.IsReady |
|||
? 'bg-green-400' |
|||
: 'bg-gray-400' |
|||
}`}
|
|||
></div> |
|||
<FaMobileAlt className="m-auto ml-1 text-xl" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
); |
|||
}; |
|||
|
|||
export default Header; |
|||
@ -0,0 +1,66 @@ |
|||
import React, { ReactNode, useEffect, useState } from 'react'; |
|||
|
|||
import { FaCaretDown, FaCaretRight, FaSpinner } from 'react-icons/fa'; |
|||
|
|||
interface NavItemProps { |
|||
isDropdown: boolean; |
|||
open?: boolean; |
|||
isNested: boolean; |
|||
titleContent: ReactNode; |
|||
dropdownContent?: ReactNode; |
|||
onClick?: Function; |
|||
isLoading?: boolean; |
|||
} |
|||
|
|||
const NavItem = (props: NavItemProps) => { |
|||
useEffect(() => { |
|||
if (props.open) { |
|||
setNavItemOpen(props.open); |
|||
} |
|||
}, []); |
|||
const [navItemOpen, setNavItemOpen] = useState(false); |
|||
return ( |
|||
<> |
|||
<div |
|||
className={`flex w-full text-lg font-medium justify-between ${ |
|||
navItemOpen && props.isNested ? 'bg-gray-100' : null |
|||
} ${props.isNested ? 'border-b px-3 py-1' : 'p-3'} ${ |
|||
props.isDropdown && navItemOpen ? 'shadow-md' : 'border-b' |
|||
} ${ |
|||
props.isDropdown || props.isNested |
|||
? 'hover:bg-gray-200 cursor-pointer' |
|||
: null |
|||
}`}
|
|||
onClick={() => { |
|||
if (props.isDropdown) setNavItemOpen(!navItemOpen); |
|||
if (props.onClick) { |
|||
props.onClick(); |
|||
} |
|||
}} |
|||
> |
|||
{props.titleContent} |
|||
{props.isDropdown && !props.isLoading ? ( |
|||
navItemOpen ? ( |
|||
<FaCaretDown className="my-auto group-hover:text-gray-700" /> |
|||
) : ( |
|||
<FaCaretRight className="my-auto group-hover:text-gray-700" /> |
|||
) |
|||
) : null} |
|||
{props.isLoading ? ( |
|||
<FaSpinner className="animate-spin my-auto" /> |
|||
) : null} |
|||
</div> |
|||
{props.isDropdown ? ( |
|||
<div |
|||
className={`duration-200 ease-in-out transition-all overflow-hidden max-h-0 border-l-8 ${ |
|||
props.isNested ? 'border-gray-500' : 'border-gray-300' |
|||
} ${navItemOpen ? 'max-h-full' : null}`}
|
|||
> |
|||
{props.dropdownContent} |
|||
</div> |
|||
) : null} |
|||
</> |
|||
); |
|||
}; |
|||
|
|||
export default NavItem; |
|||
@ -0,0 +1,62 @@ |
|||
import React from 'react'; |
|||
|
|||
import type { |
|||
IHTTPConnection, |
|||
Protobuf, |
|||
Types, |
|||
} from '@meshtastic/meshtasticjs'; |
|||
|
|||
import type { LanguageEnum, languageTemplate } from '../App'; |
|||
import SidebarChannels from './Sidebar/SidebarChannels'; |
|||
import SidebarDeviceSettings from './Sidebar/SidebarDeviceSettings'; |
|||
import SidebarNodes from './Sidebar/SidebarNodes'; |
|||
import SidebarUISettings from './Sidebar/SidebarUISettings'; |
|||
|
|||
interface SidebarProps { |
|||
IsReady: boolean; |
|||
Nodes: Types.NodeInfoPacket[]; |
|||
Channels: Protobuf.Channel[]; |
|||
Preferences: Protobuf.RadioConfig_UserPreferences; |
|||
Connection: IHTTPConnection; |
|||
MobileNavOpen: boolean; |
|||
Language: LanguageEnum; |
|||
SetLanguage: Function; |
|||
Translations: languageTemplate; |
|||
} |
|||
|
|||
const Sidebar = (props: SidebarProps) => { |
|||
const updatePreferences = () => {}; |
|||
|
|||
return ( |
|||
<div |
|||
className={`flex flex-col rounded-md m-3 md:ml-0 shadow-md w-full max-w-sm ${ |
|||
!props.MobileNavOpen ? 'hidden' : 'visible' |
|||
}`}
|
|||
> |
|||
<SidebarNodes |
|||
IsReady={props.IsReady} |
|||
Nodes={props.Nodes} |
|||
Translations={props.Translations} |
|||
/> |
|||
<SidebarDeviceSettings |
|||
IsReady={props.IsReady} |
|||
Preferences={props.Preferences} |
|||
Connection={props.Connection} |
|||
Translations={props.Translations} |
|||
/> |
|||
<SidebarChannels |
|||
IsReady={props.IsReady} |
|||
Channels={props.Channels} |
|||
Translations={props.Translations} |
|||
/> |
|||
<div className="flex-grow border-b"></div> |
|||
<SidebarUISettings |
|||
Language={props.Language} |
|||
SetLanguage={props.SetLanguage} |
|||
Translations={props.Translations} |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default Sidebar; |
|||
@ -0,0 +1,145 @@ |
|||
import React from 'react'; |
|||
|
|||
import { FaStream } from 'react-icons/fa'; |
|||
|
|||
import { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import type { languageTemplate } from '../../App'; |
|||
import NavItem from '../NavItem'; |
|||
|
|||
interface SidebarChannelsProps { |
|||
IsReady: boolean; |
|||
Channels: Protobuf.Channel[]; |
|||
Translations: languageTemplate; |
|||
} |
|||
|
|||
const SidebarChannels = (props: SidebarChannelsProps) => { |
|||
return ( |
|||
<NavItem |
|||
isDropdown={true} |
|||
open={false} |
|||
isNested={false} |
|||
titleContent={ |
|||
<div className="flex"> |
|||
<FaStream className="my-auto mr-2" /> |
|||
{props.Translations.device_channels_title} |
|||
</div> |
|||
} |
|||
isLoading={!props.IsReady} |
|||
dropdownContent={ |
|||
<> |
|||
{props.Channels.map((channel, index) => { |
|||
if (channel.role !== Protobuf.Channel_Role.DISABLED) |
|||
return ( |
|||
<NavItem |
|||
key={index} |
|||
isDropdown={true} |
|||
isNested={true} |
|||
titleContent={ |
|||
<div className="flex"> |
|||
{channel.index} - {Protobuf.Channel_Role[channel.role]} |
|||
</div> |
|||
} |
|||
dropdownContent={ |
|||
<NavItem |
|||
isDropdown={false} |
|||
isNested={false} |
|||
titleContent={ |
|||
<div className="w-full"> |
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Bandwidth:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.bandwidth} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Channel Number:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.channelNum} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Coding Rate:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.codingRate} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>ID:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.id} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Modem Config:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.modemConfig |
|||
? Protobuf.ChannelSettings_ModemConfig[ |
|||
channel.settings.modemConfig |
|||
] |
|||
: null} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Name:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.name} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>PSK:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.psk.toLocaleString()} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Spread Factor:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.spreadFactor} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Tx Power:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.txPower} |
|||
</code> |
|||
</div> |
|||
|
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Uplink:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.uplinkEnabled |
|||
? 'true' |
|||
: 'false'} |
|||
</code> |
|||
</div> |
|||
<div className="flex justify-between border-b hover:bg-gray-200"> |
|||
<p>Downlink:</p> |
|||
<code className="bg-gray-200 rounded-full px-2"> |
|||
{channel.settings?.downlinkEnabled |
|||
? 'true' |
|||
: 'false'} |
|||
</code> |
|||
</div> |
|||
</div> |
|||
} |
|||
/> |
|||
} |
|||
/> |
|||
); |
|||
})} |
|||
</> |
|||
} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
export default SidebarChannels; |
|||
@ -0,0 +1,106 @@ |
|||
import React from 'react'; |
|||
|
|||
import { FaSave, FaUserCog } from 'react-icons/fa'; |
|||
|
|||
import { IHTTPConnection, Protobuf } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import type { languageTemplate } from '../../App'; |
|||
import NavItem from '../NavItem'; |
|||
|
|||
interface SidebarDeviceSettingsProps { |
|||
IsReady: boolean; |
|||
Preferences: Protobuf.RadioConfig_UserPreferences; |
|||
Connection: IHTTPConnection; |
|||
Translations: languageTemplate; |
|||
} |
|||
|
|||
const SidebarDeviceSettings = (props: SidebarDeviceSettingsProps) => { |
|||
return ( |
|||
<NavItem |
|||
isDropdown={true} |
|||
open={false} |
|||
isNested={false} |
|||
titleContent={ |
|||
<div className="flex"> |
|||
<FaUserCog className="my-auto mr-2" /> |
|||
{props.Translations.device_settings_title} |
|||
</div> |
|||
} |
|||
isLoading={!props.IsReady} |
|||
dropdownContent={ |
|||
<> |
|||
<div className="flex whitespace-nowrap p-3 justify-between border-b"> |
|||
<div className="my-auto"> |
|||
{props.Translations.device_region_title} |
|||
</div> |
|||
<div className="flex shadow-md rounded-md ml-2"> |
|||
<select |
|||
value={props.Preferences?.region ?? Protobuf.RegionCode.Unset} |
|||
onChange={(e) => { |
|||
props.Preferences.region = parseInt(e.target.value); |
|||
}} |
|||
> |
|||
<option value={Protobuf.RegionCode.ANZ}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.ANZ]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.CN}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.CN]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.EU433}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.EU433]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.EU865}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.EU865]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.JP}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.JP]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.KR}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.KR]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.TW}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.TW]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.US}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.US]} |
|||
</option> |
|||
<option value={Protobuf.RegionCode.Unset}> |
|||
{Protobuf.RegionCode[Protobuf.RegionCode.Unset]} |
|||
</option> |
|||
</select> |
|||
</div> |
|||
</div> |
|||
<div className="flex whitespace-nowrap p-3 justify-between border-b"> |
|||
<div className="my-auto">{props.Translations.device_wifi_ssid}</div> |
|||
<div className="flex shadow-md rounded-md ml-2"> |
|||
<input |
|||
onChange={() => {}} |
|||
type="text" |
|||
value={props.Preferences.wifiSsid} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div className="flex whitespace-nowrap p-3 justify-between border-b"> |
|||
<div className="my-auto">{props.Translations.device_wifi_psk}</div> |
|||
<div className="flex shadow-md rounded-md ml-2"> |
|||
<input type="password" value={props.Preferences.wifiPassword} /> |
|||
</div> |
|||
</div> |
|||
<div className="flex group p-1 bg-gray-100 cursor-pointer hover:bg-gray-200 border-b"> |
|||
<div |
|||
className="flex m-auto font-medium group-hover:text-gray-700" |
|||
onClick={() => { |
|||
props.Connection.setPreferences(props.Preferences); |
|||
}} |
|||
> |
|||
<FaSave className="m-auto mr-2 group-hover:text-gray-700" /> |
|||
{props.Translations.save_changes_button} |
|||
</div> |
|||
</div> |
|||
</> |
|||
} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
export default SidebarDeviceSettings; |
|||
@ -0,0 +1,88 @@ |
|||
import React from 'react'; |
|||
|
|||
import { FaDesktop, FaUsers } from 'react-icons/fa'; |
|||
|
|||
import type { Types } from '@meshtastic/meshtasticjs'; |
|||
|
|||
import type { languageTemplate } from '../../App'; |
|||
import NavItem from '../NavItem'; |
|||
|
|||
interface sidebarNodesProps { |
|||
IsReady: boolean; |
|||
Nodes: Types.NodeInfoPacket[]; |
|||
Translations: languageTemplate; |
|||
} |
|||
|
|||
const SidebarNodes = (props: sidebarNodesProps) => { |
|||
return ( |
|||
<NavItem |
|||
isDropdown={true} |
|||
open={false} |
|||
isNested={false} |
|||
titleContent={ |
|||
<div className="flex"> |
|||
<FaUsers className="my-auto mr-2" /> |
|||
{props.Translations.nodes_title} |
|||
<div className="flex m-auto rounded-full bg-gray-300 w-6 h-6 text-sm ml-2"> |
|||
<div className="m-auto">{props.Nodes.length ?? 0}</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
isLoading={!props.IsReady} |
|||
dropdownContent={ |
|||
props.Nodes.length ? ( |
|||
props.Nodes.map((node, index) => ( |
|||
<NavItem |
|||
key={index} |
|||
isDropdown={true} |
|||
isNested={true} |
|||
open={false} |
|||
titleContent={ |
|||
<div key={index} className="flex"> |
|||
<FaDesktop className="my-auto mr-2" /> |
|||
<div className="m-auto">{node.data.user?.longName}</div> |
|||
</div> |
|||
} |
|||
dropdownContent={ |
|||
<NavItem |
|||
isDropdown={false} |
|||
isNested={true} |
|||
titleContent={ |
|||
<div> |
|||
<p> |
|||
SNR:{' '} |
|||
{node.packet?.rxSnr ? node.packet.rxSnr : 'Unknown'} |
|||
</p> |
|||
<p> |
|||
RSSI:{' '} |
|||
{node.packet?.rxRssi ? node.packet.rxRssi : 'Unknown'} |
|||
</p> |
|||
<p> |
|||
Last heard:{' '} |
|||
{node.data?.lastHeard ? node.data.lastHeard : 'Unknown'} |
|||
</p> |
|||
<p> |
|||
Loc:{' '} |
|||
{node.data?.position |
|||
? `alt: ${node.data?.position.altitude}, lat: ${node.data?.position.latitudeI}, lng: ${node.data?.position.longitudeI}, time: ${node.data?.position.time}, batt: ${node.data?.position.batteryLevel}` |
|||
: 'Unknown'} |
|||
</p> |
|||
</div> |
|||
} |
|||
/> |
|||
} |
|||
/> |
|||
)) |
|||
) : ( |
|||
<div className="flex border-b border-gray-300"> |
|||
<div className="m-auto p-3 text-gray-500"> |
|||
{props.Translations.no_nodes_message} |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
export default SidebarNodes; |
|||
@ -0,0 +1,110 @@ |
|||
import React from 'react'; |
|||
|
|||
// import Flags from 'country-flag-icons/react/3x2';
|
|||
import { FaCog, FaLaptop, FaMoon, FaSun } from 'react-icons/fa'; |
|||
|
|||
import type { languageTemplate } from '../../App'; |
|||
import { LanguageEnum } from '../../App'; |
|||
import ToggleSwitch from '../basic/ToggleSwitch'; |
|||
import NavItem from '../NavItem'; |
|||
|
|||
interface SidebarUISettingsProps { |
|||
Language: LanguageEnum; |
|||
SetLanguage: Function; |
|||
Translations: languageTemplate; |
|||
} |
|||
|
|||
const SidebarUISettings = (props: SidebarUISettingsProps) => { |
|||
return ( |
|||
<NavItem |
|||
isDropdown={true} |
|||
isNested={false} |
|||
titleContent={ |
|||
<div className="flex"> |
|||
<FaCog className="my-auto mr-2" /> |
|||
{props.Translations.ui_settings_title} |
|||
</div> |
|||
} |
|||
dropdownContent={ |
|||
<> |
|||
<NavItem |
|||
isDropdown={false} |
|||
isNested={true} |
|||
titleContent={ |
|||
<> |
|||
<div className="my-auto"> |
|||
{props.Translations.color_scheme_title} |
|||
</div> |
|||
<div className="flex shadow-md rounded-md ml-2"> |
|||
<div className="bg-gray-200 flex group p-2 rounded-l-md border border-gray-300 hover:bg-gray-200 cursor-pointer"> |
|||
<FaSun className="m-auto group-hover:text-gray-700" /> |
|||
</div> |
|||
<div className="flex group p-2 border border-gray-300 hover:bg-gray-200 cursor-pointer"> |
|||
<FaMoon className="m-auto group-hover:text-gray-700" /> |
|||
</div> |
|||
<div className="flex group p-2 rounded-r-md border border-gray-300 hover:bg-gray-200 cursor-pointer"> |
|||
<FaLaptop className="m-auto group-hover:text-gray-700" /> |
|||
</div> |
|||
</div> |
|||
</> |
|||
} |
|||
/> |
|||
<NavItem |
|||
isDropdown={true} |
|||
isNested={true} |
|||
open={false} |
|||
titleContent={ |
|||
<div className="flex my-auto"> |
|||
{/* {props.Translations.language_title} |
|||
{props.Language === LanguageEnum.ENGLISH ? ( |
|||
<Flags.US className="ml-2 w-8 shadow-md" /> |
|||
) : props.Language === LanguageEnum.JAPANESE ? ( |
|||
<Flags.JP className="ml-2 w-8 shadow-md" /> |
|||
) : ( |
|||
'' |
|||
)} */} |
|||
</div> |
|||
} |
|||
dropdownContent={ |
|||
<> |
|||
<NavItem |
|||
onClick={() => { |
|||
props.SetLanguage(LanguageEnum.ENGLISH); |
|||
}} |
|||
isDropdown={false} |
|||
isNested={true} |
|||
titleContent={ |
|||
<>{/* English <Flags.US className="w-8 shadow-md" /> */}</> |
|||
} |
|||
/> |
|||
<NavItem |
|||
onClick={() => { |
|||
props.SetLanguage(LanguageEnum.JAPANESE); |
|||
}} |
|||
isDropdown={false} |
|||
isNested={true} |
|||
titleContent={ |
|||
<>{/* 日本語 <Flags.JP className="w-8 shadow-md" /> */}</> |
|||
} |
|||
/> |
|||
</> |
|||
} |
|||
/> |
|||
<NavItem |
|||
isDropdown={false} |
|||
isNested={true} |
|||
open={false} |
|||
titleContent={ |
|||
<> |
|||
<div className="">Test</div> |
|||
<ToggleSwitch active={true} /> |
|||
</> |
|||
} |
|||
/> |
|||
</> |
|||
} |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
export default SidebarUISettings; |
|||
@ -0,0 +1,9 @@ |
|||
import React from 'react'; |
|||
|
|||
interface TimelineProps {} |
|||
|
|||
const Timeline = (props: TimelineProps) => { |
|||
return <div></div>; |
|||
}; |
|||
|
|||
export default Timeline; |
|||
@ -0,0 +1,12 @@ |
|||
import React from 'react'; |
|||
|
|||
interface TimelineItemProps { |
|||
time: number; |
|||
color: string; |
|||
} |
|||
|
|||
const TimelineItem = (props: TimelineItemProps) => { |
|||
return <div className={`rounded-full h-6 w-6 bg-${props.color}`}>Test</div>; |
|||
}; |
|||
|
|||
export default TimelineItem; |
|||
@ -0,0 +1,33 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
|
|||
interface ToggleSwitchProps { |
|||
active: boolean; |
|||
toggle?: Function; |
|||
} |
|||
|
|||
const ToggleSwitch = (props: ToggleSwitchProps) => { |
|||
const [active, setActive] = useState(false); |
|||
|
|||
useEffect(() => { |
|||
setActive(props.active); |
|||
}, []); |
|||
|
|||
return ( |
|||
<div |
|||
onClick={() => { |
|||
setActive(!active); |
|||
}} |
|||
className={`w-12 h-6 flex items-center bg-gray-300 rounded-full p-1 duration-300 ease-in-out my-auto ${ |
|||
active ? 'bg-green-400' : null |
|||
}`}
|
|||
> |
|||
<div |
|||
className={`bg-white w-4 h-4 rounded-full shadow-md transform duration-300 ease-in-out ${ |
|||
active ? 'translate-x-6' : null |
|||
}`}
|
|||
></div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ToggleSwitch; |
|||
@ -1,13 +1,3 @@ |
|||
body { |
|||
margin: 0; |
|||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", |
|||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", |
|||
sans-serif; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
} |
|||
|
|||
code { |
|||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", |
|||
monospace; |
|||
} |
|||
@tailwind base; |
|||
@tailwind components; |
|||
@tailwind utilities; |
|||
|
|||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -0,0 +1,17 @@ |
|||
import type { languageTemplate } from '../App'; |
|||
|
|||
export default { |
|||
no_messages_message: 'No messages yet', |
|||
ui_settings_title: 'UI Settings', |
|||
nodes_title: 'Nodes', |
|||
device_settings_title: 'Device Settings', |
|||
device_channels_title: 'Channels', |
|||
color_scheme_title: 'Color scheme', |
|||
language_title: 'Language', |
|||
device_region_title: 'Device Region', |
|||
device_wifi_ssid: 'WiFi SSID', |
|||
device_wifi_psk: 'WiFi PSK', |
|||
save_changes_button: 'Save changes', |
|||
no_nodes_message: 'No nodes found', |
|||
no_message_placeholder: 'Enter Message', |
|||
} as languageTemplate; |
|||
@ -0,0 +1,16 @@ |
|||
import type { languageTemplate } from '../App'; |
|||
|
|||
export default { |
|||
no_messages_message: 'まだメッセージはありません', |
|||
ui_settings_title: 'UI設定', |
|||
nodes_title: 'ノード', |
|||
device_settings_title: 'デバイスの設定', |
|||
color_scheme_title: 'カラースキーム', |
|||
language_title: '言語', |
|||
device_region_title: 'デバイスリージョン', |
|||
device_wifi_ssid: 'WiFi名', |
|||
device_wifi_psk: 'WiFiパスワード', |
|||
save_changes_button: '変更内容を保存', |
|||
no_nodes_message: 'ノードが見つかりません', |
|||
no_message_placeholder: 'メッセージを入力してください', |
|||
} as languageTemplate; |
|||
@ -0,0 +1,11 @@ |
|||
module.exports = { |
|||
purge: ['./src/**/*.{js,ts,jsx,tsx}'], |
|||
darkMode: false, // or 'media' or 'class'
|
|||
theme: { |
|||
extend: {}, |
|||
}, |
|||
variants: { |
|||
extend: {}, |
|||
}, |
|||
plugins: [], |
|||
}; |
|||
@ -0,0 +1,56 @@ |
|||
Arguments: |
|||
/usr/bin/node /usr/share/yarn/bin/yarn.js add -D tailwindcss@latest postcss@latest autoprefixer@latest |
|||
|
|||
PATH: |
|||
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/mnt/c/Program Files/AdoptOpenJDK/jdk-11.0.10.9-hotspot/bin:/mnt/c/Program Files (x86)/Common Files/Oracle/Java/javapath:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files (x86)/NVIDIA Corporation/PhysX/Common:/mnt/c/Program Files/Git/cmd:/mnt/c/Program Files/nodejs/:/mnt/c/Users/sacha/AppData/Local/Programs/Python/Python39/Scripts/:/mnt/c/Users/sacha/AppData/Local/Programs/Python/Python39/:/mnt/c/Users/sacha/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/sacha/AppData/Local/Programs/Microsoft VS Code/bin:/mnt/c/Users/sacha/AppData/Roaming/npm:/home/sachaw/.yarn/bin |
|||
|
|||
Yarn version: |
|||
1.22.5 |
|||
|
|||
Node version: |
|||
15.12.0 |
|||
|
|||
Platform: |
|||
linux x64 |
|||
|
|||
Trace: |
|||
Error: getaddrinfo EAI_AGAIN registry.yarnpkg.com |
|||
at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:69:26) |
|||
|
|||
npm manifest: |
|||
{ |
|||
"scripts": { |
|||
"start": "snowpack dev", |
|||
"build": "snowpack build", |
|||
"test": "web-test-runner \"src/**/*.test.tsx\"", |
|||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", |
|||
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"" |
|||
}, |
|||
"dependencies": { |
|||
"react": "^17.0.0", |
|||
"react-dom": "^17.0.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@snowpack/plugin-dotenv": "^2.0.5", |
|||
"@snowpack/plugin-react-refresh": "^2.4.0", |
|||
"@snowpack/plugin-typescript": "^1.2.0", |
|||
"@snowpack/web-test-runner-plugin": "^0.2.0", |
|||
"@testing-library/react": "^11.0.0", |
|||
"@types/chai": "^4.2.13", |
|||
"@types/mocha": "^8.2.0", |
|||
"@types/react": "^17.0.0", |
|||
"@types/react-dom": "^17.0.0", |
|||
"@types/snowpack-env": "^2.3.2", |
|||
"@web/test-runner": "^0.12.0", |
|||
"chai": "^4.2.0", |
|||
"prettier": "^2.0.5", |
|||
"snowpack": "^3.0.1", |
|||
"typescript": "^4.0.0" |
|||
} |
|||
} |
|||
|
|||
yarn manifest: |
|||
No manifest |
|||
|
|||
Lockfile: |
|||
No lockfile |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue