Skip to content

Commit

Permalink
feat: add query parameter to specify your custom server lists to disp…
Browse files Browse the repository at this point in the history
…lay in UI: ?serversList

feat: custom proxy is applied in servers list ui when ?proxy is specified
feat: add a way to quickly copy server list with ctrl+c in servers list ui
  • Loading branch information
zardoy committed Nov 22, 2024
1 parent b881a61 commit c97dbbd
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Server specific:
- `?username=<username>` - Set the username for the server
- `?lockConnect=true` - Only works then `ip` parameter is set. Disables cancel/save buttons and all inputs in the connect screen already set as parameters. Useful for integrates iframes.
- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing.
- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs.

Single player specific:

Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,10 @@ downloadAndOpenFile().then((downloadAction) => {
})
}
})

if (qs.get('serversList')) {
showModal({ reactType: 'serversList' })
}
}, (err) => {
console.error(err)
alert(`Failed to download file: ${err}`)
Expand Down
75 changes: 64 additions & 11 deletions src/react/ServersListProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
import { useDidUpdateEffect } from './utils'
import { useIsModalActive } from './utilsApp'
import { showOptionsModal } from './SelectOption'
import { useCopyKeybinding } from './simpleHooks'

interface StoreServerItem extends BaseServerInfo {
lastJoined?: number
Expand Down Expand Up @@ -92,7 +93,11 @@ const getInitialServersList = () => {
return servers
}

const setNewServersList = (serversList: StoreServerItem[]) => {
const serversListQs = new URLSearchParams(window.location.search).get('serversList')
const proxyQs = new URLSearchParams(window.location.search).get('proxy')

const setNewServersList = (serversList: StoreServerItem[], force = false) => {
if (serversListQs && !force) return
localStorage['serversList'] = JSON.stringify(serversList)

// cleanup legacy
Expand Down Expand Up @@ -133,13 +138,14 @@ export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAcc
// todo move to base
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')

const Inner = ({ hidden }: { hidden?: boolean }) => {
const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersList?: string[] }) => {
const [proxies, setProxies] = useState<readonly string[]>(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies())
const [selectedProxy, setSelectedProxy] = useState(localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
const [selectedProxy, setSelectedProxy] = useState(proxyQs ?? localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
const [serverEditScreen, setServerEditScreen] = useState<StoreServerItem | true | null>(null) // true for add
const [defaultUsername, _setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
const [authenticatedAccounts, _setAuthenticatedAccounts] = useState<AuthenticatedAccount[]>(JSON.parse(localStorage['authenticatedAccounts'] || '[]'))
const [quickConnectIp, setQuickConnectIp] = useState('')
const [selectedIndex, setSelectedIndex] = useState(-1)

const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
_setAuthenticatedAccounts(newState)
Expand All @@ -151,18 +157,35 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
localStorage.setItem('username', newState)
}

const saveNewProxy = () => {
if (!selectedProxy || proxyQs) return
localStorage.setItem('selectedProxy', selectedProxy)
}

useEffect(() => {
if (proxies.length) {
localStorage.setItem('proxies', JSON.stringify(proxies))
}
if (selectedProxy) {
localStorage.setItem('selectedProxy', selectedProxy)
}
saveNewProxy()
}, [proxies])

const [serversList, setServersList] = useState<StoreServerItem[]>(() => getInitialServersList())
const [serversList, setServersList] = useState<StoreServerItem[]>(() => (customServersList ? [] : getInitialServersList()))
const [additionalData, setAdditionalData] = useState<Record<string, AdditionalDisplayData>>({})

useEffect(() => {
if (customServersList) {
setServersList(customServersList.map(row => {
const [ip, name] = row.split(' ')
const [_ip, _port, version] = ip.split(':')
return {
ip,
versionOverride: version,
name,
}
}))
}
}, [customServersList])

useDidUpdateEffect(() => {
// save data only on user changes
setNewServersList(serversList)
Expand Down Expand Up @@ -218,6 +241,16 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
}
}, [isEditScreenModal])

useCopyKeybinding(() => {
const item = serversList[selectedIndex]
if (!item) return
let str = `${item.ip}`
if (item.versionOverride) {
str += `:${item.versionOverride}`
}
return str
})

const editModalJsx = isEditScreenModal ? <AddServerOrConnect
placeholders={{
proxyOverride: selectedProxy,
Expand Down Expand Up @@ -319,15 +352,14 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
// setProxies([...proxies, selectedProxy])
localStorage.setItem('proxies', JSON.stringify([...proxies, selectedProxy]))
}
if (selectedProxy) {
localStorage.setItem('selectedProxy', selectedProxy)
}
saveNewProxy()
},
serverIndex: shouldSave ? serversList.length.toString() : indexOrIp // assume last
} satisfies ConnectOptions
dispatchEvent(new CustomEvent('connect', { detail: options }))
// qsOptions
}}
lockedEditing={!!customServersList}
username={defaultUsername}
setUsername={setDefaultUsername}
setQuickConnectIp={setQuickConnectIp}
Expand Down Expand Up @@ -377,6 +409,9 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
setSelectedProxy(selected)
}}
hidden={hidden}
onRowSelect={(_, i) => {
setSelectedIndex(i)
}}
/>
return <>
{serversListJsx}
Expand All @@ -385,12 +420,30 @@ const Inner = ({ hidden }: { hidden?: boolean }) => {
}

export default () => {
const [customServersList, setCustomServersList] = useState<string[] | undefined>(serversListQs ? [] : undefined)

useEffect(() => {
if (serversListQs) {
if (serversListQs.startsWith('http')) {
void fetch(serversListQs).then(async r => r.text()).then((text) => {
const isJson = serversListQs.endsWith('.json') ? true : serversListQs.endsWith('.txt') ? false : text.startsWith('[')
setCustomServersList(isJson ? JSON.parse(text) : text.split('\n').map(x => x.trim()).filter(x => x.trim().length > 0))
}).catch((err) => {
console.error(err)
alert(`Failed to get servers list file: ${err}`)
})
} else {
setCustomServersList(serversListQs.split(','))
}
}
}, [])

const modalStack = useSnapshot(activeModalStack)
const hasServersListModal = modalStack.some(x => x.reactType === 'serversList')
const editServerModalActive = useIsModalActive('editServer')
const isServersListModalActive = useIsModalActive('serversList')

const eitherModal = isServersListModalActive || editServerModalActive
const render = eitherModal || hasServersListModal
return render ? <Inner hidden={!isServersListModalActive} /> : null
return render ? <Inner hidden={!isServersListModalActive} customServersList={customServersList} /> : null
}
10 changes: 6 additions & 4 deletions src/react/Singleplayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ interface Props {
listStyle?: React.CSSProperties
setListHovered?: (hovered: boolean) => void
secondRowStyles?: React.CSSProperties
lockedEditing?: boolean
}

export default ({
Expand All @@ -116,7 +117,8 @@ export default ({
defaultSelectedRow,
listStyle,
setListHovered,
secondRowStyles
secondRowStyles,
lockedEditing
}: Props) => {
const containerRef = useRef<any>()
const firstButton = useRef<HTMLButtonElement>(null)
Expand Down Expand Up @@ -213,10 +215,10 @@ export default ({
<Button onClick={() => onGeneralAction('create')} disabled={isReadonly}>Create New World</Button>
</div>}
<div style={{ ...secondRowStyles }}>
{serversLayout ? <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('edit', focusedWorld)}>Edit</Button> : <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('export', focusedWorld)}>Export</Button>}
<Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('delete', focusedWorld)}>Delete</Button>
{serversLayout ? <Button style={{ width: 100 }} disabled={!focusedWorld || lockedEditing} onClick={() => onWorldAction('edit', focusedWorld)}>Edit</Button> : <Button style={{ width: 100 }} disabled={!focusedWorld} onClick={() => onWorldAction('export', focusedWorld)}>Export</Button>}
<Button style={{ width: 100 }} disabled={!focusedWorld || lockedEditing} onClick={() => onWorldAction('delete', focusedWorld)}>Delete</Button>
{serversLayout ?
<Button style={{ width: 100 }} onClick={() => onGeneralAction('create')}>Add</Button> :
<Button style={{ width: 100 }} onClick={() => onGeneralAction('create')} disabled={lockedEditing}>Add</Button> :
<Button style={{ width: 100 }} onClick={() => onWorldAction('edit', focusedWorld)} disabled>Edit</Button>}
<Button style={{ width: 100 }} onClick={() => onGeneralAction('cancel')}>Cancel</Button>
</div>
Expand Down
21 changes: 21 additions & 0 deletions src/react/simpleHooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import { useUtilsEffect } from '@zardoy/react-util'
import { useMedia } from 'react-use'

const SMALL_SCREEN_MEDIA = '@media (max-width: 440px)'
export const useIsSmallWidth = () => {
return useMedia(SMALL_SCREEN_MEDIA.replace('@media ', ''))
}

export const useCopyKeybinding = (getCopyText: () => string | undefined) => {
useUtilsEffect(({ signal }) => {
addEventListener('keydown', (e) => {
if (e.code === 'KeyC' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
const { activeElement } = document
if (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement) {
return
}
if (window.getSelection()?.toString()) {
return
}
e.preventDefault()
const copyText = getCopyText()
if (!copyText) return
void navigator.clipboard.writeText(copyText)
}
}, { signal })
}, [getCopyText])
}

0 comments on commit c97dbbd

Please sign in to comment.