Browse Source
			
			
			Merge branch 'asmodeus' of git.aiterp.net:lucifer/new-server into asmodeus
			
			
				pull/1/head
			
			
		
		Merge branch 'asmodeus' of git.aiterp.net:lucifer/new-server into asmodeus
	
		
	
			
			
				pull/1/head
			
			
		
				 21 changed files with 366 additions and 12 deletions
			
			
		- 
					4app/api/bridges.go
 - 
					83app/api/presets.go
 - 
					15app/config/driver.go
 - 
					9app/config/repo.go
 - 
					1app/server.go
 - 
					13cmd/bridgetest/main.go
 - 
					2cmd/goose/main.go
 - 
					14internal/drivers/provider.go
 - 
					2internal/mysql/bridgerepo.go
 - 
					99internal/mysql/presetrepo.go
 - 
					2models/bridge.go
 - 
					26models/colorpreset.go
 - 
					1models/errors.go
 - 
					17scripts/20210523175111_color_preset.sql
 - 
					11scripts/20210523175425_color_preset_name.sql
 - 
					2webui/package.json
 - 
					7webui/src/App.tsx
 - 
					43webui/src/primitives/Forms.tsx
 - 
					2webui/src/primitives/Layout.tsx
 - 
					2webui/src/res/colors.sass
 - 
					23webui/yarn.lock
 
@ -0,0 +1,83 @@ | 
				
			|||
package api | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"git.aiterp.net/lucifer/new-server/app/config" | 
				
			|||
	"git.aiterp.net/lucifer/new-server/models" | 
				
			|||
	"github.com/gin-gonic/gin" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
func ColorPresets(r gin.IRoutes) { | 
				
			|||
	r.GET("", handler(func(c *gin.Context) (interface{}, error) { | 
				
			|||
		return config.ColorPresetRepository().FetchAll(ctxOf(c)) | 
				
			|||
	})) | 
				
			|||
 | 
				
			|||
	r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) { | 
				
			|||
		return config.ColorPresetRepository().Find(ctxOf(c), intParam(c, "id")) | 
				
			|||
	})) | 
				
			|||
 | 
				
			|||
	r.POST("", handler(func(c *gin.Context) (interface{}, error) { | 
				
			|||
		var body struct { | 
				
			|||
			Name        string `json:"name"` | 
				
			|||
			ColorString string `json:"colorString"` | 
				
			|||
		} | 
				
			|||
		err := parseBody(c, &body) | 
				
			|||
		if err != nil { | 
				
			|||
			return nil, err | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		newColor, err := models.ParseColorValue(body.ColorString) | 
				
			|||
		if err != nil { | 
				
			|||
			return nil, err | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		preset := models.ColorPreset{ | 
				
			|||
			Name: body.Name, | 
				
			|||
			Value: newColor, | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		preset.Validate() | 
				
			|||
		err = config.ColorPresetRepository().Save(ctxOf(c), &preset) | 
				
			|||
		if err != nil { | 
				
			|||
			return nil, err | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		return preset, nil | 
				
			|||
	})) | 
				
			|||
 | 
				
			|||
	r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) { | 
				
			|||
		var body struct { | 
				
			|||
			Name        *string `json:"name"` | 
				
			|||
			ColorString *string `json:"colorString"` | 
				
			|||
		} | 
				
			|||
		err := parseBody(c, &body) | 
				
			|||
		if err != nil { | 
				
			|||
			return nil, err | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		preset, err := config.ColorPresetRepository().Find(ctxOf(c), intParam(c, "id")) | 
				
			|||
		if err != nil { | 
				
			|||
			return nil, err | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		if body.Name != nil { | 
				
			|||
			preset.Name = *body.Name | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		if body.ColorString != nil { | 
				
			|||
			newColor, err := models.ParseColorValue(*body.ColorString) | 
				
			|||
			if err != nil { | 
				
			|||
				return nil, err | 
				
			|||
			} | 
				
			|||
 | 
				
			|||
			preset.Value = newColor | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		preset.Validate() | 
				
			|||
		err = config.ColorPresetRepository().Save(ctxOf(c), &preset) | 
				
			|||
		if err != nil { | 
				
			|||
			return nil, err | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		return preset, nil | 
				
			|||
	})) | 
				
			|||
} | 
				
			|||
@ -1,12 +1,23 @@ | 
				
			|||
package config | 
				
			|||
 | 
				
			|||
import "git.aiterp.net/lucifer/new-server/models" | 
				
			|||
import ( | 
				
			|||
	"git.aiterp.net/lucifer/new-server/internal/drivers" | 
				
			|||
	"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf" | 
				
			|||
	"git.aiterp.net/lucifer/new-server/models" | 
				
			|||
	"sync" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
var dp models.DriverProvider | 
				
			|||
var dpMutex sync.Mutex | 
				
			|||
 | 
				
			|||
func DriverProvider() models.DriverProvider { | 
				
			|||
	dpMutex.Lock() | 
				
			|||
	defer dpMutex.Unlock() | 
				
			|||
 | 
				
			|||
	if dp == nil { | 
				
			|||
		panic("not implemented yet") | 
				
			|||
		dp = drivers.DriverMap{ | 
				
			|||
			models.DTNanoLeaf: &nanoleaf.Driver{}, | 
				
			|||
		} | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return dp | 
				
			|||
 | 
				
			|||
@ -0,0 +1,14 @@ | 
				
			|||
package drivers | 
				
			|||
 | 
				
			|||
import "git.aiterp.net/lucifer/new-server/models" | 
				
			|||
 | 
				
			|||
type DriverMap map[models.DriverKind]models.Driver | 
				
			|||
 | 
				
			|||
func (m DriverMap) Provide(kind models.DriverKind) (models.Driver, error) { | 
				
			|||
	if m[kind] == nil { | 
				
			|||
		return nil, models.ErrUnknownDriver | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return m[kind], nil | 
				
			|||
} | 
				
			|||
 | 
				
			|||
@ -0,0 +1,99 @@ | 
				
			|||
package mysql | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"context" | 
				
			|||
	"git.aiterp.net/lucifer/new-server/models" | 
				
			|||
	"github.com/jmoiron/sqlx" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
type presetRecord struct { | 
				
			|||
	ID         int     `db:"id"` | 
				
			|||
	Name       string  `db:"name"` | 
				
			|||
	Hue        float64 `db:"hue"` | 
				
			|||
	Saturation float64 `db:"saturation"` | 
				
			|||
	Kelvin     int     `db:"kelvin"` | 
				
			|||
} | 
				
			|||
 | 
				
			|||
type ColorPresetRepo struct { | 
				
			|||
	DBX *sqlx.DB | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (c *ColorPresetRepo) Find(ctx context.Context, id int) (models.ColorPreset, error) { | 
				
			|||
	var record presetRecord | 
				
			|||
	err := c.DBX.GetContext(ctx, &record, "SELECT * FROM color_preset WHERE id = ?", id) | 
				
			|||
	if err != nil { | 
				
			|||
		return models.ColorPreset{}, dbErr(err) | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return c.fromRecords(record)[0], nil | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (c *ColorPresetRepo) FetchAll(ctx context.Context) ([]models.ColorPreset, error) { | 
				
			|||
	records := make([]presetRecord, 0, 16) | 
				
			|||
	err := c.DBX.SelectContext(ctx, &records, "SELECT * FROM color_preset") | 
				
			|||
	if err != nil { | 
				
			|||
		return nil, dbErr(err) | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return c.fromRecords(records...), nil | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (c *ColorPresetRepo) Save(ctx context.Context, preset *models.ColorPreset) error { | 
				
			|||
	if preset.ID > 0 { | 
				
			|||
		_, err := c.DBX.ExecContext( | 
				
			|||
			ctx, | 
				
			|||
			"UPDATE color_preset SET name = ?, hue = ?, saturation = ?, kelvin = ? WHERE id = ?", | 
				
			|||
			preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin, preset.ID, | 
				
			|||
		) | 
				
			|||
 | 
				
			|||
		if err != nil { | 
				
			|||
			return dbErr(err) | 
				
			|||
		} | 
				
			|||
	} else { | 
				
			|||
		rs, err := c.DBX.ExecContext( | 
				
			|||
			ctx, | 
				
			|||
			"INSERT INTO color_preset (name, hue, saturation, kelvin) VALUES (?, ?, ?, ?)", | 
				
			|||
			preset.Name, preset.Value.Hue, preset.Value.Saturation, preset.Value.Kelvin, | 
				
			|||
		) | 
				
			|||
 | 
				
			|||
		if err != nil { | 
				
			|||
			return dbErr(err) | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		id, err := rs.LastInsertId() | 
				
			|||
		if err != nil { | 
				
			|||
			return dbErr(err) | 
				
			|||
		} | 
				
			|||
 | 
				
			|||
		preset.ID = int(id) | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return nil | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (c *ColorPresetRepo) Delete(ctx context.Context, preset *models.ColorPreset) error { | 
				
			|||
	_, err := c.DBX.ExecContext(ctx, "DELETE FROM color_preset WHERE id = ?", preset.ID) | 
				
			|||
	if err != nil { | 
				
			|||
		return dbErr(err) | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	preset.ID = 0 | 
				
			|||
	return nil | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (c *ColorPresetRepo) fromRecords(records ...presetRecord) []models.ColorPreset { | 
				
			|||
	newList := make([]models.ColorPreset, len(records), len(records)) | 
				
			|||
	for i, record := range records { | 
				
			|||
		newList[i] = models.ColorPreset{ | 
				
			|||
			ID:   record.ID, | 
				
			|||
			Name: record.Name, | 
				
			|||
			Value: models.ColorValue{ | 
				
			|||
				Hue:        record.Hue, | 
				
			|||
				Saturation: record.Saturation, | 
				
			|||
				Kelvin:     record.Kelvin, | 
				
			|||
			}, | 
				
			|||
		} | 
				
			|||
	} | 
				
			|||
 | 
				
			|||
	return newList | 
				
			|||
} | 
				
			|||
@ -0,0 +1,26 @@ | 
				
			|||
package models | 
				
			|||
 | 
				
			|||
import ( | 
				
			|||
	"context" | 
				
			|||
	"strings" | 
				
			|||
) | 
				
			|||
 | 
				
			|||
type ColorPreset struct { | 
				
			|||
	ID    int        `json:"id"` | 
				
			|||
	Name  string     `json:"name"` | 
				
			|||
	Value ColorValue `json:"value"` | 
				
			|||
} | 
				
			|||
 | 
				
			|||
type ColorPresetRepository interface { | 
				
			|||
	Find(ctx context.Context, id int) (ColorPreset, error) | 
				
			|||
	FetchAll(ctx context.Context) ([]ColorPreset, error) | 
				
			|||
	Save(ctx context.Context, preset *ColorPreset) error | 
				
			|||
	Delete(ctx context.Context, preset *ColorPreset) error | 
				
			|||
} | 
				
			|||
 | 
				
			|||
func (c *ColorPreset) Validate() { | 
				
			|||
	c.Name = strings.Trim(c.Name, " \t\n") | 
				
			|||
	if len(c.Name) == 0 { | 
				
			|||
		c.Name = c.Value.String() | 
				
			|||
	} | 
				
			|||
} | 
				
			|||
@ -0,0 +1,17 @@ | 
				
			|||
-- +goose Up | 
				
			|||
-- +goose StatementBegin | 
				
			|||
CREATE TABLE color_preset | 
				
			|||
( | 
				
			|||
    id         INT    NOT NULL AUTO_INCREMENT, | 
				
			|||
    hue        DOUBLE NOT NULL, | 
				
			|||
    saturation DOUBLE NOT NULL, | 
				
			|||
    kelvin     INT    NOT NULL, | 
				
			|||
 | 
				
			|||
    PRIMARY KEY (id) | 
				
			|||
); | 
				
			|||
-- +goose StatementEnd | 
				
			|||
 | 
				
			|||
-- +goose Down | 
				
			|||
-- +goose StatementBegin | 
				
			|||
DROP TABLE color_preset; | 
				
			|||
-- +goose StatementEnd | 
				
			|||
@ -0,0 +1,11 @@ | 
				
			|||
-- +goose Up | 
				
			|||
-- +goose StatementBegin | 
				
			|||
ALTER TABLE color_preset | 
				
			|||
    ADD COLUMN name VARCHAR(255) NOT NULL DEFAULT 'Unnamed'; | 
				
			|||
-- +goose StatementEnd | 
				
			|||
 | 
				
			|||
-- +goose Down | 
				
			|||
-- +goose StatementBegin | 
				
			|||
ALTER TABLE color_preset | 
				
			|||
    DROP COLUMN name; | 
				
			|||
-- +goose StatementEnd | 
				
			|||
@ -0,0 +1,43 @@ | 
				
			|||
import React, {useLayoutEffect, useState} from 'react'; | 
				
			|||
// @ts-ignore
 | 
				
			|||
import iro from "@jaames/iro"; | 
				
			|||
 | 
				
			|||
interface ColorPickerProps { | 
				
			|||
  h: number | 
				
			|||
  s: number | 
				
			|||
  onChange: (h: number, v: number) => void | 
				
			|||
} | 
				
			|||
 | 
				
			|||
const randomId = () => Math.floor(Math.random() * 100000); | 
				
			|||
 | 
				
			|||
export const HSColorPicker: React.FC<ColorPickerProps> = ({h, s, onChange}) => { | 
				
			|||
  const [random] = useState(() => `color-picker-${randomId()}`); | 
				
			|||
 | 
				
			|||
  useLayoutEffect(() => { | 
				
			|||
    // @ts-ignore
 | 
				
			|||
    const colorPicker = new iro.ColorPicker(`#${random}`, { | 
				
			|||
      color: {h, s: s * 100, v: 255}, | 
				
			|||
      layout: [ | 
				
			|||
        { | 
				
			|||
          component: iro.ui.Wheel, | 
				
			|||
          options: {} | 
				
			|||
        } | 
				
			|||
      ], | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    colorPicker.on("input:end", (color: { hsv: { h: number, s: number } }) => { | 
				
			|||
      onChange(color.hsv.h || 0, (color.hsv.s || 0) / 100); | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    return () => { | 
				
			|||
      const elem = document.getElementById(`color-picker-${random}`); | 
				
			|||
      if (elem === null) { | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      elem.innerHTML = ""; | 
				
			|||
    }; | 
				
			|||
  }, [h, s, onChange, random]); | 
				
			|||
 | 
				
			|||
  return <div id={random} style={{margin: "0 auto"}}/>; | 
				
			|||
}; | 
				
			|||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue