diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca5750e --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/shells-go +/shells-go.exe +/dist +/contrib/ios-password.txt +.*.swp +.DS_Store +/installer/win64/Shells.exe +.idea +/installer/win64/ShellsSetup.exe diff --git a/Icon.png b/Icon.png new file mode 100644 index 0000000..507101c Binary files /dev/null and b/Icon.png differ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9fe8fe1 --- /dev/null +++ b/Makefile @@ -0,0 +1,143 @@ +#!/bin/make + +GOROOT:=$(shell PATH="/pkg/main/dev-lang.go/bin:$$PATH" go env GOROOT) +GO_TAG:=$(shell /bin/sh -c 'eval `$(GOROOT)/bin/go tool dist env`; echo "$${GOOS}_$${GOARCH}"') +GIT_TAG:=$(shell git rev-parse --short HEAD) +GIT_VERS_FULL:=$(shell git 2>/dev/null describe --tags --match 'v*' HEAD || echo "v0.0.0") +GIT_BUILD:=$(shell echo "$(GIT_VERS_FULL)" | sed -e 's/[^-]*-//;s/-.*//;s/v.*/0/') +GIT_VERS:=$(shell echo "$(GIT_VERS_FULL)" | sed -e 's/^v//;s/-.*//') +GOPATH:=$(shell $(GOROOT)/bin/go env GOPATH) +SOURCES:=$(shell find . -name '*.go') +AWS:=$(shell which 2>/dev/null aws) +ifeq ($(DATE_TAG),) +DATE_TAG:=$(shell date '+%Y%m%d%H%M%S') +endif +export DATE_TAG +export GO111MODULE=on + +# do we have a defined target arch? +ifneq ($(TARGET_ARCH),) +TARGET_ARCH_SPACE:=$(subst _, ,$(TARGET_ARCH)) +TARGET_GOOS=$(word 1,$(TARGET_ARCH_SPACE)) +TARGET_GOARCH=$(word 2,$(TARGET_ARCH_SPACE)) +endif + +-include contrib/config.mak + +# variables that should be set in contrib/config.mak +ifeq ($(DIST_ARCHS),) +DIST_ARCHS=linux_amd64 linux_386 linux_arm linux_arm64 linux_ppc64 linux_ppc64le darwin_amd64 darwin_386 freebsd_386 freebsd_amd64 freebsd_arm windows_386 windows_amd64 +endif +ifeq ($(PROJECT_NAME),) +PROJECT_NAME:=$(shell basename `pwd`) +endif +ifeq ($(GOOS),windows) +EXT=.exe +endif + +.PHONY: all deps update fmt test check doc dist update-make gen cov ios ios-deploy + +all: $(PROJECT_NAME)$(EXT) + +$(PROJECT_NAME)$(EXT): $(SOURCES) + $(GOPATH)/bin/goimports -w -l . + $(GOROOT)/bin/go build -v -gcflags="-N -l" -ldflags=all="-X github.com/TrisTech/goupd.PROJECT_NAME=$(PROJECT_NAME) -X github.com/TrisTech/goupd.MODE=DEV -X github.com/TrisTech/goupd.GIT_TAG=$(GIT_TAG) -X github.com/TrisTech/goupd.DATE_TAG=$(DATE_TAG) $(GOLDFLAGS)" + +clean: + $(GOROOT)/bin/go clean + +deps: + $(GOROOT)/bin/go get -v . + +update: + $(GOROOT)/bin/go get -u . + +fmt: + $(GOROOT)/bin/go fmt ./... + $(GOPATH)/bin/goimports -w -l . + +test: + $(GOROOT)/bin/go test ./... + +gen: + $(GOROOT)/bin/go generate + +cov: + $(GOROOT)/bin/go test -coverprofile=coverage.out ./... + $(GOROOT)/bin/go tool cover -html=coverage.out -o coverage.html + +check: + @if [ ! -f $(GOPATH)/bin/gometalinter ]; then go get github.com/alecthomas/gometalinter; fi + $(GOPATH)/bin/gometalinter ./... + +doc: + @if [ ! -f $(GOPATH)/bin/godoc ]; then go get golang.org/x/tools/cmd/godoc; fi + $(GOPATH)/bin/godoc -v -http=:6060 -index -play + +dist: + @mkdir -p dist/$(PROJECT_NAME)_$(GIT_TAG)/upload + @make -s $(patsubst %,dist/$(PROJECT_NAME)_$(GIT_TAG)/upload/$(PROJECT_NAME)_%.bz2,$(DIST_ARCHS)) +ifneq ($(AWS),) + @echo "Uploading ..." + @aws s3 cp --cache-control 'max-age=31536000' --recursive "dist/$(PROJECT_NAME)_$(GIT_TAG)/upload" "s3://dist-go/$(PROJECT_NAME)/$(PROJECT_NAME)_$(DATE_TAG)_$(GIT_TAG)/" + @echo "Configuring dist repository" + @echo "$(DIST_ARCHS)" | aws s3 cp --cache-control 'max-age=31536000' --content-type 'text/plain' - "s3://dist-go/$(PROJECT_NAME)/$(PROJECT_NAME)_$(DATE_TAG)_$(GIT_TAG).arch" + @echo "$(DATE_TAG) $(GIT_TAG) $(PROJECT_NAME)_$(DATE_TAG)_$(GIT_TAG)" | aws s3 cp --cache-control 'max-age=60' --content-type 'text/plain' - "s3://dist-go/$(PROJECT_NAME)/LATEST" + @echo "Sending to production complete!" +ifneq ($(NOTIFY),) + @echo "Sending notify..." + @curl -s "$(NOTIFY)" +endif +endif + +dist/$(PROJECT_NAME)_$(GIT_TAG)/upload/$(PROJECT_NAME)_%.bz2: dist/$(PROJECT_NAME)_$(GIT_TAG)/$(PROJECT_NAME).% + @echo "Generating $@" + @bzip2 --stdout --compress --keep -9 "$<" >"$@" + +dist/$(PROJECT_NAME)_$(GIT_TAG): + @mkdir "$@" + +dist/$(PROJECT_NAME)_$(GIT_TAG)/$(PROJECT_NAME).%: $(SOURCES) + @echo " * Building $(PROJECT_NAME) for $*" + @TARGET_ARCH="$*" make -s dist/$(PROJECT_NAME)_$(GIT_TAG)/build_$(PROJECT_NAME).$* + @mv 'dist/$(PROJECT_NAME)_$(GIT_TAG)/build_$(PROJECT_NAME).$*' 'dist/$(PROJECT_NAME)_$(GIT_TAG)/$(PROJECT_NAME).$*' + +ifneq ($(TARGET_ARCH),) +dist/$(PROJECT_NAME)_$(GIT_TAG)/build_$(PROJECT_NAME).$(TARGET_ARCH): $(SOURCES) + @GOOS="$(TARGET_GOOS)" GOARCH="$(TARGET_GOARCH)" $(GOROOT)/bin/go build -a -o "$@" -gcflags="-N -l -trimpath=$(shell pwd)" -ldflags=all="-s -w -X github.com/TrisTech/goupd.PROJECT_NAME=$(PROJECT_NAME) -X github.com/TrisTech/goupd.MODE=PROD -X github.com/TrisTech/goupd.GIT_TAG=$(GIT_TAG) -X github.com/TrisTech/goupd.DATE_TAG=$(DATE_TAG) $(GOLDFLAGS)" +endif + +update-make: + @echo "Updating Makefile ..." + @curl -s "https://raw.githubusercontent.com/TrisTech/make-go/master/Makefile" >Makefile.upd + @mv -f "Makefile.upd" "Makefile" + +$(GOPATH)/bin/fyne: + go get fyne.io/fyne/cmd/fyne + +$(GOPATH)/bin/gomobile: + go get golang.org/x/mobile/cmd/gomobile + +android: $(GOPATH)/bin/fyne + "$(GOPATH)/bin/fyne" release -os android -keyStore ~/.secure/key.jks -appID "com.shells.app" -appVersion $(GIT_VERS) -appBuild $(GIT_BUILD) + +ios: $(GOPATH)/bin/fyne + "$(GOPATH)/bin/fyne" release -os ios -certificate "Apple Distribution" -profile "My App Distribution" -appID "com.shells.app" -appVersion $(GIT_VERS) -appBuild $(GIT_BUILD) + #"$(GOPATH)/bin/gomobile" build -target=ios -bundleid=com.shells.app + +darwin: $(GOPATH)/bin/fyne + rm -fr shells-go.app build + "$(GOPATH)/bin/fyne" package -release -os darwin -appID "com.shells.app" -appVersion $(GIT_VERS) -appBuild $(GIT_BUILD) + codesign -dvv --force --timestamp --sign 'Developer ID Application: E Shells Inc. (VMDDRAJZ7W)' --options runtime shells-go.app + mkdir build + mv shells-go.app build/Shells.app + pkgbuild --root build --identifier "com.shells.app" --version $(GIT_VERS) --install-location "/Applications" --sign 'Developer ID Installer: E Shells Inc. (VMDDRAJZ7W)' shells-release.pkg + xcrun altool --notarize-app --primary-bundle-id com.shells.app --username 'mark@shells.com' --password $(shell cat contrib/ios-password.txt) --file shells-release.pkg + @echo run: xcrun altool --notarization-info uuid --username 'mark@shells.com' --password $(shell cat contrib/ios-password.txt) + @echo run: xcrun stapler staple shells-release.pkg + +ios-deploy: + xcrun altool --upload-app --type ios --file Shells.ipa --username 'mark@shells.com' --password $(shell cat contrib/ios-password.txt) + +win64: + make GOOS=windows TARGET_GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..05e9a6c --- /dev/null +++ b/TODO.md @@ -0,0 +1,11 @@ +# Stuff todo + +* Widget controls +* audio recording support +* file transfer +* video streaming support +* Clipboard +* USB support +* Virtual keyboard +* Improve UI +* Add full screen button diff --git a/cgo.go b/cgo.go new file mode 100644 index 0000000..42256bd --- /dev/null +++ b/cgo.go @@ -0,0 +1,6 @@ +package main + +// Fix for windows builds? + +// #cgo windows LDFLAGS: -Wl,-Bstatic -lssp -Wl,-Bdynamic +import "C" diff --git a/const.go b/const.go new file mode 100644 index 0000000..83f86e5 --- /dev/null +++ b/const.go @@ -0,0 +1,4 @@ +package main + +const WWidth float32 = 415 +const WHeight float32 = 670 diff --git a/contrib/config.mak b/contrib/config.mak new file mode 100644 index 0000000..e8e2ce4 --- /dev/null +++ b/contrib/config.mak @@ -0,0 +1,4 @@ +ifeq ($(TARGET_GOOS),windows) +GOLDFLAGS+=-H=windowsgui +endif +DIST_ARCHS=linux_amd64 linux_arm64 linux_arm diff --git a/contrib/iOS_App_Store.mobileprovision b/contrib/iOS_App_Store.mobileprovision new file mode 100644 index 0000000..cbf57ab Binary files /dev/null and b/contrib/iOS_App_Store.mobileprovision differ diff --git a/fyne-enterentry.go b/fyne-enterentry.go new file mode 100644 index 0000000..c9f0bd8 --- /dev/null +++ b/fyne-enterentry.go @@ -0,0 +1,31 @@ +package main + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +type enterEntry struct { + widget.Entry + + onEnter func() +} + +func newEnterEntry() *enterEntry { + entry := &enterEntry{} + entry.ExtendBaseWidget(entry) + return entry +} + +func (e *enterEntry) TypedKey(key *fyne.KeyEvent) { + switch key.Name { + case fyne.KeyReturn: + if e.onEnter != nil { + e.onEnter() + return + } + fallthrough + default: + e.Entry.TypedKey(key) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1634256 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module github.com/Shells-com/shells-go + +go 1.16 + +require ( + fyne.io/fyne/v2 v2.0.3 + github.com/KarpelesLab/csscolor v0.0.1 // indirect + github.com/KarpelesLab/goclip v0.0.1 + github.com/KarpelesLab/rest v0.2.2 + github.com/KarpelesLab/static-opus v0.3.131 + github.com/KarpelesLab/static-portaudio v0.4.190600 + github.com/Shells-com/spice v0.0.2 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb + github.com/golang/snappy v0.0.2 + github.com/gordonklaus/portaudio v0.0.0-20200911161147-bb74aa485641 // indirect + github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 + github.com/stretchr/testify v1.7.0 + github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 + golang.org/x/image v0.0.0-20200430140353-33d19683fad8 + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + gopkg.in/go-playground/colors.v1 v1.2.0 // indirect +) + +replace fyne.io/fyne/v2 v2.0.3 => github.com/MagicalTux/fyne/v2 v2.0.4-0.20210517173427-d303af45520c diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..babc7a0 --- /dev/null +++ b/go.sum @@ -0,0 +1,116 @@ +github.com/KarpelesLab/csscolor v0.0.1 h1:vpIp8pz+xFNPIcnKQgoCCMtzocOEG3xZGoW09Mfe8KY= +github.com/KarpelesLab/csscolor v0.0.1/go.mod h1:8dxihntqWCpDm3JG41hB3lJWWiCTzkAATkQ+No7uYgk= +github.com/KarpelesLab/goclip v0.0.0-20210723144624-26a674fa926b h1:a1vnQ7d39tdcy/LMqjKrdgPxFf+qwOtW8R4XaaiZDFc= +github.com/KarpelesLab/goclip v0.0.0-20210723144624-26a674fa926b/go.mod h1:rS1UzyGATtRSEAJ6Dv/Ni3ochNVwPy5bIXciNKaEvUo= +github.com/KarpelesLab/goclip v0.0.1 h1:gH4f8hsaTECVMNrHSwBPd3B2MTm31F25oLWCrmgvjqQ= +github.com/KarpelesLab/goclip v0.0.1/go.mod h1:rS1UzyGATtRSEAJ6Dv/Ni3ochNVwPy5bIXciNKaEvUo= +github.com/KarpelesLab/rest v0.2.2 h1:cSj/WF3ffbW+dh61+TFCE6CY0IOOEgkY7VeQEr3pajY= +github.com/KarpelesLab/rest v0.2.2/go.mod h1:rVA9bURXRRZLN2ms3A5JmH+zpc9SNoUHNX5KFCemtlE= +github.com/KarpelesLab/static-opus v0.3.131 h1:nxbTIpPijjot5QmX+5n6C1NLQNxCIxCAuDqyyt0ivIk= +github.com/KarpelesLab/static-opus v0.3.131/go.mod h1:imuTrncTHBbXQoleH8j32Li//haNTz8N7S95RiTkP6g= +github.com/KarpelesLab/static-portaudio v0.4.190600 h1:y+wZ2jHuxklHUzjet9lq3Tv/0g4j+Ghq1wTM1Qse6Zs= +github.com/KarpelesLab/static-portaudio v0.4.190600/go.mod h1:gJo/PfziOxbetHg+KzbgFMZeKy57tGRrsuwqfOtQ5/c= +github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= +github.com/MagicalTux/fyne/v2 v2.0.4-0.20210517173427-d303af45520c h1:bO38sQEbdZ+T96EWD1WKZI9JGfrDspfbR/h+PBNNsRo= +github.com/MagicalTux/fyne/v2 v2.0.4-0.20210517173427-d303af45520c/go.mod h1:nNpgL7sZkDVLraGtQII2ArNRnnl6kHup/KfQRxIhbvs= +github.com/Shells-com/spice v0.0.1 h1:xktJip+6QnpkAefckjmzeACwHlhgkbGQ4+Gr2eA3XCM= +github.com/Shells-com/spice v0.0.1/go.mod h1:g+j8FZ447fQ87m6NDi8vULcIeDX+YPLnnuEhvxlEK6Q= +github.com/Shells-com/spice v0.0.2 h1:ziaVVCQlo0gt6nGQERJ7E6ZWpMnLJL3Tld2BIYW6Vp0= +github.com/Shells-com/spice v0.0.2/go.mod h1:UseQUmSdSNUZD9pnW4AKZI8xTzWy05ZZvGDPvw2eu54= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fyne-io/mobile v0.1.1 h1:Snu9tKaVgu81314egPeqMC09z/k4D/bts0n1O2MfPbk= +github.com/fyne-io/mobile v0.1.1/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= +github.com/fyne-io/mobile v0.1.3-0.20210412090810-650a3139866a h1:3TAJhl8vXyli0tooKB0vd6gLCyBdWL4QEYbDoJpHEZk= +github.com/fyne-io/mobile v0.1.3-0.20210412090810-650a3139866a/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gordonklaus/portaudio v0.0.0-20200911161147-bb74aa485641 h1:B7ADnac3Yy6Vtcp2mstnsjUtarYcjy4AL0R6eNEhZAk= +github.com/gordonklaus/portaudio v0.0.0-20200911161147-bb74aa485641/go.mod h1:HfYnZi/ARQKG0dwH5HNDmPCHdLiFiBf+SI7DbhW7et4= +github.com/hraban/opus v0.0.0-20210415224706-ab1467d63813 h1:kJ//kFxpLIkSAqPzCwrEFWXKxnR9/TqL2+ViyBeLKJc= +github.com/hraban/opus v0.0.0-20210415224706-ab1467d63813/go.mod h1:YQQXrWHN3JEvCtw5ImyTCcPeU/ZLo/YMA+TpB64XdrU= +github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= +github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 h1:uxE3GYdXIOfhMv3unJKETJEhw78gvzuQqRX/rVirc2A= +github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/colors.v1 v1.2.0 h1:SPweMUve+ywPrfwao+UvfD5Ah78aOLUkT5RlJiZn52c= +gopkg.in/go-playground/colors.v1 v1.2.0/go.mod h1:AvbqcMpNXVl5gBrM20jBm3VjjKBbH/kI5UnqjU7lxFI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/installer/win64/README.md b/installer/win64/README.md new file mode 100644 index 0000000..2c9712d --- /dev/null +++ b/installer/win64/README.md @@ -0,0 +1,4 @@ +. copy your .exe build outpout into this folder and rename it to Shells.exe +. sign it +. launch the wix script +. Sign the ShellsSetup.exe output \ No newline at end of file diff --git a/installer/win64/installer.nsi b/installer/win64/installer.nsi new file mode 100644 index 0000000..717ed5b --- /dev/null +++ b/installer/win64/installer.nsi @@ -0,0 +1,123 @@ +Unicode True +;-------------------------------- +;Include Modern UI + + !include "MUI2.nsh" + +;-------------------------------- +; Start + + + !define MUI_PRODUCT "Shells" + !define MUI_FILE "Shells" + !define MUI_VERSION "" + + +;--------------------------------- +;General +!define MUI_ICON "../../res/icon.ico" + + ;Name and file + Name "Shells" + OutFile "ShellsSetup.exe" + ShowInstDetails "nevershow" + ShowUninstDetails "nevershow" + + + ;Default installation folder + InstallDir "$PROGRAMFILES64\Shells\Client" + + ;Get installation folder from registry if available + InstallDirRegKey HKCU "Software\Shells\Client" "" + + RequestExecutionLevel admin + +;-------------------------------- +;Interface Settings + + !define MUI_WELCOMEPAGE + !define MUI_LICENSEPAGE + !define MUI_DIRECTORYPAGE + !define MUI_ABORTWARNING + !define MUI_UNINSTALLER + !define MUI_UNCONFIRMPAGE + !define MUI_FINISHPAGE_NOAUTOCLOSE + !define MUI_FINISHPAGE_RUN + !define MUI_FINISHPAGE_RUN_TEXT "Launch Shells" + !define MUI_FINISHPAGE_RUN_FUNCTION "LaunchShells" + +;-------------------------------- +;Pages + + !insertmacro MUI_PAGE_LICENSE "license.rtf" + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_INSTFILES + + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + !insertmacro MUI_PAGE_FINISH +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +;Installer Sections + +Section "Shells" Installation + SectionIn RO + SetOutPath "$INSTDIR" + + File "${MUI_FILE}.exe" + + ;create desktop shortcut + CreateShortCut "$DESKTOP\${MUI_PRODUCT}.lnk" "$INSTDIR\${MUI_FILE}.exe" "" + + ;create start-menu items + CreateDirectory "$SMPROGRAMS\${MUI_PRODUCT}" + CreateShortCut "$SMPROGRAMS\${MUI_PRODUCT}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\${MUI_PRODUCT}\${MUI_PRODUCT}.lnk" "$INSTDIR\${MUI_FILE}.exe" "" "$INSTDIR\${MUI_FILE}.exe" 0 + + + ;Store installation folder + WriteRegStr HKCU "Software\Shells\Client" "" $INSTDIR + +;write uninstall information to the registry + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" "DisplayName" "${MUI_PRODUCT} (remove only)" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" "UninstallString" "$INSTDIR\Uninstall.exe" + + WriteUninstaller "$INSTDIR\Uninstall.exe" + +SectionEnd + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + + ;ADD YOUR OWN FILES HERE... + + Delete "$INSTDIR\Uninstall.exe" + + ;Delete Files + RMDir /r "$INSTDIR\*.*" + + ;Remove the installation directory + RMDir "$INSTDIR" + + ;Delete Start Menu Shortcuts + Delete "$DESKTOP\${MUI_PRODUCT}.lnk" + Delete "$SMPROGRAMS\${MUI_PRODUCT}\*.*" + RMDir "$SMPROGRAMS\${MUI_PRODUCT}" + + ;Delete Uninstaller And Unistall Registry Entries + DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\${MUI_PRODUCT}" + DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${MUI_PRODUCT}" + + DeleteRegKey /ifempty HKCU "Software\Shells\Client" + +SectionEnd + +Function LaunchShells + ExecShell "" "$INSTDIR\${MUI_PRODUCT}.exe" +FunctionEnd diff --git a/installer/win64/license.rtf b/installer/win64/license.rtf new file mode 100644 index 0000000..df0f0cf --- /dev/null +++ b/installer/win64/license.rtf @@ -0,0 +1,527 @@ +{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff31507\deff0\stshfdbch31506\stshfloch31506\stshfhich31506\stshfbi31507\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f2\fbidi \fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New;}{\f2\fbidi \fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New;} +{\f39\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\f40\fbidi \fmodern\fcharset0\fprq1{\*\panose 020b0609020204030204}Consolas;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;} +{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;} +{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f61\fbidi \fmodern\fcharset238\fprq1 Courier New CE;}{\f62\fbidi \fmodern\fcharset204\fprq1 Courier New Cyr;} +{\f64\fbidi \fmodern\fcharset161\fprq1 Courier New Greek;}{\f65\fbidi \fmodern\fcharset162\fprq1 Courier New Tur;}{\f66\fbidi \fmodern\fcharset177\fprq1 Courier New (Hebrew);}{\f67\fbidi \fmodern\fcharset178\fprq1 Courier New (Arabic);} +{\f68\fbidi \fmodern\fcharset186\fprq1 Courier New Baltic;}{\f69\fbidi \fmodern\fcharset163\fprq1 Courier New (Vietnamese);}{\f61\fbidi \fmodern\fcharset238\fprq1 Courier New CE;}{\f62\fbidi \fmodern\fcharset204\fprq1 Courier New Cyr;} +{\f64\fbidi \fmodern\fcharset161\fprq1 Courier New Greek;}{\f65\fbidi \fmodern\fcharset162\fprq1 Courier New Tur;}{\f66\fbidi \fmodern\fcharset177\fprq1 Courier New (Hebrew);}{\f67\fbidi \fmodern\fcharset178\fprq1 Courier New (Arabic);} +{\f68\fbidi \fmodern\fcharset186\fprq1 Courier New Baltic;}{\f69\fbidi \fmodern\fcharset163\fprq1 Courier New (Vietnamese);}{\f431\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\f432\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;} +{\f434\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f435\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\f436\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\f437\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);} +{\f438\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\f439\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\f441\fbidi \fmodern\fcharset238\fprq1 Consolas CE;}{\f442\fbidi \fmodern\fcharset204\fprq1 Consolas Cyr;} +{\f444\fbidi \fmodern\fcharset161\fprq1 Consolas Greek;}{\f445\fbidi \fmodern\fcharset162\fprq1 Consolas Tur;}{\f448\fbidi \fmodern\fcharset186\fprq1 Consolas Baltic;}{\f449\fbidi \fmodern\fcharset163\fprq1 Consolas (Vietnamese);} +{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;} +{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);} +{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);} +{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;} +{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;} +{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\f41\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f42\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\f44\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f45\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\f46\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f47\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f48\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\f49\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0; +\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}{\*\defchp \f31506\fs22 }{\*\defpap +\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 +\af31507\afs22\alang1025 \ltrch\fcs0 \f31506\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \f31506\fs22\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext11 \ssemihidden \sunhideused Normal Table;}{ +\s15\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs21\alang1025 \ltrch\fcs0 \f40\fs21\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 +\sbasedon0 \snext15 \slink16 \sunhideused \styrsid5523255 Plain Text;}{\*\cs16 \additive \rtlch\fcs1 \af0\afs21 \ltrch\fcs0 \f40\fs21 \sbasedon10 \slink15 \slocked \styrsid5523255 Plain Text Char;}}{\*\rsidtbl \rsid5523255\rsid9534598\rsid16348483} +{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author word}{\operator word}{\creatim\yr2020\mo12\dy17\hr2\min53}{\revtim\yr2020\mo12\dy17\hr2\min53}{\version2} +{\edmins0}{\nofpages3}{\nofwords4527}{\nofchars25807}{\nofcharsws30274}{\vern99}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}\paperw12240\paperh15840\margl1501\margr1502\margt1440\margb1440\gutter0\ltrsect +\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors1\noxlattoyen +\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin360\dgvorigin360\dghshow1\dgvshow1 +\jexpand\viewscale100\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct\asianbrkrule +\rsidroot5523255\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0{\*\wgrffmtfilter 2450} +\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\endnhere\sectlinegrid360\sectdefaultcl\sectrsid5523255\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}} +{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (} +{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar +\s15\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid5523255 \rtlch\fcs1 \af31507\afs21\alang1025 \ltrch\fcs0 \f40\fs21\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af2 \ltrch\fcs0 +\f2\insrsid9534598\charrsid5523255 Please read these terms of service (the "Terms") carefully before using the website of E Shells, Inc. ("Shells\'99 +"). By accessing, browsing or using the website, you acknowledge that you have read, understood and agree to be bound by these Terms and to comply with all applicable laws and regulations, as well as the terms and conditions of Shells\'99 +' Privacy Policy found on this website, in addition to all amendments and modifications (collectively referred to as the "Agreement"). Your right to access and use the Shells\'99 + Website (defined below) is non-exclusive, non-transferable, non-sublicenseable, and fully revocable. If you do not agree to the Terms, you are not authorized to use the website. These Terms govern your use of the website, any content (such as tex +t, data, information, software, graphics or photographs) that Shells\'99 may make available through the website (collectively, "Materials") and any services that Shells\'99 + may provide through the website (collectively, "Services"). The website, Materials and Services are referred to in these terms collectively as the "Shells\'99 Website." +\par +\par You ("Client" or "Subscriber") (Shells\'99 and Client/Subscriber together known as "Parties") acknowledge that Shells\'99 nor any of its parent companies, constituents or affiliates +will be held liable for any and all liability arising from your use of the Shells\'99 Website. +\par +\par NOTE: THE TERMS CONTAIN A BINDING INDIVIDUAL ARBITRATION AND CLASS ACTION WAIVER PROVISION IN "BINDING INDIVIDUAL ARBITRATION" SECTION THAT AFFECTS YOUR RIGHTS WITH RESPECT TO ANY "DISPUTE" (AS DEFINED BELOW) BETWEEN YOU AND SHELLS\'99 +, ITS AFFILIATES, PARENTS OR SUBSIDIARIES (COLLECTIVELY, "SHELLS\'99 ENTITIES"). YOU HAVE A RIGHT TO OPT OUT OF THE BINDING ARBITRATION AND CLASS ACTION WAIVER PROVISIONS AS DESCRIBED IN "BINDING INDIVIDUAL ARBITRATION" SECTION. +\par +\par This is a legal agreement that includes clauses that gives you the right to settle a dispute out of court. +\par +\par Subscriber affirms that they are more than eighteen (18) years of age, a valid legal entity, or an ema +ncipated minor, or possess legal parental or guardian consent, and are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations, and warranties set forth in these Terms of Service, and to abide by and comply + +with all of these terms contained herein. If you are not an adult you must receive permission from a parent or legal guardian. If you are under the age of eighteen (18) years of age and between thirteen (13) and seventeen (17) years of age, then you must +f +ind a legal parent or guardian to purchase and activate this service for you. If you are unable to find a legal parent or guardian to purchase and activate this service for you or if you are under thirteen (13) years of age, you are not permitted to use t +his website or its services. +\par +\par +\par RESTRICTED USE +\par +\par Shells\'99 grants Subscriber a limited, revocable, non-exclusive license to subscribe to an account to which Subscriber has access for Subscriber\rquote s personal, private, commercial, non-transferable, limited use +s solely as set forth herein and as set forth in any additional documentation and/or agreements applicable to the Services accessed by Subscriber. All intellectual property rights on Shells\'99 + Website are owned by E Shells, Inc. and are protected by United S +tates and International copyright, trade dress, patent, and trademark laws, international conventions, and other laws protecting intellectual property and related proprietary rights. Subscriber may not copy or download any content from the Shells\'99 + Website unless expressly authorized to do so. Subscriber agrees not to remove, obscure, or alter copyright, patent, trademark, or other proprietary rights notices affixed to Shells\'99 Website content. Subscriber\rquote +s rights are subject to compliance with these Terms of Service as well as any other agreements applicable to Shells\'99. New or future services that may be offered by Shells\'99 + will require a separate subscription or agreement. You are buying a license that gives you the right to use our service. +\par +\par +\par CHANGES TO THIS AGREEMENT +\par +\par Client understands that the present Terms of Service are subject to changes made by Shells\'99 at any time at its sole discretion, and you agree to be bound by any and all modifications, changes and/or revisions. You understand that it is you +r obligation to periodically review this webpage in order to account for any changes made, as they will be binding upon assent. +\par +\par The terms and conditions of service herein apply to all users of Shells\'99 Website whether a 'visitor,' 'commercial user,' a 'subscriber,' or a 'client' and you are only authorized to use Shells\'99 + Website if you agree to abide by all applicable federal and state laws and be legally bound by all of the terms of this Agreement. +\par +\par +\par CONDUCT +\par +\par You agree to comply with all applicable laws and regulations in connection with use of this service. You must also agree that you and any other user that you have provided access to will not engage in any of the following activities: +\par +\par Sending or receiving unsolicited and/or commercial emails in violation of law, promotional materials, "junk mail," "spam," "chain letters," or "pyramid schemes"; +\par +\par Exploiting, possessing, producing, receiving, transporting, or distributing any illegal content, including but not limited to any sexually explicit depiction of children; +\par +\par Uploading, possessing, receiving, transporting, or distributing any copyrighted, trademark, or patented content which you do not own or lack written consent or a license from the copyright owner (we will respond to takedown notices +and removed any infringing content if that content is a copyright infringement); +\par +\par Forging headers or otherwise manipulating e-mail identifiers in order to mask or mislead the origins of certain content; +\par +\par Interfering with the service to any other user, client, host or network which reduces the quality of service for other clients and users; +\par +\par Using the service to engage in Denial-of-service ("DOS") attacks to any third-parties or to Shells\'99; +\par +\par Accessing data, systems or networks including attempts to probe scan or test for vulnerabilities of a system or network or to breach security or authentication measures without written consent from the owner of the system or network; +\par +\par Transmitting any material (by email, uploading, posting, or otherwise) that abuses, bullies, threatens or encourages bodily harm, injury or destruction of property, defames one or more third parties, or promotes any act of cruelty to animals; or +\par +\par Accessing the service to violate any laws at the local, state and federal level in the United States of America or the country/territory in which you reside. +\par +\par +\par EXPORT CONTROLS +\par +\par The Shells\'99 Website is subject to all relevant United States export control laws and regulations. Shells\'99 makes no representation that the Shells\'99 Website is a +ppropriate or available for use in other locations outside the United States. By using the Shells\'99 + Website, you represent and warrant that: (i) you are not listed on the U.S. Commerce Department's Table of Denial Orders, the U.S. Treasury Department's list +s of specially designated nationals, or otherwise denied the privilege of participating in transactions involving the export of U.S.-origin products and services; (ii) you are not located in a country that is subject to embargo by the United States (curre +n +tly Cuba, Iraq, Libya, North Korea, Sudan, Syria, or the Taliban Occupied Part of Afghanistan); (iii) you are not engaged, directly or indirectly, in the design, development, production, stockpiling, or use of nuclear, chemical, or biological weapons or m +i +ssiles; and (iv) you will not, without prior authorization from the Bureau of Export Administration, (a) knowingly re-export the technical data received from you to any destination or (b) export the direct product of the technical data, directly or indire +c +tly, to a country listed in Country Group D:1 or E:2 in Supplement No. 1 to Part 740 of the Export Administration Regulations (Albania, Armenia, Azerbaijan, Belarus, Bulgaria, Cambodia, Cuba, Estonia, Georgia, Kazakhstan, Kyrgyzstan, Laos, Latvia, Libya, +Lithuania, Macau, Moldova, Mongolia, North Korea, People's Republic of China, Romania, Russia, Tajikistan, Turkmenistan, Ukraine, Uzbekistan, or Vietnam). +\par +\par +\par BREACH +\par +\par Shells\'99 abides by a ZERO TOLERANCE policy relating to any activity which breaches or violates our terms and conditions. +\par +\par We can terminate and disable your account if you break our rules. +\par +\par Along with the ZERO TOLERANCE policy, Clients who materially breach the terms and conditions will have their account or a subscription removed without any refund. Additionally, Client understands that Shells\'99 + expressly reserves the right to hold the Client or any third-party using the service on Client\rquote s behalf responsible for any and all financial damages and losses which may be incurred arising out of +said breach or breaches, including, but not limited to attorneys fees, fees for expert witnesses, court costs, and other charges. +\par +\par Subscriber understands that Shells\'99 reserves the right in its sole discretion to enforce breaches of this Agreement. Failur +e to comply with the present Terms of Service constitutes a material breach of the Agreement, and may result in one or more of these following actions: +\par +\par Issuance of a warning; +\par +\par Immediate, temporary, or permanent revocation of access to Shells\'99 with no refund; +\par +\par Independent legal action by Shells\'99 as a result of a breach; or +\par +\par Disclosure of such information to law enforcement authorities as deemed reasonably necessary. +\par +\par Shells\'99 reserves the right to take any other actions deemed necessary to enforce and protect its rights. If you find that your Shells\'99 account or subscription has been suspended, then you may contact: contact@shells.com. +\par +\par +\par SERVICE LEVEL AGREEMENT +\par +\par Service coverage, speeds, locations and quality are not guaranteed. While Shells\'99 will make every attempt to maintain the Shells\'99 Website availability at all times, the Shells\'99 + Website may be subject to unavailability for numerous reasons including maintenance, emergencies, third party service failures, transmission errors, equipment failures, network issues, interference, natural disaster, amongst other reasons. Shells\'99 + does not guarantee that data, messages, or packets will be delivered and shall not be held responsible in the event data, messages, or packets are lost, not delivered, del +ayed, misdirected or are otherwise inaccessible. Sometimes hardware malfunctions - we will do our best to have 100% uptime. Additionally, we may impose usage limits to our services, suspend or block services, or cancel any and all services at our sole dis +cretion at any time. Finally, we do not guarantee the accuracy and timeliness of any data received. +\par +\par We make no guarantee that the Shells\'99 Website will be accessible at any time. However, we will do our best to keep the service up and running for our beloved clients. +\par +\par +\par CLIENT RESPONSIBILITIES +\par +\par As a client of Shells\'99, you are responsible for: +\par +\par Providing valid and accurate identifying information related to the user account and +\par +\par Liability for any use and/or abuse which occurs while you or any third-party is logged into the Shells\'99 Website with your account credentials. +\par +\par +\par FEES; CHARGEBACKS +\par +\par You acknowledge that Shells\'99 reserves the right to create a subscription service through one or more third party merchants. With each account you may have onl +y one active subscription at a time. Payments will be charged on the day you sign up for service and will cover use of that service for the duration of one (1) month or one (1) year plan, depending on the service level plan. A subscription plan is an auto +matic payment recurring based on the service plan. All accounts are offered as is at the time of purchase. Future services offered by Shells\'99 + Website, E Shells, Inc., or its partners may not be included with the cost of the subscription. You may cancel the subscription at anytime; the account will remain active for the remainder of your billing cycle. +\par +\par Shells\'99 reserves the right to change the fees at anytime at its discretion. Subscriber understands that Shells\'99 is not obligated to honor errors due to typo +s and is not responsible for misinformation provided on third party websites or affiliates. Subscriber also understands that any gift-card based transactions for service are not subject to any reductions in price, discounts, promotional rates, or other lo +w +ered subscription rates. If you contact your bank or credit card company to decline, chargeback or otherwise reverse the charge of any payable fees to us ("Chargeback"), we may automatically terminate your Account. If you have questions about a payment ma +de to us, we encourage you to contact Customer Care before filing a Chargeback. We reserve our right to dispute any Chargeback. +\par +\par +\par CANCELLATION +\par +\par You understand and agree that Shells\'99 shall maintain your email address after your subscription ends. You may access the Client Control Panel to reactivate your subscription at any time. +\par +\par +\par REFUND POLICY +\par +\par If you are less than 100% satisfied with the Shells\'99 service, we will gladly provide a prorated refund for the current billing cycle ONLY, at the monthly r +ate, if the refund is requested within seven (7) days from the date of the initial purchase and/or renewal. Requests made after the 7 day purchase date window will be denied. You understand that if you purchase a new account within three (3) months of bei +ng issued a refund on a previous account purchase, you will not be eligible for a refund on that new account, even if you request one during or after the (7) day period. +\par +\par If you are any less than 100% satisfied, we will refund you within seven (7) days of your purchase. +\par +\par In the event of an unauthorized chargeback, your account details shall be blacklisted. +\par +\par If you are seeking a refund after paying for the Services via a cryptocurrency, then you must provide to Shells\'99 a wallet address for the refund to be credited. +\par +\par You understand that by paying for Shells\'99 using cryptocurrency as a transaction method, you are using a payment means that is not backed by an official governmental entity or international financial institution, and that the payment system + may be prone to large fluctuations in value in a short period of time. The Parties agree that any refunds for transactions using a specific cryptocurrency will be assessed on the cryptocurrency\rquote +s exchange rate to USD at the time of the refund disbursement, and not at the time of the original transaction or refund request. +\par +\par Due to limitations with 3rd party payment processors, certain accounts cannot be refunded by Shells\'99 directly. Accounts purchased through Gift Cards or 3rd party deal sites, can only be refunded by the payment processor. +\par +\par +\par RIGHTS; TERMINATION AND EFFECT +\par +\par Shells\'99 reserves the right to terminate and close your account at any given time without any given notice. While Shells\'99 will, at its best interest, attempt to provide full and com +plete service to its users, this right is reserved for reasons which may arise at a later date. +\par +\par Subscriber understands that Shells\'99 also reserves the right to scale back or throttle bandwidth originating from subscriber accounts that may breach the present Agreement or in the event of excessive usage on the Shells\'99 network. +\par +\par Subscriber also understands that Shells\'99 for reasons beyond its control may shut down and terminate services. If Shells\'99 ceases operations, subscribers will be notified with at lea +st thirty (30) days advance notice. Subscribers will not be eligible for a pro-rated, partial, or complete refund in the event of a shut down. +\par +\par Without limiting other remedies, Shells\'99 may immediately terminate or suspend your access to the Shells\'99 Website and remove any material (including User Content) from the Shells\'99 + Website or our servers, in the event that You breach this Agreement. Notwithstanding the foregoing, we also reserve the right to terminate, limit or suspend your access to or use of the Shells\'99 Website at any time and for any reason or no reason. + +\par +\par After any termination by You or Company: You understand and acknowledge that we will have no further obligation to provide or allow access to the Shells\'99 Website. Upon termination, all licenses + and other rights granted to You by this Agreement will immediately cease. Shells\'99 is not liable to You or any third party for termination of the Shells\'99 Website or termination of your use of the Shells\'99 + Website. UPON ANY TERMINATION OR SUSPENSION, ANY INFORMATION (INCLUDING ANY USER CONTENT OR OTHER USER SUBMISSIONS) THAT YOU HAVE SUBMITTED, POSTED, UPLOADED OR OTHERWISE MADE AVAILABLE ON THE SHELLS\'99 + WEBSITE OR THAT WHICH IS RELATED TO YOUR ACCOUNT MAY NO LONGER BE ACCESSED BY YOU. Furthermore, except as may be required by applicable law, Shells\'99 + will have no obligation to store or maintain any User Content or other information stored in our database related to your account or to forward any information to You or any third party. +\par +\par Any suspension, termination or cancellation will not affect your obligations to Shells\'99 + under this Agreement (including but not limited to ownership, indemnification and limitation of liability), which by their sense and context are intended to survive such suspension, termination or cancellation. +\par +\par +\par WARRANTIES +\par +\par Subscriber represents and warrants that all of the identifying information provided to Shells\'99 to use the Shells\'99 Website is accurate and current and you have all necessary right, power, and authority to enter into thi +s Agreement and to perform the acts required of you hereunder. +\par +\par By entering into this Agreement, you are representing that the information you are providing or representing is truthful and not misleading and that you agree to respect and follow our Privacy Policy. +\par +\par As a condition to using the Shells\'99 Website, you must agree to all of the terms of Shells\rquote Privacy Policy, and any modifications and/or updates. You acknowledge and agree that the technical processing and transmission of the Shells\'99 Website m +ay involve transmissions over various networks; and changes to conform and adapt to technical requirements of connecting networks or devices. You further acknowledge and agree that other data collected and maintained by Shells\'99 + with regard to its users may be disclosed in accordance with the Shells\'99 Privacy Policy. +\par +\par +\par WARRANTY DISCLAIMER +\par +\par SUBSCRIBER UNDERSTANDS THAT THE SHELLS\'99 WEBSITE IS PROVIDED AS-IS. SUBSCRIBER AGREES THAT USE OF THE SHELLS\'99 WEBSITE SHALL BE AT YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, SHELLS\'99 +, ITS AFFILIATES AND RESPECTIVE OFFICERS, DIRECTORS, EMPLOYEES, AND AGENTS DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, IN CONNECTION WITH THE SHELLS\'99 WEBSITE AND YOUR USE THEREOF. SHELLS\'99 MAKES NO WARRANTIES, EXPRESS, OR IMPLIED, +NOR ANY REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE SHELLS\'99 WEBSITE'S CONTENT OR THE CONTENT OF ANY SITES LINKED TO THE SHELLS\'99 + WEBSITE AND ASSUMES NO LIABILITY OR RESPONSIBILITY FOR ANY: ERRORS, MISTAKES, OR INACCURACIES OF CONTENT, PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SHELLS\'99 + WEBSITE, ANY UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR FINANCIAL INFORMATION STORED THEREIN, + ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE SHALLS WEBSITE AND/OR SERVICE, ANY BUGS, VIRUSES, TROJAN HORSES, OR THE LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE SHELLS\'99 + WEBSITE BY ANY THIRD PARTY, ANY ERRORS OR OMISSIONS IN ANY CONTENT OR FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY CONTENT POSTED, EMAILED, TRANSMITTED, OR OTHERWISE MADE AVAILABLE VIA THE SHELLS\'99 WEBSITE. +\par +\par SHELLS\'99 DOES NOT WARRANT, ENDORSE, GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR OFFERED BY A THIRD PARTY THROUGH THE SHELLS\'99 + WEBSITE OR ANY HYPERLINKED WEBSITE OR FEATURED IN ANY BANNER OR OTHER ADVERTISING, AND SHELLS\'99 WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR MONITORING ANY TRANSACTION BETWE +EN YOU AND THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES. AS WITH THE PURCHASE OF A PRODUCT OR SERVICE THROUGH ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD USE YOUR BEST JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE. THE FOREGOING LIMITATION OF LIABILIT +Y + SHALL APPLY TO THE FULLEST EXTENT PERMITTED BY LAW IN THE APPLICABLE JURISDICTION. YOU SPECIFICALLY ACKNOWLEDGE THAT E SHELLS, INC. SHALL NOT BE LIABLE FOR DEFAMATORY, OFFENSIVE, OR ILLEGAL CONDUCT OF ANY THIRD PARTY AND THAT THE RISK OF HARM OR DAMAGE F +ROM THE FOREGOING RESTS ENTIRELY WITH YOU. +\par +\par +\par LIMITATION OF LIABILITY +\par +\par IN NO EVENT SHALL SHELLS\'99, NOR ANY AFFILIATES OR THEIR RESPECTIVE OFFICERS, DIRECTORS, EMPLOYEES, OR AGENTS, BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, PUNITIVE, + OR CONSEQUENTIAL DAMAGES WHATSOEVER RESULTING FROM ANY USE OF THE SHELLS\'99 + WEBSITE SERVICE IN AN AREA OR COUNTRY WHICH PROHIBITS SUCH ACTIONS ERRORS, MISTAKES, OR INACCURACIES OF CONTENT, PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SHELLS\'99 + WEBSITE, ANY UNAUTHORIZED ACCESS TO OR USE OF OUR SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR FINANCIAL INFORMATION STORED THEREIN, ANY INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE SHELLS\'99 + WEBSITE AND/OR SERVICE ANY BUGS, VIRUSES, TROJAN HORSES, OR THE LIKE, WHICH MAY BE TRANSMITTED TO OR THROUGH THE SHELLS\'99 WEBSITE BY ANY THIRD PARTY, AND/OR ANY ERRORS OR OMISSIONS IN ANY CONTENT OR FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A + RESULT OF YOUR USE OF ANY CONTENT POSTED, EMAILED, TRANSMITTED, OR OTHERWISE MADE AVAILABLE VIA THE PIA.COM WEBSITE, WHETHER BASED ON WARRANTY, CONTRACT, TORT, OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT SHELLS\'99 + IS ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +\par +\par THE FOREGOING LIMITATION OF LIABILITY SHALL APPLY TO THE FULLEST EXTENT PERMITTED BY LAW IN THE APPLICABLE JURISDICTION. YOU SPECIFICALLY ACKNOWLEDGE THAT E SHELLS, INC. SHALL NOT BE LIABLE FOR DEFAMATORY, OFFENSIVE, OR ILLEGAL CONDUCT OF ANY THIR +D PARTY AND THAT THE RISK OF HARM OR DAMAGE FROM THE FOREGOING RESTS ENTIRELY WITH YOU. +\par +\par +\par INDEMNITY +\par +\par Subscriber agrees to defend, indemnify and hold harmless E Shells, Inc., its parent corporation and their respective, shareholders, members, officers, + directors, employees and agents, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, and expenses (including but not limited to attorney's fees) arising from: +\par +\par your use of and access to the Shells\'99 Website; +\par +\par your material breach of any term of these Terms of Service; +\par +\par your violation of any third party right, including without limitation any copyright, patent, trademark, property, or privacy right; or +\par +\par any claim that your use caused damage or injury to any third party. +\par +\par This defense and indemnification obligation will survive these Terms of Service and your use of the Shells\'99 Website. +\par +\par +\par ARBITRATION +\par +\par Purpose. The term "Dispute" means any dispute, claim, or controversy between you and Shells\'99 regarding these Terms or the use of the Shells\'99 + Website, whether based in contract, statute, regulation, ordinance, tort (including, but not limited to, fraud, misrepresentation, fraudulent inducement, or negligence), or any other legal or equitable theory, and incl +udes the validity, enforceability or scope of this "BINDING INDIVIDUAL ARBITRATION" Section (with the exception of the enforceability of the Class Action Waiver clause below). "Dispute" is to be given the broadest possible meaning that will be enforced. I +f you have a Dispute with Shells\'99 or any company, subsidiary, parent, vendor associated with Shells\'99 that cannot be resolved through negotiation within the time frame described in the "Notice of Dispute" clause below, you and Shells\'99 + that you have a Dispute + with agree to seek resolution of the Dispute only through arbitration of that Dispute in accordance with the terms of this Section, and not litigate any Dispute in court, except for those matters listed in the Exclusions from Arbitration clause. Arbitrat +ion means that the Dispute will be resolved by a neutral arbitrator instead of in a court by a judge or jury. +\par +\par Disputes may be settled outside of court. +\par +\par Exclusions from Arbitration. YOU AND E SHELLS, INC. AGREE THAT ANY CLAIM FILED BY YOU OR BY E SHELLS, INC. IN SMALL CLAIMS COURT ARE NOT SUBJECT TO THE ARBITRATION TERMS CONTAINED IN THIS SECTION. +\par +\par RIGHT TO OPT OUT OF BINDING ARBITRATION AND CLASS ACTION WAIVER WITHIN 30 DAYS. IF YOU DO NOT WISH TO BE BOUND BY THE BINDING ARBITRATION AND CLASS ACTION +WAIVER IN THIS SECTION, YOU MUST NOTIFY E SHELLS, INC. IN WRITING WITHIN 30 DAYS OF THE DATE THAT YOU ACCEPT THIS AGREEMENT UNLESS A LONGER PERIOD IS REQUIRED BY APPLICABLE LAW. YOUR WRITTEN NOTIFICATION MUST BE MAILED TO 8550 W. CHARLESTON BLVD., UNIT 10 +2 BOX 104, LAS VEGAS, NEVADA 89117, ATTN: LEGAL DEPARTMENT/ARBITRATION AND MUST INCLUDE: (1) YOUR NAME, (2) YOUR ADDRESS, (3) YOUR SHELLS.COM ONLINE ID, AND (4) A CLEAR STATEMENT THAT YOU DO NOT WISH TO RESOLVE DISPUTES WITH SHELLS\'99 + THROUGH ARBITRATION. +\par +\par Notice of Dispute. IF YOU HAVE A DISPUTE WITH THE SHELLS\'99 + WEBSITE, YOU MUST SEND WRITTEN NOTICE TO E SHELLS, INC., 8550 W. CHARLESTON BLVD., UNIT 102 BOX 104, LAS VEGAS, NEVADA 89117, ATTN: LEGAL DEPARTMENT/ARBITRATION, ATTN: E SHELLS, INC.: DISPUTE RESOLU +TION" TO GIVE E SHELLS, INC. THE OPPORTUNITY TO RESOLVE THE DISPUTE INFORMALLY THROUGH NEGOTIATION. You agree to negotiate resolution of the Dispute in good faith for no less than 60 days after you provide notice of the Dispute. If Shells\'99 + does not resolve your Dispute within 60 days from receipt of notice of the Dispute, you or E Shells, Inc. may pursue your claim in arbitration pursuant to the terms in this Section. +\par +\par No class action lawsuits. +\par +\par Class Action Waiver. ANY DISPUTE RESOLUTION PROCEEDINGS, W +HETHER IN ARBITRATION OR COURT, WILL BE CONDUCTED ONLY ON AN INDIVIDUAL BASIS AND NOT IN A CLASS OR REPRESENTATIVE ACTION OR AS A NAMED OR UNNAMED MEMBER IN A CLASS, CONSOLIDATED, REPRESENTATIVE OR PRIVATE ATTORNEY GENERAL ACTION, UNLESS BOTH YOU AND E SH +E +LLS, INC. SPECIFICALLY AGREE TO DO SO IN WRITING FOLLOWING INITIATION OF THE ARBITRATION. THIS PROVISION DOES NOT PRECLUDE YOUR PARTICIPATION AS A MEMBER IN A CLASS ACTION FILED ON OR BEFORE AUGUST 20, 2011. THIS PROVISION IS NOT APPLICABLE TO THE EXTENT +SUCH WAIVER IS PROHIBITED BY LAW. +\par +\par Initiation of Arbitration Proceeding/Selection of Arbitrator. If you or E Shells, Inc. elects to resolve your Dispute through arbitration, the party initiating the arbitration proceeding may initiate it with the American + Arbitration Association ("AAA"), www.adr.org, or JAMS www.jamsadr.com. The terms of this Section govern in the event they conflict with the rules of the arbitration organization selected by the parties. +\par +\par Arbitration Procedures. Because the software and/or service provided to you by Shells\'99 +, you may have a Dispute with concern interstate commerce, the Federal Arbitration Act ("FAA") governs the arbitrability of all Disputes. However, applicable federal or state law may also apply to the substance of any D +isputes. For claims of less than $75,000, the AAA's Supplementary Procedures for Consumer-Related Disputes ("Supplementary Procedures") shall apply including the schedule of arbitration fees set forth in Section C-8 of the Supplementary Procedures; for cl +a +ims over $75,000, the AAA's Commercial Arbitration Rules and relevant fee schedules for non-class action proceedings shall apply. The AAA rules are available at www.adr.org or by calling 1-800-778-7879. Further, if your claims do not exceed $75,000 and yo +u provided notice to and negotiated in good faith with Shells\'99 + as described above, if the arbitrator finds that you are the prevailing party in the arbitration, you may be entitled to recover reasonable attorneys' fees and costs as determined by the arbitra +tor, in addition to any rights to recover the same under controlling state or federal law afforded to E Shells, Inc.. The arbitrator will make any award in writing but need not provide a statement of reasons unless requested by a party. Such award will be + binding and final, excerpt for any right of appeal provided by the FAA, and may be entered in any court having jurisdiction over the parties for purposes of enforcement. +\par +\par Location of Arbitration. You or E Shells, Inc. may initiate arbitration in either C +lark County, Nevada or the United States county in which you reside. In the event that you select the county of your United States residence, E Shells, Inc. may transfer the arbitration to Clark County in the event that it agrees to pay any additional fee +s or costs you incur as a result of the change in location as determined by the arbitrator. +\par +\par ASSIGNMENT +\par +\par The terms and conditions contained herein and any rights and licenses granted hereunder, may not be transferred or assigned by you, but may be assigned by E Shells, Inc. without restriction. +\par +\par +\par SEVERANCE +\par +\par If any term, clause or provision of the Agreement is held invalid or unenforceable by a court of competent jurisdiction, such invalidity shall not affect the validity or operation of any term, cl +ause or provision and such invalid term, clause or provision shall be deemed to be severed from this Agreement. +\par +\par +\par CHOICE OF LAW +\par +\par This Agreement shall be governed by and construed in accordance with the laws of the State of Nevada, Clark County, withou +t regard to conflicts of law principles. The sole and exclusive jurisdiction and venue for any action or proceeding arising out of or related to this Agreement shall be in an appropriate state or federal court located in the State of Nevada, Clark County, + or in nearest proximity thereto. You hereby submit to the jurisdiction and venue of said Courts. You consent to service of process in any legal proceeding. +\par +\par You are agreeing that Nevada law will apply and that any dispute must be litigated in Nevada. +\par +\par If Subscriber agrees to all of the foregoing terms and conditions, Subscriber may gain access to and use the Shells\'99 Website.}{\rtlch\fcs1 \af2 \ltrch\fcs0 \f2\insrsid9534598\charrsid5523255 +\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a +9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad +5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 +b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 +0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 +a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f +c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 +0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 +a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 +6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b +4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b +4757e8d3f729e245eb2b260a0238fd010000ffff0300504b03041400060008000000210007b740aaca0600008f1a0000160000007468656d652f7468656d652f +7468656d65312e786d6cec595b8bdb46147e2ff43f08bd3bbe49be2cf1065bb69336bb49889d943cceda636bb2238dd18c776342a0244f7d2914d2d28706fad6 +87521a68a0a12ffd310b1bdaf447f4cc489667ec71f6420aa1640d8b34face996fce39face48ba7aed51449d239c70c2e2965bbe52721d1c8fd898c4d3967b6f +d82f345c870b148f1165316eb90bccdd6bbb9f7e7215ed881047d801fb98efa0961b0a31db2916f9088611bfc26638866b13964448c069322d8e13740c7e235a +ac944ab5628448ec3a318ac0ededc9848cb033942edddda5f31e85d358703930a2c940bac68685c28e0fcb12c1173ca089738468cb8579c6ec78881f09d7a188 +0bb8d0724beacf2dee5e2da29dcc888a2db69a5d5ffd657699c1f8b0a2e64ca607f9a49ee77bb576ee5f01a8d8c4f5eabd5aaf96fb5300341ac14a532eba4fbf +d3ec74fd0cab81d2438bef6ebd5b2d1b78cd7f758373db973f03af40a97f6f03dfef07104503af4029dedfc07b5ebd1278065e81527c6d035f2fb5bb5eddc02b +5048497cb8812ef9b56ab05c6d0e99307ac30a6ffa5ebf5ec99caf50500d7975c929262c16db6a2d420f59d2078004522448ec88c50c4fd008aa3840941c24c4 +d923d3100a6f8662c661b85429f54b55f82f7f9e3a5211413b1869d6921730e11b43928fc34709998996fb39787535c8e9ebd7274f5f9d3cfdfde4d9b393a7bf +66732b5786dd0d144f75bbb73f7df3cf8b2f9dbf7ffbf1edf36fd3a9d7f15cc7bff9e5ab377ffcf92ef7b0e255284ebf7bf9e6d5cbd3efbffeebe7e716efed04 +1de8f0218930776ee163e72e8b608116fef820b998c5304444b768c7538e622467b1f8ef89d040df5a208a2cb80e36e3783f01a9b101afcf1f1a8407613217c4 +e2f1661819c07dc6688725d628dc947369611ecee3a97df264aee3ee2274649b3b40b191e5de7c061a4b6c2e83101b34ef50140b34c531168ebcc60e31b6acee +0121465cf7c928619c4d84f380381d44ac21199203a39a56463748047959d80842be8dd8ecdf773a8cda56ddc5472612ee0d442de487981a61bc8ee602453697 +4314513de07b48843692834532d2713d2e20d3534c99d31b63ce6d36b71358af96f49b2033f6b4efd345642213410e6d3ef710633ab2cb0e831045331b7640e2 +50c77ec60fa144917387091b7c9f9977883c873ca0786bbaef136ca4fb6c35b8070aab535a1588bc324f2cb9bc8e9951bf83059d20aca4061a80a1eb1189cf14 +f93579f7ff3b7907113dfde1856545ef47d2ed8e8d7c5c50ccdb09b1de4d37d6247c1b6e5db803968cc987afdb5d348fef60b855369bd747d9fe28dbeeff5eb6 +b7ddcfef5fac57fa0cd22db7ade9765d6ddea3ad7bf709a174201614ef71b57de7d095c67d189476eab915e7cf72b3100ee59d0c1318b86982948d9330f10511 +e1204433d8e3975de964ca33d753eecc1887adbf1ab6fa96783a8ff6d9387d642d97e5e3692a1e1c89d578c9cfc7e17143a4e85a7df51896bb576ca7ea717949 +40da5e8484369949a26a21515f0eca20a98773089a85845ad97b61d1b4b06848f7cb546db0006a795660dbe4c066abe5fa1e9880113c55218ac7324f69aa97d9 +55c97c9f99de164ca302600fb1ac8055a69b92ebd6e5c9d5a5a5768e4c1b24b4723349a8c8a81ec64334c65975cad1f3d0b868ae9bab941af46428d47c505a2b +1af5c6bb585c36d760b7ae0d34d69582c6ce71cbad557d2899119ab5dc093cfac3613483dae172bb8be814de9f8d4492def097519659c24517f1300db8129d54 +0d222270e25012b55cb9fc3c0d34561aa2b8952b20081f2cb926c8ca87460e926e26194f267824f4b46b2332d2e929287caa15d6abcafcf26069c9e690ee4138 +3e760ee83cb98ba0c4fc7a5906704c38bc012aa7d11c1378a5990bd9aafed61a5326bbfa3b455543e938a2b310651d4517f314aea43ca7a3cef2186867d99a21 +a05a48b2467830950d560faad14df3ae9172d8da75cf369291d34473d5330d55915dd3ae62c60ccb36b016cbcb35798dd532c4a0697a874fa57b5d729b4bad5b +db27e45d02029ec7cfd275cfd110346aabc90c6a92f1a60c4bcdce46cddeb15ce019d4ced32434d5af2dddaec52def11d6e960f0529d1fecd6ab168626cb7da5 +8ab4faf6a17f9e60070f413cbaf022784e0557a9848f0f09820dd140ed4952d9805be491c86e0d3872e60969b98f4b7edb0b2a7e502835fc5ec1ab7aa542c36f +570b6ddfaf967b7eb9d4ed549e4063116154f6d3ef2e7d780d4517d9d71735bef105265abe69bb32625191a92f2c45455c7d812957b67f81710888cee35aa5df +ac363bb542b3daee17bc6ea7516806b54ea15b0beadd7e37f01bcdfe13d7395260af5d0dbc5aaf51a89583a0e0d54a927ea359a87b954adbabb71b3daffd24db +c6c0ca53f9c86201e155bc76ff050000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f7468656d652f5f72 +656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c08 +2e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd0 +8a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa +4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000000000000000000000005b436f +6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000000000300100005f72 +656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000190200007468656d652f746865 +6d652f7468656d654d616e616765722e786d6c504b01022d001400060008000000210007b740aaca0600008f1a00001600000000000000000000000000d60200 +007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b01000027000000000000000000000000 +00d40900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000cf0a00000000} +{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d +617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 +6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 +656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} +{\*\latentstyles\lsdstimax371\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; +\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Table;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Theme;\lsdsemihidden1 \lsdlocked0 Placeholder Text; +\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2; +\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List; +\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1; +\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision; +\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1; +\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1; +\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2; +\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2; +\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2; +\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3; +\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3; +\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4; +\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4; +\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4; +\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5; +\lsdpriority62 \lsdlocked0 Light Grid Accent 5;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; +\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; +\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; +\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; +\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; +\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; +\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; +\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; +\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; +\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; +\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; +\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;}}{\*\datastore 010500000200000018000000 +4d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 +d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000c05c +317a17d4d601feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000105000000000000}} \ No newline at end of file diff --git a/login.go b/login.go new file mode 100644 index 0000000..eb15f18 --- /dev/null +++ b/login.go @@ -0,0 +1,416 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log" + "strings" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/KarpelesLab/csscolor" + "github.com/KarpelesLab/rest" + "github.com/Shells-com/shells-go/res" + "github.com/Shells-com/shells-go/shellsui" + "github.com/pkg/browser" + "github.com/vincent-petithory/dataurl" +) + +const clientID = "oaap-yg3n7j-nxzj-d5zm-eulq-oot76jli" + +type loginShells struct { + a fyne.App + w fyne.Window + session string + vals map[string]interface{} + fields []fyne.CanvasObject + next func(fyne.App, fyne.Window, *rest.Token) +} + +type loginOauth2 struct { + ID string `json:"OAuth2_Consumer__"` + Name string + TokenName string `json:"Token_Name"` +} + +type loginField struct { + Cat string `json:"cat"` + Type string `json:"type"` + Label string `json:"label"` + Name string `json:"name"` + Button *loginButton `json:"button"` // "button":{"background-color":"#1da1f2","logo":" + Info *loginOauth2 `json:"info"` +} + +type loginButton struct { + BackgroundColor string `json:"background-color"` // #ffffff #000 #1da1f2 etc... + Logo string `json:"logo"` // data:image/svg+xml;base64,... or https://... +} + +type loginRes struct { + Complete bool `json:"complete"` + Fields []*loginField + Initial bool `json:"initial"` // if true, no "reset" button + Message string `json:"message"` + Req []string `json:"req"` + Session string `json:"session"` + User map[string]interface{} `json:"user"` + Token *rest.Token // bearer token, 1h lifetime + Url string `json:"url"` +} + +var email string = "" + +func loginWindow(a fyne.App, w fyne.Window, next func(fyne.App, fyne.Window, *rest.Token)) { + w.Resize(fyne.Size{Width: WWidth, Height: WHeight}) + + l := &loginShells{a: a, w: w, next: next} + l.loading() + w.Show() + + // if we have a saved session, do not go further + if l.checkSession() { + return + } + + go func() { + time.Sleep(50 * time.Millisecond) + l.call(nil) + }() +} + +func (l *loginShells) doSubmit() { + // check entry, etc + l.call(l.vals) +} + +func (l *loginShells) checkSession() bool { + // check preferences for a token + if jsB := l.a.Preferences().String("token"); jsB != "" { + log.Printf("login: found saved token") + if js, err := base64.RawURLEncoding.DecodeString(jsB); err == nil { + var token *rest.Token + if err := json.Unmarshal(js, &token); err == nil { + token.ClientID = clientID + go l.next(l.a, l.w, token) + return true + } else { + log.Printf("json parse failed: %s", err) + } + } else { + log.Printf("base64 parse failed: %s", err) + } + } + return false +} + +func (l *loginShells) call(vars map[string]interface{}) { + req := make(map[string]interface{}) + if vars != nil { + // duplicate values + for k, v := range vars { + req[k] = v + } + } + req["client_id"] = clientID + if l.session != "" { + req["session"] = l.session + } + + l.loading() + go l.doCall(req) +} + +func (l *loginShells) doErr(err error) { + if err == nil { + return + } + + var msg string + switch e := err.(type) { + case *rest.Error: + msg = e.Response.Error + default: + msg = err.Error() + } + + d := dialog.NewError(errors.New(msg), l.w) + + d.SetOnClosed(func() { + // restore focus + l.restoreFocus() + }) + + l.w.SetContent( + container.New( + layout.NewBorderLayout( + shellsui.GetMarginRectangle(), + shellsui.GetMarginRectangle(), + shellsui.GetMarginRectangle(), + shellsui.GetMarginRectangle()), + container.NewVBox(l.fields...), + ), + ) + l.restoreFocus() +} + +func (l *loginShells) doCall(vars map[string]interface{}) { + var result loginRes + err := rest.Apply(context.Background(), "User:flow", "POST", vars, &result) + if err != nil { + l.doErr(err) + return + } + + if result.Complete { + log.Printf("login operation has completed") + + result.Token.ClientID = clientID + + if js, err := json.Marshal(result.Token); err == nil { + // should be the case + jsB := base64.RawURLEncoding.EncodeToString(js) + l.a.Preferences().SetString("token", jsB) + } + + l.next(l.a, l.w, result.Token) + return + } + + //log.Printf("got response: %+v", res) + l.session = result.Session + + var fields []fyne.CanvasObject + var focus fyne.Focusable + + vals := make(map[string]interface{}) + var vLock sync.Mutex + socialbtnsC := container.NewHBox() + + for _, f := range result.Fields { + switch f.Type { + case "text", "email", "password", "phone": + e := widget.NewEntry() + switch f.Type { + case "email": + l1 := shellsui.GetShellsLabel("Please enter your Shells Account") + l2 := shellsui.GetShellsLabel("email address to log in") + + fields = append(fields, l1) + fields = append(fields, l2) + case "password": + wb := shellsui.GetShellsLabel("Welcome Back") + emailStr := shellsui.GetShellsLabel(email) + pwd := shellsui.GetShellsLabel("Please enter your password") + + fields = append(fields, wb) + fields = append(fields, emailStr) + fields = append(fields, shellsui.GetMarginRectangleWithHeight(30)) + fields = append(fields, pwd) + e.Password = true + } + + name := f.Name + fType := f.Type + e.OnChanged = func(s string) { + vLock.Lock() + defer vLock.Unlock() + + vals[name] = s + if fType == "email" { + email = s + } + } + e.OnSubmitted = func(value string) { + l.call(l.vals) + } + + ctn := container.New( + layout.NewBorderLayout( + shellsui.GetMarginRectangleWithHeight(15), + shellsui.GetMarginRectangleWithHeight(30), + nil, + nil, + ), + e, + ) + // TODO e.validator + fields = append(fields, ctn) + if focus == nil { + focus = e + } + case "checkbox": + name := f.Name + e := widget.NewCheck(f.Label, func(v bool) { + vLock.Lock() + defer vLock.Unlock() + + vals[name] = v + }) + fields = append(fields, e) + case "oauth2": + var icon fyne.Resource + + //icon := getOAuth2Icon(f.Info.TokenName) + if f.Button != nil { + logo := f.Button.Logo + if logo != "" { + if strings.HasPrefix(logo, "data:") { + // parse as data uri + d, err := dataurl.DecodeString(logo) + if err == nil { + icon = fyne.NewStaticResource(fmt.Sprintf("logo-%s.svg", f.Info.ID), d.Data) + } + } else { + // url, download it + icon, _ = fyne.LoadResourceFromURLString(logo) + } + } + } + + id := f.Info.ID + bgColor, err := csscolor.Parse(f.Button.BackgroundColor) + if err != nil { + bgColor = theme.PrimaryColor() + } + + btn := shellsui.NewSocialButton(icon, bgColor, func() { + l.oauth2(id) + }) + socialbtnsC.Add(btn) + + default: + log.Printf("unknown field = %+v", f) + } + } + + if len(socialbtnsC.Objects) > 0 { + fields = append(fields, container.NewCenter(socialbtnsC)) + } + + submit := widget.NewButtonWithIcon("", res.RightArrow, l.doSubmit) + + submit.Importance = widget.HighImportance + fields = append(fields, container.NewMax(submit)) + + l.vals = vals + l.fields = fields + + l.w.SetContent(shellsui.GetMainContainer(container.NewVBox(fields...))) + l.w.CenterOnScreen() + + if focus != nil { + l.w.Canvas().Focus(focus) + } +} + +func (l *loginShells) loading() { + l.w.SetContent(shellsui.GetMainContainer(container.NewVBox(widget.NewProgressBarInfinite()))) +} + +func (l *loginShells) restoreFocus() { + for _, w := range l.fields { + if f, ok := w.(fyne.Focusable); ok { + l.w.Canvas().Focus(f) + break + } + } +} + +func (l *loginShells) oauth2(id string) { + l.loading() + + var result map[string]interface{} + err := rest.Apply(context.Background(), "OAuth2/App/"+clientID+":token_create", "POST", map[string]interface{}{}, &result) + + if err != nil { + log.Printf("failed to fetch the token: %s", err) + l.doErr(err) + return + } + + tok, ok := result["polltoken"].(string) + if !ok { + log.Printf("failed to fetch polltoken") + l.doErr(errors.New("invalid response from API")) + return + } + tokuri := "polltoken:" + tok + + req := make(map[string]interface{}) + if l.vals != nil { + // duplicate values + for k, v := range l.vals { + req[k] = v + } + } + req["client_id"] = clientID + if l.session != "" { + req["session"] = l.session + } + req["oauth2"] = id + req["redirect_uri"] = tokuri + + var resultUserFlow loginRes + err = rest.Apply(context.Background(), "User:flow", "POST", req, &resultUserFlow) + + if err != nil { + log.Printf("failed to update the User flow with oauth2: %s", err) + l.doErr(err) + return + } + if resultUserFlow.Url == "" { + log.Printf("failed to get the url from the payload from User:flow") + l.doErr(errors.New("missing login URL")) + return + } + err = browser.OpenURL(resultUserFlow.Url) + if err != nil { + l.doErr(err) + return + } + + for { + var res map[string]interface{} + err := rest.Apply(context.Background(), "OAuth2/App/"+clientID+":token_poll", "POST", map[string]interface{}{"polltoken": tok}, &res) + if err != nil { + log.Printf("failed to fetch the token poll: %s", err) + l.doErr(err) + return + } + + log.Printf("api response = %+v", res) + + v, ok := res["response"] + if !ok { + time.Sleep(time.Second) // just in case + continue + } + + resp, ok := v.(map[string]interface{}) + if !ok { + log.Printf("invalid response from api, response of invalid type") + l.doErr(errors.New("invalid response from API")) + return + } + + session, ok := resp["session"].(string) + if !ok { + log.Printf("invalid response from api, response not containing session") + l.doErr(errors.New("invalid response from API")) + return + } + l.session = session + l.call(nil) + return + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4ce937b --- /dev/null +++ b/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "flag" + "log" + "os" + "runtime/pprof" + + "github.com/Shells-com/shells-go/res" + + "fyne.io/fyne/v2/app" + "github.com/KarpelesLab/rest" + "github.com/gordonklaus/portaudio" +) + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") + +func main() { + flag.Parse() + + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal("could not create CPU profile: ", err) + } + defer f.Close() // error handling omitted for example + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + } + + log.Printf("Initializing PortAudio version %s", portaudio.VersionText()) + portaudio.Initialize() + defer portaudio.Terminate() + + if dev, err := portaudio.DefaultOutputDevice(); err == nil { + log.Printf("default output device: %s type %s", dev.Name, dev.HostApi.Name) + } + + rest.Host = "www.shells.com" + rest.Debug = true + + os.Setenv("FYNE_SCALE", "1") + + a := app.NewWithID("com.shells.app") + a.Settings().SetTheme(getShellsTheme()) + w := a.NewWindow("Shells") + w.SetIcon(res.ShellsIcon) + + loginWindow(a, w, shellsList) + + a.Run() +} diff --git a/res/Icon-alpha-128x128.png b/res/Icon-alpha-128x128.png new file mode 100644 index 0000000..2fefe64 Binary files /dev/null and b/res/Icon-alpha-128x128.png differ diff --git a/res/Icon-alpha.png b/res/Icon-alpha.png new file mode 100644 index 0000000..104dcde Binary files /dev/null and b/res/Icon-alpha.png differ diff --git a/res/embed.go b/res/embed.go new file mode 100644 index 0000000..556ff77 --- /dev/null +++ b/res/embed.go @@ -0,0 +1,28 @@ +package res + +import _ "embed" +import "fyne.io/fyne/v2" + +//go:embed rightarrow.svg +var rightArrowBin []byte + +//go:embed shellslogo.png +var shellsLogoBin []byte + +//go:embed Icon-alpha-128x128.png +var shellsIcon []byte + +var RightArrow = &fyne.StaticResource{ + StaticName: "res/rightarrow.svg", + StaticContent: rightArrowBin, +} + +var ShellsLogo = &fyne.StaticResource{ + StaticName: "res/shellslogo.png", + StaticContent: shellsLogoBin, +} + +var ShellsIcon = &fyne.StaticResource{ + StaticName: "res/Icon-alpha-128x128.png", + StaticContent: shellsIcon, +} diff --git a/res/fallback.go b/res/fallback.go new file mode 100644 index 0000000..082dc4a --- /dev/null +++ b/res/fallback.go @@ -0,0 +1,3 @@ +package res + +// ... diff --git a/res/gen.sh b/res/gen.sh new file mode 100755 index 0000000..4fa1f05 --- /dev/null +++ b/res/gen.sh @@ -0,0 +1,5 @@ +#!/bin/sh +go get github.com/akavel/rsrc + +rsrc -ico icon.ico -arch 386 +rsrc -ico icon.ico -arch amd64 diff --git a/res/icon.ico b/res/icon.ico new file mode 100644 index 0000000..50da7a1 Binary files /dev/null and b/res/icon.ico differ diff --git a/res/rightarrow.svg b/res/rightarrow.svg new file mode 100644 index 0000000..ae0b4ec --- /dev/null +++ b/res/rightarrow.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/rsrc_windows_386.syso b/res/rsrc_windows_386.syso new file mode 100644 index 0000000..6cb23c8 Binary files /dev/null and b/res/rsrc_windows_386.syso differ diff --git a/res/rsrc_windows_amd64.syso b/res/rsrc_windows_amd64.syso new file mode 100644 index 0000000..a317bec Binary files /dev/null and b/res/rsrc_windows_amd64.syso differ diff --git a/res/shellslogo.png b/res/shellslogo.png new file mode 100644 index 0000000..9ee5969 Binary files /dev/null and b/res/shellslogo.png differ diff --git a/res/windows_386.manifest b/res/windows_386.manifest new file mode 100644 index 0000000..3c277f5 --- /dev/null +++ b/res/windows_386.manifest @@ -0,0 +1,17 @@ + + + +Shells™ Client + + + + + + + + diff --git a/shells.go b/shells.go new file mode 100644 index 0000000..776724e --- /dev/null +++ b/shells.go @@ -0,0 +1,171 @@ +package main + +import ( + "context" + "crypto/tls" + "encoding/binary" + "errors" + "fmt" + "log" + "net" + "os" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + "github.com/KarpelesLab/rest" + "github.com/Shells-com/shells-go/shellsui" + "github.com/Shells-com/shells-go/spicefyne" +) + +type shellHost struct { + ID string `json:"Shell_Host__"` + Name string + IP string + IPv6 string +} + +type shellsOS struct { + ID string `json:"Shell_OS__"` + Code string + URL string + Family string // "linux", etc + Boot string + Name string +} + +type shellSpice struct { + Host string `json:"host"` // "la01-01.shellsnet.com" + ID string `json:"id"` // Shell ID (shell-***) + Key string `json:"key"` // socket key + Password string `json:"password"` // spice password + Port int `json:"port"` // 443 + Protocol string `json:"protocol"` // "wss" + Token string `json:"token"` // "shell-asqn2r-ucp5-c2fh-heow-ronwydbe.dQQsb3X9UHt0ZFyCqZnJrHPQniDW3TMZ" + URL string `json:"url"` // "wss://..." +} + +type shell struct { + ID string `json:"Shell__"` + Label string + Engine string // "full" + Size int // 16 + Status string // "valid" + State string // "running" + Ssh_Port int // 12345 + Username string + Hostname string + MAC string + IPv4 string + IPv6 string + Created rest.Time + Expires rest.Time + Last_Snapshot rest.Time + Timer_Allowance int + Host *shellHost + OS *shellsOS + + w fyne.Window + a fyne.App + token *rest.Token + spice shellSpice +} + +func shellsList(a fyne.App, w fyne.Window, token *rest.Token) { + // try to use token + var shells []shell + p := map[string]interface{}{ + "Status": "valid", + } + err := rest.Apply(token.Use(context.Background()), "Shell", "GET", p, &shells) + if err != nil { + log.Printf("failed to get: %s", err) + a.Preferences().RemoveValue("token") + loginWindow(a, w, shellsList) + return + } else { + //log.Printf("got: %+v", shells) + } + + var fields []fyne.CanvasObject + title := widget.NewLabel("Choose:") + title.Alignment = fyne.TextAlignCenter + title.TextStyle = fyne.TextStyle{Bold: true} + fields = append(fields, title) + + for _, shl := range shells { + shlCopy := shl + btn := widget.NewButton(shl.Label, func() { + shlCopy.w = w + shlCopy.a = a + shlCopy.token = token + shlCopy.run() + }) // TODO + fields = append(fields, btn) + } + + logout := widget.NewButton("Logout", func() { + a.Preferences().SetString("token", "") + a.Preferences().RemoveValue("token") + loginWindow(a, w, shellsList) + }) + logout.Importance = widget.LowImportance + fields = append(fields, logout) + w.SetContent(shellsui.GetMainContainer(container.NewVBox(fields...))) +} + +func (s *shell) SpiceConnect(compress bool) (net.Conn, error) { + // connect to server + cfg := &tls.Config{ + NextProtos: []string{"shl-spice"}, + } + c, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", s.spice.Host, s.spice.Port), cfg) + if err != nil { + return nil, err + } + + // check negociated protocol + if c.ConnectionState().NegotiatedProtocol != "shl-spice" { + c.Close() + return nil, errors.New("failed to establish shl-spice") + } + + var flags uint8 + + if compress { + flags |= 1 // compress flag + } + + // send token + tok := append([]byte{flags}, s.spice.Token...) + ln := make([]byte, 2) + binary.BigEndian.PutUint16(ln, uint16(len(tok))) + + c.Write(ln) + c.Write(tok) + + // c is either ready to use, or closed. + if compress { + return &snappyConn{c: c}, nil + } + return c, nil +} + +func (s *shell) run() { + s.w.SetContent(shellsui.GetMainContainer(container.NewVBox(widget.NewProgressBarInfinite()))) + + // need to get spice access + err := rest.Apply(s.token.Use(context.Background()), "Shell/"+s.ID+":spice", "POST", map[string]interface{}{}, &s.spice) + if err != nil { + log.Printf("failed: %s", err) + return + } + + log.Printf("got spice = %+v", s.spice) + + _, err = spicefyne.New(s.w, s.a, s, s.spice.Password) + if err != nil { + log.Printf("spice init failed: %s", err) + os.Exit(1) + } +} diff --git a/shellsui/button.go b/shellsui/button.go new file mode 100644 index 0000000..0ccaa6f --- /dev/null +++ b/shellsui/button.go @@ -0,0 +1,157 @@ +package shellsui + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type SocialButton struct { + widget.Button + + icon fyne.Resource + color color.Color + + tapAnim *fyne.Animation +} + +func NewSocialButton(icon fyne.Resource, color color.Color, tapped func()) *SocialButton { + btn := &SocialButton{ + color: color, + icon: icon, + } + btn.ExtendBaseWidget(btn) + btn.SetIcon(icon) + btn.OnTapped = tapped + return btn +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (b *SocialButton) CreateRenderer() fyne.WidgetRenderer { + b.ExtendBaseWidget(b) + + background := canvas.NewCircle(b.color) + tapBG := canvas.NewCircle(color.Transparent) + b.tapAnim = newButtonTapAnimation(tapBG, b) + b.tapAnim.Curve = fyne.AnimationEaseOut + objects := []fyne.CanvasObject{ + background, + tapBG, + } + r := &buttonRenderer{ + objects: objects, + background: background, + tapBG: tapBG, + button: b, + layout: layout.NewHBoxLayout(), + } + r.updateIcon() + r.background.FillColor = b.color + r.background.Refresh() + return r +} + +type buttonRenderer struct { + fyne.WidgetRenderer + + icon *canvas.Image + objects []fyne.CanvasObject + background *canvas.Circle + tapBG *canvas.Circle + button *SocialButton + layout fyne.Layout +} + +func (r *buttonRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +// Layout the components of the button widget +func (r *buttonRenderer) Layout(size fyne.Size) { + var inset fyne.Position + bgSize := size + inset = fyne.NewPos(theme.Padding()/2, theme.Padding()/2) + bgSize = size.Subtract(fyne.NewSize(theme.Padding(), theme.Padding())) + + r.background.Move(inset) + r.background.Resize(bgSize) + + hasIcon := r.icon != nil + if !hasIcon { + // Nothing to layout + return + } + iconSize := fyne.NewSize(30, 30) + + // Icon Only + r.icon.Move(alignedPosition(iconSize, size)) + r.icon.Resize(iconSize) + +} + +// MinSize calculates the minimum size of a button. +// This is based on the contained text, any icon that is set and a standard +// amount of padding added. +func (r *buttonRenderer) MinSize() (size fyne.Size) { + iconSize := fyne.NewSize(40, 40) + size.Width += iconSize.Width + size.Height = iconSize.Height + size = size.Add(r.padding()) + return +} + +func (r *buttonRenderer) Refresh() { + r.updateIcon() + r.background.FillColor = r.button.color + r.background.Refresh() + r.Layout(r.button.Size()) + canvas.Refresh(r.button) //maybe wrong +} + +func (r *buttonRenderer) padding() fyne.Size { + return fyne.NewSize(theme.Padding()*4, theme.Padding()*4) +} + +func (r *buttonRenderer) updateIcon() { + if r.button.Icon != nil { + if r.icon == nil { + r.icon = canvas.NewImageFromResource(r.button.Icon) + r.icon.FillMode = canvas.ImageFillContain + r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.icon}) + } + r.icon.Resource = r.button.Icon + r.icon.Refresh() + r.icon.Show() + } else if r.icon != nil { + r.icon.Hide() + } +} + +func (r *buttonRenderer) SetObjects(objects []fyne.CanvasObject) { + r.objects = objects +} + +func alignedPosition(objectSize, layoutSize fyne.Size) (pos fyne.Position) { + pos.Y = (layoutSize.Height - objectSize.Height) / 2 + pos.X = (layoutSize.Width - objectSize.Width) / 2 + return pos +} + +func newButtonTapAnimation(bg *canvas.Circle, w fyne.Widget) *fyne.Animation { + return fyne.NewAnimation(canvas.DurationStandard, func(done float32) { + mid := (w.Size().Width - theme.Padding()) / 2 + size := mid * done + bg.Resize(fyne.NewSize(size*2, w.Size().Height-theme.Padding())) + bg.Move(fyne.NewPos(mid-size, theme.Padding()/2)) + + r, g, bb, a := theme.PressedColor().RGBA() + aa := uint8(a) + fade := aa - uint8(float32(aa)*done) + bg.FillColor = &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(bb), A: fade} + canvas.Refresh(bg) + }) +} diff --git a/shellsui/label.go b/shellsui/label.go new file mode 100644 index 0000000..b00684f --- /dev/null +++ b/shellsui/label.go @@ -0,0 +1,14 @@ +package shellsui + +import ( + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" +) + +const LabelSize float32 = 16 + +func GetShellsLabel(text string) *canvas.Text { + label := canvas.NewText(text, theme.ForegroundColor()) + label.TextSize = LabelSize + return label +} diff --git a/shellsui/margin.go b/shellsui/margin.go new file mode 100644 index 0000000..bd34bba --- /dev/null +++ b/shellsui/margin.go @@ -0,0 +1,36 @@ +package shellsui + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" +) + +const WMargin float32 = 10 + +func GetMarginRectangle() *canvas.Rectangle { + margin := canvas.NewRectangle(color.Transparent) + margin.SetMinSize(fyne.NewSize(WMargin, WMargin)) + return margin +} + +func GetMarginRectangleWithWidth(width int) *canvas.Rectangle { + if width < 0 { + width = int(WMargin) + } + + margin := canvas.NewRectangle(color.Transparent) + margin.SetMinSize(fyne.NewSize(float32(width), 1)) + return margin +} + +func GetMarginRectangleWithHeight(height int) *canvas.Rectangle { + if height < 0 { + height = int(WMargin) + } + + margin := canvas.NewRectangle(color.Transparent) + margin.SetMinSize(fyne.NewSize(1, float32(height))) + return margin +} diff --git a/shellsui/toggle-button.go b/shellsui/toggle-button.go new file mode 100644 index 0000000..6b7daad --- /dev/null +++ b/shellsui/toggle-button.go @@ -0,0 +1,46 @@ +package shellsui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +type ToggleButton struct { + widget.Button + text string + icon fyne.Resource + textToggled string + iconToggled fyne.Resource + toggled bool +} + +func NewToggleButton(text string, icon fyne.Resource, textToggled string, iconToggled fyne.Resource, initState bool, tapped func()) *ToggleButton { + btn := &ToggleButton{ + text: text, + icon: icon, + textToggled: textToggled, + iconToggled: iconToggled, + } + btn.ExtendBaseWidget(btn) + if initState { + btn.SetText(textToggled) + btn.SetIcon(iconToggled) + } else { + btn.SetText(text) + btn.SetIcon(icon) + } + btn.Button.OnTapped = tapped + return btn +} + +func (b *ToggleButton) Tapped(*fyne.PointEvent) { + if b.toggled { + b.Button.SetText(b.text) + b.Button.SetIcon(b.icon) + } else { + b.Button.SetText(b.textToggled) + b.Button.SetIcon(b.iconToggled) + } + b.toggled = !b.toggled + b.OnTapped() +} diff --git a/shellsui/ui.go b/shellsui/ui.go new file mode 100644 index 0000000..db0836b --- /dev/null +++ b/shellsui/ui.go @@ -0,0 +1,47 @@ +package shellsui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "github.com/Shells-com/shells-go/res" +) + +var logoContainer *fyne.Container + +func getLogoContainer() *fyne.Container { + if logoContainer == nil { + logo := canvas.NewImageFromResource(res.ShellsLogo) + logo.SetMinSize(fyne.Size{Width: 242, Height: 76}) + + cLogo := container.New(layout.NewCenterLayout(), logo) + + logoContainer = container.New( + layout.NewBorderLayout( + nil, + GetMarginRectangleWithHeight(40), + nil, + nil, + ), + cLogo, + ) + } + + return logoContainer +} + +func GetMainContainer(content *fyne.Container) *fyne.Container { + return container.New( + layout.NewBorderLayout( + GetMarginRectangle(), + GetMarginRectangle(), + GetMarginRectangle(), + GetMarginRectangle(), + ), + container.NewVBox( + getLogoContainer(), + content, + ), + ) +} diff --git a/snappy.go b/snappy.go new file mode 100644 index 0000000..e375f9e --- /dev/null +++ b/snappy.go @@ -0,0 +1,53 @@ +package main + +import ( + "net" + "time" + + "github.com/golang/snappy" +) + +type snappyConn struct { + c net.Conn + + sr *snappy.Reader + sw *snappy.Writer +} + +func (s *snappyConn) Close() error { + return s.c.Close() +} + +func (s *snappyConn) LocalAddr() net.Addr { + return s.c.LocalAddr() +} + +func (s *snappyConn) RemoteAddr() net.Addr { + return s.c.RemoteAddr() +} + +func (s *snappyConn) Read(b []byte) (int, error) { + if s.sr == nil { + s.sr = snappy.NewReader(s.c) + } + return s.sr.Read(b) +} + +func (s *snappyConn) Write(b []byte) (int, error) { + if s.sw == nil { + s.sw = snappy.NewWriter(s.c) + } + return s.sw.Write(b) +} + +func (s *snappyConn) SetDeadline(t time.Time) error { + return s.c.SetDeadline(t) +} + +func (s *snappyConn) SetReadDeadline(t time.Time) error { + return s.c.SetReadDeadline(t) +} + +func (s *snappyConn) SetWriteDeadline(t time.Time) error { + return s.c.SetWriteDeadline(t) +} diff --git a/spicefyne/clipboard.go b/spicefyne/clipboard.go new file mode 100644 index 0000000..f946c42 --- /dev/null +++ b/spicefyne/clipboard.go @@ -0,0 +1,126 @@ +package spicefyne + +import ( + "bytes" + "context" + "errors" + "image/png" + "log" + "time" + + "github.com/KarpelesLab/goclip" + "github.com/Shells-com/spice" +) + +type clipOption struct { + spice *SpiceFyne + selection spice.SpiceClipboardSelection + clipboardType spice.SpiceClipboardFormat +} + +func (o *clipOption) Type() goclip.Type { + switch o.clipboardType { + case spice.VD_AGENT_CLIPBOARD_UTF8_TEXT: + return goclip.Text + case spice.VD_AGENT_CLIPBOARD_IMAGE_PNG: + return goclip.Image + case spice.VD_AGENT_CLIPBOARD_IMAGE_BMP: + return goclip.Image + case spice.VD_AGENT_CLIPBOARD_IMAGE_TIFF: + return goclip.Image + case spice.VD_AGENT_CLIPBOARD_IMAGE_JPG: + return goclip.Image + default: + return goclip.Invalid + } +} + +func (o *clipOption) Mime() string { + switch o.clipboardType { + case spice.VD_AGENT_CLIPBOARD_UTF8_TEXT: + return "text/plain;charset=utf-8" + case spice.VD_AGENT_CLIPBOARD_IMAGE_PNG: + return "image/png" + case spice.VD_AGENT_CLIPBOARD_IMAGE_BMP: + return "image/bmp" + case spice.VD_AGENT_CLIPBOARD_IMAGE_TIFF: + return "image/tiff" + case spice.VD_AGENT_CLIPBOARD_IMAGE_JPG: + return "image/jpeg" + default: + return "application/octet-string" + } +} + +func (o *clipOption) Data(ctx context.Context) ([]byte, error) { + // fetch from spice + return o.spice.main.RequestClipboard(o.selection, o.clipboardType) +} + +func (s *SpiceFyne) ClipboardGrabbed(selection spice.SpiceClipboardSelection, clipboardTypes []spice.SpiceClipboardFormat) { + //s.main.RequestClipboard(selection, clipboardTypes[0]) + var opts []goclip.DataOption + + for _, opt := range clipboardTypes { + opts = append(opts, &clipOption{selection: selection, clipboardType: opt, spice: s}) + } + + data := &goclip.StaticData{TargetBoard: board_vd2go(selection), Options: opts} + err := goclip.CopyTo(context.Background(), board_vd2go(selection), data) + if err != nil { + log.Printf("copy operation failed: %s", err) + } +} + +func (s *SpiceFyne) ClipboardFetch(selection spice.SpiceClipboardSelection, clipboardType spice.SpiceClipboardFormat) ([]byte, error) { + ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) + + data, err := goclip.PasteFrom(ctx, board_vd2go(selection)) + if err != nil { + log.Printf("failed to paste: %s", err) + return nil, err + } + + //log.Printf("grabbed data from goclip: %+v", data) + + // typically we do not want to offer formats such as bmp or tiff as these are heavy + + switch clipboardType { + case spice.VD_AGENT_CLIPBOARD_UTF8_TEXT: + res, err := data.ToText(ctx) + if err != nil { + return nil, err + } + return []byte(res), nil + case spice.VD_AGENT_CLIPBOARD_IMAGE_PNG: + res, err := data.GetFormat(ctx, "image/png") + if err == nil { + return res, nil + } + log.Printf("spicefyne: failed to fetch png, will try to convert: %s", err) + + // fetch image, convert + img, err := data.ToImage(ctx) + if err != nil { + return nil, err + } + buf := &bytes.Buffer{} + err = png.Encode(buf, img) + if err != nil { + return nil, err + } + return buf.Bytes(), nil + case spice.VD_AGENT_CLIPBOARD_IMAGE_BMP: + return data.GetFormat(ctx, "image/bmp") + case spice.VD_AGENT_CLIPBOARD_IMAGE_TIFF: + return data.GetFormat(ctx, "image/tiff") + case spice.VD_AGENT_CLIPBOARD_IMAGE_JPG: + return data.GetFormat(ctx, "image/jpeg") + default: + return nil, errors.New("unsupported format") + } +} + +func (s *SpiceFyne) ClipboardRelease(selection spice.SpiceClipboardSelection) { + log.Printf("spicefyne: TODO release clipboard") +} diff --git a/spicefyne/const.go b/spicefyne/const.go new file mode 100644 index 0000000..dbe897b --- /dev/null +++ b/spicefyne/const.go @@ -0,0 +1,11 @@ +package spicefyne + +const SideBarWidth = 300 +const SideBarPadding = 10 +const SideBarIconButton = 30 + +const MuteIconSize float32 = 30 +const MuteIconMargin float32 = 20 + +const PreferencesKeyMute = "PreferencesKeyMute" +const PreferencesKeyPosition = "PreferencesKeyPosition" diff --git a/spicefyne/control.go b/spicefyne/control.go new file mode 100644 index 0000000..6bad83b --- /dev/null +++ b/spicefyne/control.go @@ -0,0 +1,180 @@ +package spicefyne + +import ( + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + resources "github.com/Shells-com/shells-go/res" +) + +type control struct { + main *fyne.Container + setW chan int + visible bool + parent *SpiceFyne + sidebarPosition string + tgtW float32 + + muteIconContainer *fyne.Container + sidebar *fyne.Container + expBtn *widget.Button + expBtnContainer *fyne.Container + zoomBtn *widget.Button + muteIcon *widget.Icon +} + +func newControl(w *SpiceFyne) *control { + res := &control{ + setW: make(chan int), + parent: w, + } + + pos := w.a.Preferences().String(PreferencesKeyPosition) + if len(pos) == 0 { + pos = "left" + } + res.sidebarPosition = pos + + // expand button + res.expBtn = widget.NewButtonWithIcon("", resources.ShellsIcon, res.toggle) + res.expBtn.Importance = widget.LowImportance + res.expBtn.Resize(fyne.NewSize(SideBarIconButton, SideBarIconButton)) + + sidebar := buildLayout(res) + res.sidebar = sidebar + + expBtnContainer := container.NewCenter(res.expBtn) + res.expBtnContainer = expBtnContainer + + if res.sidebarPosition == "left" { + c := container.NewHBox(sidebar, expBtnContainer) + res.main = c + } else { + c := container.NewHBox(expBtnContainer, sidebar) + res.main = c + } + + isMuted := w.a.Preferences().Bool(PreferencesKeyMute) + muteIcon := widget.NewIcon(theme.VolumeMuteIcon()) + muteIcon.Hidden = !isMuted + res.muteIcon = muteIcon + lmi := NewMuteIconLayout() + res.muteIconContainer = container.New(lmi, muteIcon) + + go res.anim() + + return res +} + +func (c *control) anim() { + t := time.NewTicker(10 * time.Millisecond) + tgtW := (0 - c.main.Size().Width) + SideBarWidth + + // anim task + for { + select { + case w := <-c.setW: + tgtW = float32(w) + t.Reset(10 * time.Millisecond) + case <-t.C: + // get width + p := c.main.Position() + if p.X == tgtW { + t.Stop() + break + } + + // update width + if c.sidebarPosition == "left" { + if p.X > tgtW { + // need to reduce + p.X -= 7 + if p.X < tgtW { + p.X = tgtW + } + c.main.Move(p) + } else { + p.X += 7 + if p.X > tgtW { + p.X = tgtW + } + c.main.Move(p) + } + } else { + if p.X < tgtW { + // need to increase + p.X += 7 + if p.X > tgtW { + p.X = tgtW + } + c.main.Move(p) + } else { + p.X -= 7 + if p.X < tgtW { + p.X = tgtW + } + c.main.Move(p) + } + } + + } + } +} + +// set visibility and calculate where the main container will go with the anim +func (c *control) toggle() { + c.visible = !c.visible + if c.visible { + if c.sidebarPosition == "left" { + c.setW <- 0 + } else { + c.setW <- int(c.parent.Size().Width - SideBarIconButton - SideBarWidth) + } + } else { + if c.sidebarPosition == "left" { + c.setW <- int((0 - c.main.Size().Width) + SideBarIconButton) + } else { + c.setW <- int(c.parent.size.Width - SideBarIconButton) + } + } +} + +func (c *control) zoom() { + if c.parent.zoomMode { + c.parent.zoomMode = false + } else { + c.parent.zoomMode = true + c.toggle() + } +} + +func (c *control) mute() { + c.parent.Client.ToggleMute() + isMuted := c.parent.Client.GetMute() + c.parent.a.Preferences().SetBool(PreferencesKeyMute, isMuted) + c.muteIcon.Hidden = !isMuted +} + +func (c *control) changePosition(direction string) { + c.sidebarPosition = direction + c.parent.a.Preferences().SetString(PreferencesKeyPosition, direction) + ps := c.main.Position() + size := c.parent.Size() + // to set the button on the right/left side, we must reorder the sidebar/button order in the container + c.main.Remove(c.sidebar) + c.main.Remove(c.expBtnContainer) + if direction == "left" { + ps.X = 0 + c.main.Add(c.sidebar) + c.main.Add(c.expBtnContainer) + } else { + ps.X = size.Width - SideBarWidth - SideBarIconButton + c.main.Add(c.expBtnContainer) + c.main.Add(c.sidebar) + } + // move the main container to the right/left side in the open state + c.main.Move(ps) +} diff --git a/spicefyne/cursor.go b/spicefyne/cursor.go new file mode 100644 index 0000000..e8a6e38 --- /dev/null +++ b/spicefyne/cursor.go @@ -0,0 +1,12 @@ +package spicefyne + +import "image" + +type fyneCursor struct { + img image.Image + x, y int +} + +func (f *fyneCursor) Image() (image.Image, int, int) { + return f.img, f.x, f.y +} diff --git a/spicefyne/layout.go b/spicefyne/layout.go new file mode 100644 index 0000000..1570f29 --- /dev/null +++ b/spicefyne/layout.go @@ -0,0 +1,142 @@ +package spicefyne + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +type autoLayout struct { + direction *string + sidebarVisible *bool +} + +func (l *autoLayout) Layout(o []fyne.CanvasObject, s fyne.Size) { + first := true + for _, obj := range o { + if first { + obj.Resize(s) + first = false + continue + } + obj.Resize(fyne.Size{Width: obj.Size().Width, Height: s.Height}) + h := obj.Size().Height + var c fyne.Position + var vsize float32 + if *l.sidebarVisible { + vsize = float32(SideBarWidth) + } else { + vsize = float32(0) + } + if *l.direction == "left" { + c = fyne.NewPos(SideBarIconButton-obj.Size().Width+vsize, (s.Height-h)/2) + } else { + c = fyne.NewPos(s.Width-SideBarIconButton-vsize, (s.Height-h)/2) + } + obj.Move(c) + } +} + +func (l *autoLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + return fyne.Size{Width: 320, Height: 200} +} + +type centerHLayout struct { +} + +func NewCenterHLayout() fyne.Layout { + return ¢erHLayout{} +} + +func (c *centerHLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + for _, child := range objects { + childMin := child.MinSize() + child.Resize(childMin) + child.Move(fyne.NewPos(SideBarPadding, 0)) + } +} + +func (c *centerHLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + minSize := fyne.NewSize(0, 0) + for _, child := range objects { + if !child.Visible() { + continue + } + + minSize = minSize.Max(child.MinSize()) + } + + return minSize +} + +type sidebarLayout struct { +} + +func NewSidebarLayout() fyne.Layout { + return &sidebarLayout{} +} + +func (s *sidebarLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + total := float32(0) + for _, child := range objects { + if !child.Visible() { + continue + } + + total += child.MinSize().Height + } + + x, y := float32(0), float32(0) + + for _, child := range objects { + if !child.Visible() { + continue + } + + height := child.MinSize().Height + child.Move(fyne.NewPos(x, y)) + y += theme.Padding() + height + child.Resize(fyne.NewSize(SideBarWidth-(SideBarPadding*2), height)) + } +} + +func (s *sidebarLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + minSize := fyne.NewSize(0, 0) + addPadding := false + + for _, child := range objects { + if !child.Visible() { + continue + } + + minSize.Width = fyne.Max(child.MinSize().Width, minSize.Width) + minSize.Height += child.MinSize().Height + if addPadding { + minSize.Height += theme.Padding() + } + addPadding = true + + } + + return minSize +} + +// as to be use as child of fyne.Window +type muteIconLayout struct { +} + +func NewMuteIconLayout() fyne.Layout { + return &muteIconLayout{} +} + +func (s *muteIconLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + for _, child := range objects { + child.Resize(fyne.NewSize(MuteIconSize, MuteIconSize)) + x := size.Width - MuteIconSize - MuteIconMargin + y := size.Height - MuteIconSize - MuteIconMargin + child.Move(fyne.NewPos(x, y)) + } +} + +func (s *muteIconLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + return fyne.NewSize(MuteIconSize, MuteIconSize) +} diff --git a/spicefyne/renderer.go b/spicefyne/renderer.go new file mode 100644 index 0000000..a74f18b --- /dev/null +++ b/spicefyne/renderer.go @@ -0,0 +1,40 @@ +package spicefyne + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +type renderer struct { + s *SpiceFyne +} + +func (r *renderer) Layout(s fyne.Size) { + r.s.size = s + r.s.UpdateSize(s) + r.s.output.Move(fyne.Position{0, 0}) + r.s.output.Resize(s) +} + +func (r *renderer) MinSize() fyne.Size { + return fyne.Size{320, 200} +} + +func (r *renderer) Refresh() { +} + +func (r *renderer) BackgroundColor() color.Color { + return color.RGBA{0, 0, 0, 0xff} +} + +func (r *renderer) Objects() []fyne.CanvasObject { + if curs := r.s.cursor; curs != nil { + return []fyne.CanvasObject{r.s.output, curs} + } else { + return []fyne.CanvasObject{r.s.output} + } +} + +func (r *renderer) Destroy() { +} diff --git a/spicefyne/scancode.go b/spicefyne/scancode.go new file mode 100644 index 0000000..cddf536 --- /dev/null +++ b/spicefyne/scancode.go @@ -0,0 +1,105 @@ +package spicefyne + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/desktop" +) + +// fyne → AT scancode table +// see: http://www.philipstorr.id.au/pcbook/book3/scancode.htm +var fyneScancodeMap = map[fyne.KeyName][]byte{ + fyne.KeyEscape: []byte{0x01}, + fyne.KeyTab: []byte{0x0f}, + fyne.KeyBackspace: []byte{0x0e}, + fyne.KeyInsert: []byte{0xe0, 0x52}, + fyne.KeyDelete: []byte{0xe0, 0x73}, + fyne.KeyRight: []byte{0xe0, 0x4d}, + fyne.KeyLeft: []byte{0xe0, 0x4b}, + fyne.KeyDown: []byte{0xe0, 0x50}, + fyne.KeyUp: []byte{0xe0, 0x48}, + fyne.KeyPageUp: []byte{0xe0, 0x49}, + fyne.KeyPageDown: []byte{0xe0, 0x51}, + fyne.KeyHome: []byte{0xe0, 0x47}, + fyne.KeyEnd: []byte{0xe0, 0x4f}, + + fyne.KeyF1: []byte{0x3b}, + fyne.KeyF2: []byte{0x3c}, + fyne.KeyF3: []byte{0x3d}, + fyne.KeyF4: []byte{0x3e}, + fyne.KeyF5: []byte{0x3f}, + fyne.KeyF6: []byte{0x40}, + fyne.KeyF7: []byte{0x41}, + fyne.KeyF8: []byte{0x42}, + fyne.KeyF9: []byte{0x43}, + fyne.KeyF10: []byte{0x44}, + fyne.KeyF11: []byte{0x57}, + fyne.KeyF12: []byte{0x58}, + + fyne.Key1: []byte{0x02}, + fyne.Key2: []byte{0x03}, + fyne.Key3: []byte{0x04}, + fyne.Key4: []byte{0x05}, + fyne.Key5: []byte{0x06}, + fyne.Key6: []byte{0x07}, + fyne.Key7: []byte{0x08}, + fyne.Key8: []byte{0x09}, + fyne.Key9: []byte{0x0a}, + fyne.Key0: []byte{0x0b}, + + fyne.KeyA: []byte{0x1e}, + fyne.KeyB: []byte{0x30}, + fyne.KeyC: []byte{0x2e}, + fyne.KeyD: []byte{0x20}, + fyne.KeyE: []byte{0x12}, + fyne.KeyF: []byte{0x21}, + fyne.KeyG: []byte{0x22}, + fyne.KeyH: []byte{0x23}, + fyne.KeyI: []byte{0x17}, + fyne.KeyJ: []byte{0x24}, + fyne.KeyK: []byte{0x25}, + fyne.KeyL: []byte{0x26}, + fyne.KeyM: []byte{0x32}, + fyne.KeyN: []byte{0x31}, + fyne.KeyO: []byte{0x18}, + fyne.KeyP: []byte{0x19}, + fyne.KeyQ: []byte{0x10}, + fyne.KeyR: []byte{0x13}, + fyne.KeyS: []byte{0x1f}, + fyne.KeyT: []byte{0x14}, + fyne.KeyU: []byte{0x16}, + fyne.KeyV: []byte{0x2f}, + fyne.KeyW: []byte{0x11}, + fyne.KeyX: []byte{0x2d}, + fyne.KeyY: []byte{0x15}, + fyne.KeyZ: []byte{0x2c}, + + fyne.KeySpace: []byte{0x39}, + fyne.KeyApostrophe: []byte{0x28}, // Quote + fyne.KeyComma: []byte{0x33}, + fyne.KeyMinus: []byte{0x0c}, + fyne.KeyPeriod: []byte{0x34}, + fyne.KeySlash: []byte{0x35}, + fyne.KeyBackslash: []byte{0x2b}, + fyne.KeyLeftBracket: []byte{0x1a}, + fyne.KeyRightBracket: []byte{0x1b}, + fyne.KeySemicolon: []byte{0x27}, + fyne.KeyEqual: []byte{0x0d}, + fyne.KeyAsterisk: []byte{0x37}, + fyne.KeyPlus: []byte{0x4e}, + fyne.KeyBackTick: []byte{0x29}, // Backquote + + fyne.KeyReturn: []byte{0xe0, 0x1c}, + fyne.KeyEnter: []byte{0x1c}, + desktop.KeyShiftLeft: []byte{0x2a}, + desktop.KeyShiftRight: []byte{0x36}, + desktop.KeyControlLeft: []byte{0x1d}, + desktop.KeyControlRight: []byte{0xe0, 0x1d}, + desktop.KeyAltLeft: []byte{0x38}, + desktop.KeyAltRight: []byte{0xe0, 0x38}, + desktop.KeyCapsLock: []byte{0x3a}, + desktop.KeyMenu: []byte{0xe0, 0x5d}, + desktop.KeyPrintScreen: []byte{0xe0, 0x2a, 0xe0, 0x37}, + + desktop.KeySuperLeft: []byte{0xe0, 0x5b}, + desktop.KeySuperRight: []byte{0xe0, 0x5c}, +} diff --git a/spicefyne/scancode_desktop.go b/spicefyne/scancode_desktop.go new file mode 100644 index 0000000..0468d8f --- /dev/null +++ b/spicefyne/scancode_desktop.go @@ -0,0 +1,129 @@ +package spicefyne + +import ( + "github.com/go-gl/glfw/v3.3/glfw" +) + +// glfw → XT scancode table +// see: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html +var glfwScancodeMap = map[glfw.Key][]byte{ + glfw.KeySpace: []byte{0x39}, + glfw.KeyApostrophe: []byte{0x28}, // Quote + glfw.KeyComma: []byte{0x33}, + glfw.KeyMinus: []byte{0x0c}, + glfw.KeyPeriod: []byte{0x34}, + glfw.KeySlash: []byte{0x35}, + + glfw.Key0: []byte{0x0b}, + glfw.Key1: []byte{0x02}, + glfw.Key2: []byte{0x03}, + glfw.Key3: []byte{0x04}, + glfw.Key4: []byte{0x05}, + glfw.Key5: []byte{0x06}, + glfw.Key6: []byte{0x07}, + glfw.Key7: []byte{0x08}, + glfw.Key8: []byte{0x09}, + glfw.Key9: []byte{0x0a}, + + glfw.KeySemicolon: []byte{0x27}, + glfw.KeyEqual: []byte{0x0d}, + + glfw.KeyA: []byte{0x1e}, + glfw.KeyB: []byte{0x30}, + glfw.KeyC: []byte{0x2e}, + glfw.KeyD: []byte{0x20}, + glfw.KeyE: []byte{0x12}, + glfw.KeyF: []byte{0x21}, + glfw.KeyG: []byte{0x22}, + glfw.KeyH: []byte{0x23}, + glfw.KeyI: []byte{0x17}, + glfw.KeyJ: []byte{0x24}, + glfw.KeyK: []byte{0x25}, + glfw.KeyL: []byte{0x26}, + glfw.KeyM: []byte{0x32}, + glfw.KeyN: []byte{0x31}, + glfw.KeyO: []byte{0x18}, + glfw.KeyP: []byte{0x19}, + glfw.KeyQ: []byte{0x10}, + glfw.KeyR: []byte{0x13}, + glfw.KeyS: []byte{0x1f}, + glfw.KeyT: []byte{0x14}, + glfw.KeyU: []byte{0x16}, + glfw.KeyV: []byte{0x2f}, + glfw.KeyW: []byte{0x11}, + glfw.KeyX: []byte{0x2d}, + glfw.KeyY: []byte{0x15}, + glfw.KeyZ: []byte{0x2c}, + + glfw.KeyLeftBracket: []byte{0x1a}, + glfw.KeyBackslash: []byte{0x2b}, + glfw.KeyRightBracket: []byte{0x1b}, + glfw.KeyGraveAccent: []byte{0x29}, // Backquote + + glfw.KeyWorld1: nil, + glfw.KeyWorld2: nil, + + glfw.KeyEscape: []byte{0x01}, + glfw.KeyEnter: []byte{0x1c}, + glfw.KeyTab: []byte{0x0f}, + glfw.KeyBackspace: []byte{0x0e}, + glfw.KeyInsert: []byte{0xe0, 0x52}, + glfw.KeyDelete: []byte{0xe0, 0x73}, + glfw.KeyRight: []byte{0xe0, 0x4d}, + glfw.KeyLeft: []byte{0xe0, 0x4b}, + glfw.KeyDown: []byte{0xe0, 0x50}, + glfw.KeyUp: []byte{0xe0, 0x48}, + glfw.KeyPageUp: []byte{0xe0, 0x49}, + glfw.KeyPageDown: []byte{0xe0, 0x51}, + glfw.KeyHome: []byte{0xe0, 0x47}, + glfw.KeyEnd: []byte{0xe0, 0x4f}, + + glfw.KeyCapsLock: []byte{0x39}, + glfw.KeyScrollLock: []byte{0x46}, + glfw.KeyNumLock: []byte{0x45}, + glfw.KeyPrintScreen: []byte{0xe0, 0x2a, 0xe0, 0x37}, + glfw.KeyPause: nil, // []byte{0xe1, 0x1d, 0x45, 0xe1, 0x9d, 0xc5}, + + glfw.KeyF1: []byte{0x3b}, + glfw.KeyF2: []byte{0x3c}, + glfw.KeyF3: []byte{0x3d}, + glfw.KeyF4: []byte{0x3e}, + glfw.KeyF5: []byte{0x3f}, + glfw.KeyF6: []byte{0x40}, + glfw.KeyF7: []byte{0x41}, + glfw.KeyF8: []byte{0x42}, + glfw.KeyF9: []byte{0x43}, + glfw.KeyF10: []byte{0x44}, + glfw.KeyF11: []byte{0x57}, + glfw.KeyF12: []byte{0x58}, + // up to F25 + + glfw.KeyKP0: []byte{0x52}, + glfw.KeyKP1: []byte{0x4f}, + glfw.KeyKP2: []byte{0x50}, + glfw.KeyKP3: []byte{0x51}, + glfw.KeyKP4: []byte{0x4b}, + glfw.KeyKP5: []byte{0x4c}, + glfw.KeyKP6: []byte{0x4d}, + glfw.KeyKP7: []byte{0x47}, + glfw.KeyKP8: []byte{0x48}, + glfw.KeyKP9: []byte{0x49}, + + glfw.KeyKPDecimal: []byte{0x53}, + glfw.KeyKPDivide: []byte{0xe0, 0x35}, + glfw.KeyKPMultiply: []byte{0x37}, + glfw.KeyKPSubtract: []byte{0x4a}, + glfw.KeyKPAdd: []byte{0x4e}, + glfw.KeyKPEnter: []byte{0xe0, 0x1c}, + glfw.KeyKPEqual: nil, // ?? []byte{}, + + glfw.KeyLeftShift: []byte{0x2a}, + glfw.KeyLeftControl: []byte{0x1d}, + glfw.KeyLeftAlt: []byte{0x38}, + glfw.KeyLeftSuper: []byte{0xe0, 0x5b}, + glfw.KeyRightShift: []byte{0x36}, + glfw.KeyRightControl: []byte{0xe0, 0x1d}, + glfw.KeyRightAlt: []byte{0xe0, 0x38}, + glfw.KeyRightSuper: []byte{0xe0, 0x5c}, + glfw.KeyMenu: []byte{0xe0, 0x5d}, +} diff --git a/spicefyne/sidebar.go b/spicefyne/sidebar.go new file mode 100644 index 0000000..b859847 --- /dev/null +++ b/spicefyne/sidebar.go @@ -0,0 +1,111 @@ +package spicefyne + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/Shells-com/shells-go/shellsui" +) + +func buildItem(labelText string, btnOptions btnOptions) (*widget.Button, *fyne.Container) { + t := widget.NewLabelWithStyle(labelText, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + t.Wrapping = fyne.TextWrapWord + + var b *widget.Button + + if btnOptions.icon != nil { + b = widget.NewButtonWithIcon(btnOptions.text, btnOptions.icon, btnOptions.cb) + } else { + b = widget.NewButton(btnOptions.text, btnOptions.cb) + } + + lh := layout.NewHBoxLayout() + lh.Layout([]fyne.CanvasObject{t, b}, fyne.Size{Width: SideBarWidth}) + + sbl := NewSidebarLayout() + sbc := container.New(sbl, t, b, shellsui.GetMarginRectangleWithHeight(10)) + return b, container.NewVBox(sbc) +} + +func buildItemWithToggle(labelText string, btnOptions btnOptions) (*shellsui.ToggleButton, *fyne.Container) { + t := widget.NewLabelWithStyle(labelText, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + t.Wrapping = fyne.TextWrapWord + + b := shellsui.NewToggleButton(btnOptions.text, btnOptions.icon, btnOptions.textToggled, btnOptions.iconToggled, btnOptions.initState, btnOptions.cb) + + lh := layout.NewHBoxLayout() + lh.Layout([]fyne.CanvasObject{t, b}, fyne.Size{Width: SideBarWidth}) + + sbl := NewSidebarLayout() + sbc := container.New(sbl, t, b, shellsui.GetMarginRectangleWithHeight(10)) + return b, container.NewVBox(sbc) +} + +func build2IconButtonsItem(labelText string, btnIcon1 fyne.Resource, cb1 func(), btnIcon2 fyne.Resource, cb2 func()) *fyne.Container { + t := widget.NewLabelWithStyle(labelText, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + t.Wrapping = fyne.TextWrapWord + + b1 := widget.NewButtonWithIcon("", btnIcon1, cb1) + b1.Importance = widget.MediumImportance + + b2 := widget.NewButtonWithIcon("", btnIcon2, cb2) + b2.Importance = widget.MediumImportance + + c2b := container.NewCenter(container.New(layout.NewHBoxLayout(), b1, shellsui.GetMarginRectangleWithWidth(20), b2)) + + lh := layout.NewHBoxLayout() + lh.Layout([]fyne.CanvasObject{t, c2b}, fyne.Size{Width: SideBarWidth}) + + sbl := NewSidebarLayout() + sbc := container.New(sbl, t, c2b, shellsui.GetMarginRectangleWithHeight(10)) + return container.NewVBox(sbc) +} + +func buildLayout(c *control) *fyne.Container { + zoomButton, zoom := buildItem("Zoom", btnOptions{text: "Zoom", cb: c.zoom}) + c.zoomBtn = zoomButton + + _, resetZoom := buildItem("Reset Zoom", btnOptions{text: "Reset", cb: c.parent.resetZoom}) + _, exit := buildItem("Return to Shells list", btnOptions{text: "Exit", cb: c.parent.terminate}) + h := c.parent.Size().Height + + _, mute := buildItemWithToggle("Mute", btnOptions{ + text: "Mute", + icon: theme.VolumeMuteIcon(), + textToggled: "Unmute", + iconToggled: theme.VolumeUpIcon(), + cb: c.mute, + initState: c.parent.a.Preferences().Bool(PreferencesKeyMute), + }) + + positions := build2IconButtonsItem("Sidebar Position", theme.NavigateBackIcon(), func() { + c.changePosition("left") + }, theme.NavigateNextIcon(), func() { + c.changePosition("right") + }) + + lv := layout.NewVBoxLayout() + lv.Layout([]fyne.CanvasObject{zoom, resetZoom, mute, positions, exit}, fyne.Size{Height: h}) + cv := container.New(lv, zoom, resetZoom, mute, positions, exit) + + cc := container.New(NewCenterHLayout(), cv) + + bg := canvas.NewRectangle(color.White) + bg.SetMinSize(fyne.NewSize(SideBarWidth, h)) + + return container.NewMax(bg, cc) +} + +type btnOptions struct { + text string + icon fyne.Resource + textToggled string + iconToggled fyne.Resource + cb func() + initState bool +} diff --git a/spicefyne/sizeupd.go b/spicefyne/sizeupd.go new file mode 100644 index 0000000..d37b0b8 --- /dev/null +++ b/spicefyne/sizeupd.go @@ -0,0 +1,74 @@ +package spicefyne + +import ( + "log" + "time" + + "fyne.io/fyne/v2" +) + +func (s *SpiceFyne) szUpdThread() { + t := time.NewTicker(40 * time.Millisecond) + run := false + cnt := 0 + t.Stop() + var siz fyne.Size + + for { + select { + case siz = <-s.szUpd: + //log.Printf("scheduling update size") + cnt = 0 + if !run { + run = true + t.Reset(40 * time.Millisecond) + } + case <-t.C: + if s.lkSize { + t.Stop() + run = false + break + } + //log.Printf("running update size") + if cnt < 10 { + cnt += 1 + break + } + cnt = 0 + t.Stop() + run = false + + w, h := siz.Width, siz.Height + if w < 1280 { + w = 1280 + } + if h < 720 { + h = 720 + } + if w > siz.Width { + // we're going to have to stretch height + zoom := float64(siz.Width) / float64(w) // zoom<1 + h = float32(float64(h) / zoom) + log.Printf("resize: zoom=%f size=%fx%f → %fx%f", zoom, siz.Width, siz.Height, w, h) + } else if h > siz.Height { + // we're going to have to stretch width + zoom := float64(siz.Height) / float64(h) // zoom<1 + w = float32(float64(w) / zoom) + log.Printf("resize: zoom=%f size=%fx%f → %fx%f", zoom, siz.Width, siz.Height, w, h) + } + if w == float32(s.width) && h == float32(s.height) { + break + } + s.Client.UpdateView(int(w), int(h)) + } + } +} + +func (s *SpiceFyne) UpdateSize(siz fyne.Size) { + if !s.init { + return + } + + //log.Printf("update size queued") + s.szUpd <- siz +} diff --git a/spicefyne/utils.go b/spicefyne/utils.go new file mode 100644 index 0000000..d0b7a5e --- /dev/null +++ b/spicefyne/utils.go @@ -0,0 +1,61 @@ +package spicefyne + +import ( + "fmt" + + "github.com/KarpelesLab/goclip" + "github.com/Shells-com/spice" +) + +func GetVDAgentClipboardTypeByFormatType(ft goclip.Type) (spice.SpiceClipboardFormat, error) { + switch ft { + case goclip.Text: + return spice.VD_AGENT_CLIPBOARD_UTF8_TEXT, nil + case goclip.Image: + return spice.VD_AGENT_CLIPBOARD_IMAGE_PNG, nil + } + return spice.VD_AGENT_CLIPBOARD_NONE, fmt.Errorf("spice/clipboard: format type %d is not available", ft) +} + +func board_go2vd(b goclip.Board) spice.SpiceClipboardSelection { + switch b { + case goclip.Default: + return spice.VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD + case goclip.PrimarySelection: + return spice.VD_AGENT_CLIPBOARD_SELECTION_PRIMARY + case goclip.SecondarySelection: + return spice.VD_AGENT_CLIPBOARD_SELECTION_SECONDARY + default: + return spice.VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD + } +} + +func board_vd2go(b spice.SpiceClipboardSelection) goclip.Board { + switch b { + case spice.VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD: + return goclip.Default + case spice.VD_AGENT_CLIPBOARD_SELECTION_PRIMARY: + return goclip.PrimarySelection + case spice.VD_AGENT_CLIPBOARD_SELECTION_SECONDARY: + return goclip.SecondarySelection + default: + return goclip.InvalidBoard + } +} + +func mime_go2sp(mime string) (spice.SpiceClipboardFormat, bool) { + switch mime { + case "text/plain", "text/plain;charset=utf-8": + return spice.VD_AGENT_CLIPBOARD_UTF8_TEXT, true + case "image/png": + return spice.VD_AGENT_CLIPBOARD_IMAGE_PNG, true + case "image/bmp": + return spice.VD_AGENT_CLIPBOARD_IMAGE_BMP, true + case "image/tiff": + return spice.VD_AGENT_CLIPBOARD_IMAGE_TIFF, true + case "image/jpeg": + return spice.VD_AGENT_CLIPBOARD_IMAGE_JPG, true + default: + return spice.VD_AGENT_CLIPBOARD_NONE, false + } +} diff --git a/spicefyne/widget.go b/spicefyne/widget.go new file mode 100644 index 0000000..a88b2d8 --- /dev/null +++ b/spicefyne/widget.go @@ -0,0 +1,488 @@ +package spicefyne + +import ( + "image" + "log" + "math" + "os" + + "github.com/KarpelesLab/goclip" + "github.com/Shells-com/spice" + "github.com/go-gl/glfw/v3.3/glfw" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/widget" +) + +type SpiceFyne struct { + widget.BaseWidget + *spice.Client + + w fyne.Window + a fyne.App + main *spice.ChMain + in *spice.ChInputs + output *canvas.Image + cursor *canvas.Image + width int + height int + size fyne.Size // size as shown. Will need to adapt + zoom float64 + init bool + clip *goclip.Monitor + ctrl *control + + cursorObj *fyneCursor + + expanded bool + + // focus + focused bool + + // mouse + mouseX int + mouseY int + + // scroll + scrollY int + + // hot + hotX, hotY int + + //zoom + zoomed bool + zoomMode bool + zoomedPos fyne.Position + + // size updater + szUpd chan fyne.Size + lkSize bool +} + +func New(w fyne.Window, a fyne.App, c spice.Connector, password string) (*SpiceFyne, error) { + final := &SpiceFyne{w: w, a: a, zoom: 1, szUpd: make(chan fyne.Size, 4)} + final.ExtendBaseWidget(final) + + // create initial dummy image + img := image.NewRGBA(image.Rect(0, 0, 800, 600)) + for p := range img.Pix { + if p%4 == 3 { + img.Pix[p] = 0xff + } else { + img.Pix[p] = 0 + } + } + final.output = canvas.NewImageFromImage(img) + //final.output.ScaleMode = canvas.ImageScalePixels + final.output.FillMode = canvas.ImageFillContain + final.output.ScaleMode = canvas.ImageScaleFastest + + // configure visibility/etc + final.output.Show() + + //final.cursor = canvas.NewImageFromImage(image.NewRGBA(image.Rect(0, 0, 16, 16))) + //final.cursor.Resize(fyne.Size{64, 64}) + //final.cursor.Show() + + final.ctrl = newControl(final) + + spiceClient, err := spice.New(c, final, password) + if err != nil { + return nil, err + } + final.Client = spiceClient + go final.szUpdThread() + + w.SetPadded(false) + w.SetContent(container.NewMax(container.New(&autoLayout{direction: &final.ctrl.sidebarPosition, sidebarVisible: &final.ctrl.visible}, final, final.ctrl.main), final.ctrl.muteIconContainer)) + w.Canvas().Focus(final) + + if mon, err := goclip.NewMonitor(); err == nil { + final.clip = mon + mon.Subscribe(func(data goclip.Data) error { + // grab clipboard now + var types []spice.SpiceClipboardFormat + typSkip := make(map[spice.SpiceClipboardFormat]bool) + + fmts, err := data.GetAllFormats() + if err != nil { + log.Printf("spicefyne: failed to fetch clipboard infos: %s", err) + return nil + } + + log.Printf("spicefyne: got clipboard %s", data) + + if data.Type() == goclip.Image { + // always have png in priority if image + types = append(types, spice.VD_AGENT_CLIPBOARD_IMAGE_PNG) + typSkip[spice.VD_AGENT_CLIPBOARD_IMAGE_PNG] = true + } + + for _, fmt := range fmts { + fmt, ok := mime_go2sp(fmt.Mime()) + if !ok { + continue + } + if _, found := typSkip[fmt]; found { + continue + } + + types = append(types, fmt) + typSkip[fmt] = true + } + + final.main.SendGrabClipboard(board_go2vd(data.Board()), types) + return nil + }) + } + + return final, nil +} + +func (s *SpiceFyne) DisplayInit(img image.Image) { + s.output.Image = img + bounds := img.Bounds() + s.width = bounds.Dx() + s.height = bounds.Dy() + + if !s.init { + s.w.Resize(fyne.Size{Width: float32(s.width), Height: float32(s.height)}) + s.w.CenterOnScreen() + s.init = true + + // call updatesize with the window size in case it's different + s.UpdateSize(s.w.Canvas().Size()) + } + canvas.Refresh(s.output) +} + +func (s *SpiceFyne) DisplayRefresh() { + canvas.Refresh(s.output) +} + +func (s *SpiceFyne) SetEventsTarget(in *spice.ChInputs) { + s.in = in +} + +func (s *SpiceFyne) SetMainTarget(main *spice.ChMain) { + s.main = main +} + +func (s *SpiceFyne) CreateRenderer() fyne.WidgetRenderer { + return &renderer{s} +} + +// TODO only care for those events if MouseDown() isn't triggered (ie. mobile) +func (s *SpiceFyne) Tapped(t *fyne.PointEvent) { +} + +func (s *SpiceFyne) TappedSecondary(*fyne.PointEvent) { +} + +func (s *SpiceFyne) relPos(pos fyne.Position) (uint32, uint32) { + if float32(s.width) == s.size.Width && float32(s.height) == s.size.Height { + // easy + s.mouseX, s.mouseY = int(pos.X), int(pos.Y) + s.zoom = 1 + return uint32(pos.X), uint32(pos.Y) + } + x, y := pos.X, pos.Y + + // say I have a 100x100 image shown in a 50x200 canvas.Image, if I have a click at position 25x100 I should be getting 50x50 + ratioA := float64(s.width) / float64(s.height) + ratioB := float64(s.size.Width) / float64(s.size.Height) + + if ratioA > ratioB { + // top/bottom padded, width accurate + zoom := float64(s.size.Width) / float64(s.width) + realHeight := int(float64(s.height) * zoom) + realTop := (s.size.Height - float32(realHeight)) / 2 + s.zoom = zoom + x = float32(float64(x) / zoom) + y = float32(float64(y-realTop) / zoom) + //log.Printf("Ratio A bigger zoom=%f realHeight=%d realTop=%d", zoom, realHeight, realTop) + } else { + // left/right padded + zoom := float64(s.size.Height) / float64(s.height) + realWidth := int(float64(s.width) * zoom) + realLeft := (s.size.Width - float32(realWidth)) / 2 + s.zoom = zoom + x = float32(float64(x-realLeft) / zoom) + y = float32(float64(y) / zoom) + //log.Printf("Ratio B bigger zoom=%f realWidth=%d realLeft=(%d - %d)/2=%d", zoom, realWidth, s.size.Width, realWidth, realLeft) + } + + if x < 0 || y < 0 || x > float32(s.width) || y > float32(s.height) { + return uint32(s.mouseX), uint32(s.mouseY) + } + + s.mouseX, s.mouseY = int(x), int(y) + return uint32(x), uint32(y) +} + +func (s *SpiceFyne) mouseBtn(v desktop.MouseButton) uint8 { + // 0=left, 1=middle, 2=right, 3=up, 4=down, 5=side, 6=extra + switch v { + case desktop.MouseButtonPrimary: + return 0 + case desktop.MouseButtonSecondary: + return 2 + case desktop.MouseButtonTertiary: + return 1 + default: + return 0 + } +} + +func (s *SpiceFyne) zoomIn(x uint32, y uint32) { + size := s.Size() + var posX float32 + var posY float32 + + if float32(x)-size.Width/4 <= 0 { + posX = 0 + } else if float32(x)+size.Width/4 >= size.Width*2 { + posX = -size.Width + } else { + posX = float32(x) - size.Width/2 + if posX > 0 { + posX = -(posX + size.Width/2) + } + } + + if float32(y)-size.Height/4 <= 0 { + posY = 0 + } else if float32(y)+size.Height/4 >= size.Height*2 { + posY = -size.Height + } else { + posY = float32(y) - size.Height/2 + if posY > 0 { + posY = -(posY + size.Height/2) + } + } + s.output.Resize(fyne.Size{Width: size.Width * 2, Height: size.Height * 2}) + s.output.Move(fyne.NewPos(posX, posY)) + s.zoomedPos = fyne.NewPos(posX, posY) +} + +func (s *SpiceFyne) resetZoom() { + size := s.Size() + s.output.Resize(size) + s.output.Move(fyne.NewPos(0, 0)) + s.zoomed = false + s.ctrl.zoomBtn.Enable() +} + +func (s *SpiceFyne) MouseDown(ev *desktop.MouseEvent) { + if in := s.in; in != nil { + x, y := s.relPos(ev.Position) + if s.ctrl.visible { + if x > SideBarWidth { + s.ctrl.toggle() + } + return + } + if s.zoomMode { + s.zoomIn(x, y) + s.zoomMode = false + s.zoomed = true + s.ctrl.zoomBtn.Disable() + return + } + + if s.zoomed { + x = x + uint32(s.zoomedPos.X) + y = y + uint32(s.zoomedPos.Y) + } + + in.MouseDown(s.mouseBtn(ev.Button), x, y) + } +} + +func (s *SpiceFyne) MouseUp(ev *desktop.MouseEvent) { + if in := s.in; in != nil { + x, y := s.relPos(ev.Position) + in.MouseUp(s.mouseBtn(ev.Button), x, y) + } +} + +func (s *SpiceFyne) MouseMoved(ev *desktop.MouseEvent) { + if in := s.in; in != nil { + x, y := s.relPos(ev.Position) + if s.ctrl.visible || s.zoomMode { + return + } + + if s.zoomed { + x = (x + uint32(math.Abs(float64(s.zoomedPos.X)))) / 2 + y = (y + uint32(math.Abs(float64(s.zoomedPos.Y)))) / 2 + pos := ev.Position + pos.X = float32(x) + pos.Y = float32(y) + s.updatedMouse(pos) + + } else { + s.updatedMouse(ev.Position) + } + in.MousePosition(x, y) + } +} + +/* +func (s *SpiceFyne) Dragged(ev *fyne.DragEvent) { + x, y := s.relPos(ev.Position) + log.Printf("mouse dragged, %dx%d", x, y) +} +func (s *SpiceFyne) DragEnd() { +} +*/ + +func (s *SpiceFyne) MouseIn(*desktop.MouseEvent) { +} + +func (s *SpiceFyne) MouseOut() { +} + +func (s *SpiceFyne) Scrolled(ev *fyne.ScrollEvent) { + // scroll happened + log.Printf("scroll dx=%f dy=%f", ev.Scrolled.DX, ev.Scrolled.DY) + + s.scrollY += int(ev.Scrolled.DY) + + if -10 < s.scrollY && s.scrollY < 10 { + return + } + + if in := s.in; in != nil { + x, y := s.relPos(ev.Position) + if s.scrollY > 0 { + // going up + for s.scrollY > 0 { + in.MouseDown(4, x, y) + in.MouseUp(4, x, y) + s.scrollY -= 10 + } + } else if s.scrollY < 0 { + // going down + for s.scrollY < 0 { + in.MouseDown(5, x, y) + in.MouseUp(5, x, y) + s.scrollY += 10 + } + } + } +} + +func (s *SpiceFyne) KeyDown(ev *fyne.KeyEvent) { + if in := s.in; in != nil { + if evk, ok := ev.Sys.(glfw.Key); ok { + if k, ok := glfwScancodeMap[evk]; ok && k != nil { + in.OnKeyDown(k) + return + } + } + k, ok := fyneScancodeMap[ev.Name] + if !ok { + log.Printf("unhandled key down = %s", ev.Name) + return + } + in.OnKeyDown(k) + } +} + +func (s *SpiceFyne) KeyUp(ev *fyne.KeyEvent) { + if in := s.in; in != nil { + if evk, ok := ev.Sys.(glfw.Key); ok { + if k, ok := glfwScancodeMap[evk]; ok && k != nil { + in.OnKeyUp(k) + return + } + } + k, ok := fyneScancodeMap[ev.Name] + if !ok { + log.Printf("unhandled key up = %s", ev.Name) + return + } + in.OnKeyUp(k) + } +} + +// focus related +func (s *SpiceFyne) FocusGained() { + if s.clip != nil { + err := s.clip.Poll() + if err != nil { + log.Printf("spice/goclip: error during poll of clipboard information: %s", err) + } + } + s.focused = true +} + +func (s *SpiceFyne) FocusLost() { + s.focused = false +} + +func (s *SpiceFyne) Focused() bool { + return s.focused +} + +// TODO have a keyboard mode where we just emulate whatever is input by the user +func (s *SpiceFyne) TypedRune(rune) { +} + +func (s *SpiceFyne) TypedKey(*fyne.KeyEvent) { +} + +func (s *SpiceFyne) SetCursor(i image.Image, x, y uint16) { + if s.cursor == nil { + if i == nil { + s.cursorObj = nil + } else { + s.cursorObj = &fyneCursor{i, int(x), int(y)} + } + } else if i == nil { + s.cursor.Image = image.NewRGBA(image.Rect(0, 0, 16, 16)) + } else { + s.cursor.Image = i + s.hotX = int(x) + s.hotY = int(y) + bounds := i.Bounds() + s.cursor.Resize(fyne.Size{float32(bounds.Dx()), float32(bounds.Dy())}) + canvas.Refresh(s.cursor) + } +} + +func (s *SpiceFyne) updatedMouse(p fyne.Position) { + if s.cursor != nil { + s.cursor.Move(p) + } +} + +func (s *SpiceFyne) Cursor() desktop.Cursor { + if s.cursor == nil { + if s.cursorObj == nil { + return desktop.DefaultCursor + } else { + return s.cursorObj + } + } else { + return desktop.HiddenCursor + } +} + +func (s *SpiceFyne) terminate() { + dialog.ShowConfirm("Exit?", "Are you sure you want to quit?", func(f bool) { + if f { + os.Exit(0) + } + }, s.w) +} + +// implement fyne.Tabbable +func (s *SpiceFyne) AcceptsTab() bool { + return true +} diff --git a/theme.go b/theme.go new file mode 100644 index 0000000..7a32c5c --- /dev/null +++ b/theme.go @@ -0,0 +1,134 @@ +package main + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +type shellsTheme struct { + background color.Color + button, disabledButton, text, placeholder, hover, shadow, disabled, scrollBar color.Color + regular, bold, italic, boldItalic, monospace fyne.Resource +} + +func getShellsTheme() fyne.Theme { + r := &shellsTheme{ + background: color.NRGBA{0xff, 0xff, 0xff, 0xff}, + button: color.Transparent, + disabled: color.NRGBA{0x0, 0x0, 0x0, 0x42}, + disabledButton: color.NRGBA{0xe5, 0xe5, 0xe5, 0xff}, + text: color.NRGBA{0x21, 0x21, 0x21, 0xff}, + placeholder: color.NRGBA{0x88, 0x88, 0x88, 0xff}, + hover: color.NRGBA{0x0, 0x0, 0x0, 0x0f}, + scrollBar: color.NRGBA{0x0, 0x0, 0x0, 0x99}, + shadow: color.NRGBA{0x0, 0x0, 0x0, 0x33}, + + regular: theme.DefaultTextFont(), + bold: theme.DefaultTextBoldFont(), + italic: theme.DefaultTextItalicFont(), + boldItalic: theme.DefaultTextBoldItalicFont(), + monospace: theme.DefaultTextMonospaceFont(), + } + return theme.FromLegacy(r) //we will have to implement the new version of theme +} + +func (c shellsTheme) BackgroundColor() color.Color { + return c.background +} + +func (c shellsTheme) ButtonColor() color.Color { + return c.button +} + +func (c shellsTheme) DisabledButtonColor() color.Color { + return c.disabledButton +} + +func (c shellsTheme) HyperlinkColor() color.Color { + // Deprecated: Hyperlinks now use the primary color for consistency. + return c.PrimaryColor() +} + +func (c shellsTheme) TextColor() color.Color { + return c.text +} + +func (c shellsTheme) DisabledTextColor() color.Color { + return c.disabled +} + +func (c shellsTheme) IconColor() color.Color { + // Deprecated: Icons now use the text colour for consistency. + return c.TextColor() +} + +func (c shellsTheme) DisabledIconColor() color.Color { + // Deprecated: Disabled icons match disabled text color for consistency. + return c.DisabledTextColor() +} + +func (c shellsTheme) PlaceHolderColor() color.Color { + return c.placeholder +} + +func (c shellsTheme) PrimaryColor() color.Color { + return color.NRGBA{R: 0x52, G: 0xbd, B: 0x15, A: 0xff} +} + +func (c shellsTheme) HoverColor() color.Color { + return c.hover +} + +func (c shellsTheme) FocusColor() color.Color { + return c.PrimaryColor() +} + +func (c shellsTheme) ScrollBarColor() color.Color { + return c.scrollBar +} + +func (c shellsTheme) ShadowColor() color.Color { + return c.shadow +} + +func (c shellsTheme) TextSize() int { + return 14 +} + +func (c shellsTheme) TextFont() fyne.Resource { + return c.regular +} + +func (c shellsTheme) TextBoldFont() fyne.Resource { + return c.bold +} + +func (c shellsTheme) TextItalicFont() fyne.Resource { + return c.italic +} + +func (c shellsTheme) TextBoldItalicFont() fyne.Resource { + return c.boldItalic +} + +func (c shellsTheme) TextMonospaceFont() fyne.Resource { + return c.monospace +} + +func (c shellsTheme) Padding() int { + return 4 +} + +func (c shellsTheme) IconInlineSize() int { + return 20 +} + +func (c shellsTheme) ScrollBarSize() int { + return 16 +} + +func (c shellsTheme) ScrollBarSmallSize() int { + return 3 +}