Skip to content

Commit

Permalink
Add remote server picker path browser
Browse files Browse the repository at this point in the history
  • Loading branch information
mircearoata committed Apr 20, 2024
1 parent 54059f9 commit e97a648
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 7 deletions.
120 changes: 120 additions & 0 deletions backend/ficsitcli/serverpicker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package ficsitcli

import (
"fmt"
"path/filepath"
"strconv"

"github.com/satisfactorymodding/ficsit-cli/cli/disk"
)

var ServerPicker = &serverPicker{
disks: make(map[string]disk.Disk),
}

type serverPicker struct {
disks map[string]disk.Disk
nextServerPickerID int
}

type PickerDirectory struct {
Name string `json:"name"`
IsValidInstall bool `json:"isValidInstall"`
}

type PickerResult struct {
IsValidInstall bool `json:"isValidInstall"`
Items []PickerDirectory `json:"items"`
}

func (s *serverPicker) getID() string {
id := s.nextServerPickerID
s.nextServerPickerID++
return strconv.Itoa(id)
}

func (s *serverPicker) StartPicker(path string) (string, error) {
id := s.getID()

d, err := disk.FromPath(path)
if err != nil {
return "", fmt.Errorf("failed to create: %w", err)
}

s.disks[id] = d

return id, nil
}

func (s *serverPicker) StopPicker(id string) error {
if _, ok := s.disks[id]; !ok {
return fmt.Errorf("no such disk: %s", id)
}
delete(s.disks, id)
return nil
}

func (s *serverPicker) TryPick(id string, path string) (PickerResult, error) {
d, ok := s.disks[id]
if !ok {
return PickerResult{}, fmt.Errorf("no such disk: %s", id)
}

result := PickerResult{
Items: make([]PickerDirectory, 0),
}

var err error

result.IsValidInstall, err = isValidInstall(d, path)
if err != nil {
return PickerResult{}, fmt.Errorf("failed to check if valid install: %w", err)
}

entries, err := d.ReadDir(path)
if err != nil {
return PickerResult{}, fmt.Errorf("failed reading directory: %w", err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}

validInstall, err := isValidInstall(d, filepath.Join(path, entry.Name()))
if err != nil {
return PickerResult{}, fmt.Errorf("failed to check if valid install: %w", err)
}

result.Items = append(result.Items, PickerDirectory{
Name: entry.Name(),
IsValidInstall: validInstall,
})
}

return result, nil
}

func isValidInstall(d disk.Disk, path string) (bool, error) {
var exists bool
var err error

exists, err = d.Exists(filepath.Join(path, "FactoryServer.sh"))
if !exists {
if err != nil {
return false, fmt.Errorf("failed reading FactoryServer.sh: %w", err)
}
} else {
return true, nil
}

exists, err = d.Exists(filepath.Join(path, "FactoryServer.exe"))
if !exists {
if err != nil {
return false, fmt.Errorf("failed reading FactoryServer.exe: %w", err)
}
} else {
return true, nil
}

return false, nil
}
187 changes: 187 additions & 0 deletions frontend/src/lib/components/RemoteServerPicker.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<script lang="ts">
import { mdiFolder, mdiLoading, mdiServerNetwork, mdiSubdirectoryArrowLeft } from '@mdi/js';
import _ from 'lodash';
import { onDestroy } from 'svelte';
import SvgIcon from '$lib/components/SVGIcon.svelte';
import { StartPicker, StopPicker, TryPick } from '$lib/generated/wailsjs/go/ficsitcli/serverPicker';
export let basePath: string;
export let disabled = false;
let currentBasePath = '';
let pickerId = '';
async function stopPicker() {
if (pickerId) {
await StopPicker(pickerId);
pickerId = '';
}
}
const restartPicker = _.debounce(async () => {
await stopPicker();
if (!disabled) {
pickerId = await StartPicker(basePath);
currentBasePath = basePath;
}
// TODO: handle errors
}, 1000);
let pendingValidCheck = false;
$: if (basePath !== currentBasePath && !disabled) {
pendingValidCheck = true;
displayedItems = [];
restartPicker();
}
$: if (disabled) {
stopPicker();
currentBasePath = '';
}
onDestroy(() => {
stopPicker();
});
interface PickerDirectory {
path: string;
name: string;
isValidInstall: boolean;
}
let displayedItems: { path: string; name: string; isValidInstall: boolean }[] = [];
export let path = '';
export let valid = false;
let validError: string | null = null;
const checkValid = _.debounce(async () => {
pendingValidCheck = true;
const forPath = path;
const forPickerId = pickerId;
try {
let { isValidInstall } = await TryPick(pickerId, path + '/');
// If the path has changed since the request was made,
// or the picker has been stopped, ignore the response
if (path !== forPath || pickerId !== forPickerId) {
return;
}
valid = isValidInstall;
validError = null;
} catch (e) {
// If the path has changed since the request was made,
// or the picker has been stopped, ignore the response
if (path !== forPath || pickerId !== forPickerId) {
return;
}
valid = false;
validError = e as string;
}
pendingValidCheck = false;
}, 250);
$: if (pickerId && !disabled) {
path;
pendingValidCheck = true;
checkValid();
}
$: trimmedPath = path.endsWith('/') ? path.slice(0, -1) : path;
let displayedPath = '';
$: if (!pendingValidCheck) {
displayedPath = valid ? trimmedPath.split('/').slice(0, -1).join('/') : trimmedPath;
}
let pendingDisplay = false;
let error: string | null = null;
const updateDisplay = _.debounce(async () => {
pendingDisplay = true;
const forPath = displayedPath;
const forPickerId = pickerId;
try {
let { items } = await TryPick(pickerId, displayedPath + '/');
// If the path has changed since the request was made,
// or the picker has been stopped, ignore the response
if (displayedPath !== forPath || pickerId !== forPickerId) {
return;
}
displayedItems = items.map((d) => ({
path: displayedPath + '/' + d.name,
name: d.name,
isValidInstall: d.isValidInstall,
}));
error = null;
} catch (e) {
// If the path has changed since the request was made,
// or the picker has been stopped, ignore the response
if (path !== forPath || pickerId !== forPickerId) {
return;
}
error = e as string;
}
pendingDisplay = false;
}, 250);
$: if(pickerId && !disabled) {
displayedPath;
pendingDisplay = true;
updateDisplay();
}
function select(item: PickerDirectory) {
valid = item.isValidInstall;
path = item.path;
}
// If the path is root and it is a valid install, don't list contents of root
$: actualDisplayedItems = (valid && displayedPath === trimmedPath) ? [{ path: '', name: '(root)', isValidInstall: true }] : displayedItems;
</script>

<div class="relative">
<div class="flex flex-col w-full card bg-surface-200-700-token">
<button class="w-full btn !scale-100" disabled={displayedPath.length <= 1 || pendingDisplay || pendingValidCheck} on:click={() => { path = displayedPath.split('/').slice(0, -1).join('/'); valid = false; }}>
<SvgIcon
class="h-5 w-5"
icon={mdiSubdirectoryArrowLeft} />
<div class="grow"/>
</button>
{#if !disabled}
<div class="overflow-y-auto">
{#each actualDisplayedItems as item}
<button
class="w-full btn !scale-100"
class:variant-ghost-primary={path.startsWith(item.path) && valid}
disabled={pendingDisplay || pendingValidCheck}
on:click={() => select(item)}>
<SvgIcon
class="h-5 w-5"
icon={item.isValidInstall ? mdiServerNetwork : mdiFolder} />
<span>{item.name}</span>
<div class="grow"/>
</button>
{/each}
</div>
{#if validError && !pendingValidCheck && !error}
<div class="text-error-500 p-4">Failed to check if selected path is a valid server</div>
{/if}
{/if}
</div>
{#if (((pendingDisplay || pendingValidCheck) && !valid) || error) && !disabled}
<div class="w-full h-full flex justify-center card items-center absolute top-0 !bg-surface-600/80">
{#if ((pendingDisplay || pendingValidCheck) && !valid)}
<SvgIcon
class="h-10 w-10 animate-spin text-primary-600"
icon={mdiLoading} />
{:else}
<div class="text-error-500">Failed to list directory</div>
{/if}
</div>
{/if}
</div>
42 changes: 35 additions & 7 deletions frontend/src/lib/components/modals/ServerManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { mdiAlert, mdiLoading, mdiServerNetwork, mdiTrashCan } from '@mdi/js';
import _ from 'lodash';
import RemoteServerPicker from '$lib/components/RemoteServerPicker.svelte';
import SvgIcon from '$lib/components/SVGIcon.svelte';
import Select from '$lib/components/Select.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
Expand Down Expand Up @@ -62,7 +63,17 @@
return newRemoteType.protocol + authString + '@' + newServerHost + ':' + actualPort + '/' + trimmedPath;
})();
$: isValid = (() => {
$: baseServerPath = (() => {
if (newRemoteType.type === 'local') {
return newServerPath;
}
if (advancedMode) {
return newRemoteType.protocol + newServerPath;
}
return newRemoteType.protocol + authString + '@' + newServerHost + ':' + actualPort;
})();
$: isBaseValid = (() => {
if (newRemoteType.type === 'local') {
return newServerPath.length > 0;
}
Expand All @@ -72,6 +83,18 @@
return newServerUsername.length > 0 && newServerHost.length > 0;
})();
let isPathValid = false;
$: isValid = (() => {
if (newRemoteType.type === 'local') {
return newServerPath.length > 0;
}
if (advancedMode) {
return newServerPath.length > 0;
}
return newServerUsername.length > 0 && newServerHost.length > 0 && isPathValid;
})();
async function addNewRemoteServer() {
if (!isValid) {
return;
Expand Down Expand Up @@ -196,8 +219,8 @@
</table>
</div>
</section>
<section class="p-4 space-y-4">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 items-start auto-rows-fr">
<section class="p-4 space-y-4 overflow-y-auto">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 items-start auto-rows-[minmax(2.5rem,_max-content)]">
<Select
name="newServerProtocol"
class="col-span-1 h-full"
Expand Down Expand Up @@ -247,11 +270,16 @@
placeholder="path"
type="text"
bind:value={newServerPath}/>
<p class="sm:col-start-2 col-span-2">
Complete path: {fullInstallPath}
</p>
<div class="sm:col-start-2 col-span-2">
<RemoteServerPicker
basePath={baseServerPath}
disabled={!isBaseValid}
bind:path={newServerPath}
bind:valid={isPathValid}
/>
</div>
{/if}
<button class="btn sm:col-start-2 col-span-2 text-sm whitespace-break-spaces bg-surface-200-700-token" on:click={() => advancedMode = !advancedMode}>
<button class="btn sm:col-start-1 col-span-1 row-start-2 text-sm whitespace-break-spaces bg-surface-200-700-token" on:click={() => advancedMode = !advancedMode}>
{#if advancedMode}
Switch to simple mode
{:else}
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func main() {
ficsitcli.FicsitCLI,
autoupdate.Updater,
settings.Settings,
ficsitcli.ServerPicker,
},
EnumBind: []interface{}{
common.AllInstallTypes,
Expand Down

0 comments on commit e97a648

Please sign in to comment.