Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial UI prototype #33

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build/
data/
node_modules/

20 changes: 20 additions & 0 deletions ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# README

This is an early-stage WIP prototype to test out the tech, make sure the various pieces work together, etc. Nothing is final yet :grin:

To build:
* Install Go 1.16
* Install [Wails](https://github.com/wailsapp/wails/) and dependencies

Then, run:
```console
$ wails build -t src/wails.d.ts -d
```

This will install all of the NPM dependencies, build and bundle the Typescript code, build the Go code, and generate a binary in `./build`. To launch, run:
```console
$ ./build/games-on-whales
```

I have only tried this on Linux. It will probably not work on Windows because the Webview component is based on IE11 and doesn't support required features.

43 changes: 43 additions & 0 deletions ui/data/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"containers": [
{
"id": "retroarch-1",
"name": "RetroArch",
"icon": "http://localhost:8081/retroarch-icon.png",
"banner": "http://localhost:8081/retroarch-banner.png",
"summary": "Cross-platform, sophisticated frontend for the libretro API",
"description": "A long description goes here"
},
{
"id": "steam-1",
"name": "Steam",
"icon": "http://localhost:8081/steam-icon.png",
"banner": "http://localhost:8081/steam-banner.png",
"summary": "Steam is steam.",
"description": "A long description goes here"
},
{
"id": "firefox-1",
"name": "Firefox",
"icon": "http://localhost:8081/firefox-icon.png",
"banner": "http://localhost:8081/firefox-banner.png",
"summary": "Browse the web with Firefox",
"description": "A long description goes here"
}
],
"featured": [
"firefox-1",
"steam-1",
"retroarch-1"
],
"lists": [
{
"name": "Popular",
"contents": [ "steam-1" ]
},
{
"name": "Another List",
"contents": [ "firefox-1", "retroarch-1" ]
}
]
}
228 changes: 228 additions & 0 deletions ui/docker/containers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package docker

import (
"context"
"encoding/json"
"errors"
"io"
"net/http"

dockerTypes "github.com/docker/docker/api/types"
dockerFilters "github.com/docker/docker/api/types/filters"
docker "github.com/docker/docker/client"
"github.com/wailsapp/wails"
"github.com/wailsapp/wails/lib/logger"
)

type cleanupFunc func() error

type ContainerList struct {
Name string
Contents []string
}

type Container struct {
Name string
Id string
Icon string
Banner string
Summary string
Description string
}

type Catalog struct {
Containers []Container
Featured []string
Lists []ContainerList
}

type ExpandedContainerList struct {
Name string
Contents []Container
}

type installedStoreType map[string]Container

type containerStore struct {
Installed installedStoreType
Featured []Container
Lists []ExpandedContainerList
Available map[string]Container
}

type Containers struct {
log *logger.CustomLogger
store *wails.Store

client *docker.Client

catalog Catalog
cleanupFuncs []cleanupFunc
}

func (c *Containers) WailsInit(runtime *wails.Runtime) error {
c.log = runtime.Log.New("Containers")

c.store = runtime.Store.New("Containers", containerStore{
Available: make(map[string]Container),
Installed: make(installedStoreType),
Featured: []Container{},
Lists: []ExpandedContainerList{},
})

c.log.Debug("Creating docker client...")
cli, err := docker.NewClientWithOpts(docker.FromEnv)
if err == nil {
c.client = cli

runtime.Events.Once("frontend-ready", func(_ ...interface{}) {
c.log.Debug("frontend ready; about to load containers")
go c.loadContainerList()
go c.loadCatalog()
// go c.watchDockerContainers()
})
}

return nil
}

func (c *Containers) WailsShutdown() {
for _, f := range c.cleanupFuncs {
defer f()
}
}

// TODO: consider if using a store for catalog data is really the best way.
// One issue is that Subscribe()-ing to a store after data has already been
// added to it does _not_ call the subscriber with that data, and if the data
// is loaded on WailsInit() it will always be loaded before the frontend is up
// and running, so there always has to be some way for the frontend to manually
// trigger a dummy update

func (c *Containers) loadCatalog() error {
res, err := http.Get("http://localhost:8081/catalog.json")
if err != nil {
return err
}
defer res.Body.Close()

body, err := io.ReadAll(res.Body)
if err != nil {
return err
}

json.Unmarshal([]byte(body), &c.catalog)

c.log.Debugf("Got catalog: %v", c.catalog)

c.store.Update(func (data containerStore) containerStore {
// first, update the "available" map
for _, item := range c.catalog.Containers {
data.Available[item.Id] = item
}

// then, expand the lists. go with the simple naive algorithm for now
data.Lists = make([]ExpandedContainerList, len(c.catalog.Lists))
for idx, list := range c.catalog.Lists {
data.Lists[idx] = ExpandedContainerList{
Name: list.Name,
Contents: make([]Container, len(list.Contents)),
}

for itemIdx, itemId := range list.Contents {
data.Lists[idx].Contents[itemIdx] = data.Available[itemId]
}
}

// finally, the featured items
data.Featured = make([]Container, len(c.catalog.Featured))
for idx, itemId := range c.catalog.Featured {
data.Featured[idx] = data.Available[itemId]
}

return data
})

return nil
}

func (c *Containers) loadContainerList() error {
c.log.Debugf("listing installed containers\n")
if c.client != nil {
filters := dockerFilters.NewArgs(dockerFilters.Arg("label", "io.github.games-on-whales.type"))

containers, err := c.client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{ All: true, Filters: filters })
if err != nil {
return err
}

c.log.Debugf("Found %d containers", len(containers))

for _, ctr := range containers {
// TODO: it's probably not the right thing to do to use docker's ID
// here, since they won't match up with the catalog. maybe the
// catalog id should be added as a label when the container is
// created?
container := Container{ Name: ctr.Names[0], Id: ctr.ID }
c.store.Update(func (data containerStore) containerStore {
c.log.Debugf("Updating with a container: %s", ctr.ID)

data.Installed[ctr.ID] = container
return data
})
}
} else {
c.log.Debug("no docker client; can't list containers")
}

return nil
}

func (c *Containers) watchDockerContainers() {
ctx, cancelFunc := context.WithCancel(context.Background())
c.cleanupFuncs = append(c.cleanupFuncs, func() error { cancelFunc(); return nil })

msgs, errs := c.client.Events(ctx, dockerTypes.EventsOptions{})

for {
select {
case err := <-errs: {
if err != nil && !errors.Is(err, context.Canceled) {
c.log.Errorf("error receiving docker event: %v", err)
}
}

case msg := <-msgs: {
c.log.Infof("docker message: %s", msg)
}
}
}
}

// TODO: this is a total hack just so i can see something on the screen tonight
func (c *Containers) TriggerStoreUpdate() error {
c.store.Update(func (data containerStore) containerStore {
return data
})
return nil
}

func (c *Containers) StartContainer(ctr Container) error {
c.log.Debugf("starting container %s\n", ctr.Name)
return nil
}

func (c *Containers) StopContainer(ctr Container) error {
c.log.Debugf("stopping container %s\n", ctr.Name)
return nil
}

func (c *Containers) InstallContainer(ctr Container) error {
c.log.Debugf("installing container %s\n", ctr.Name)
return nil
}

func (c *Containers) RemoveContainer(ctr Container) error {
c.log.Debugf("removing container %s\n", ctr.Name)
return nil
}
4 changes: 4 additions & 0 deletions ui/frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
node_modules*/
/build
/*.log
68 changes: 68 additions & 0 deletions ui/frontend/assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package frontend

import (
"embed"
"encoding/base64"
"fmt"
"mime"
"path/filepath"
"strings"

"github.com/wailsapp/wails"
"github.com/wailsapp/wails/lib/logger"
)

//go:embed build/*
var files embed.FS

type Assets struct {
log *logger.CustomLogger
}

func (assets *Assets) WailsInit(runtime *wails.Runtime) error {
assets.log = runtime.Log.New("Containers")
return nil
}

func (assets *Assets) GetNumbers() []int32 {
return []int32{1,2,3,4}
}

func (assets *Assets) GetBytes(filename string) ([]byte, error) {
if strings.HasPrefix(filename, "build") {
return files.ReadFile(filename)
} else {
return files.ReadFile(filepath.Join("build", filename))
}
}

func (assets *Assets) GetString(filename string) (string, error) {
data, err := assets.GetBytes(filename)
if err != nil {
return "", err
}

return string(data), nil
}

func (assets *Assets) GetDataUri(filename string) (string, error) {
data, err := assets.GetBytes(filename)
if err != nil {
assets.log.Errorf("There was an error getting bytes: %s", err)
return "", err
}

mimeType := mime.TypeByExtension(filepath.Ext(filename))
if mimeType == "" {
return "", fmt.Errorf("Couldn't get mime type for %s", filename)
}

encoded := base64.StdEncoding.EncodeToString(data)
if mimeType == "" {
return "", fmt.Errorf("Couldn't encode %s to base64", filename)
}

return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil
}


Loading