diff options
| author | Mitch Riedstra <mitch@riedstra.us> | 2021-03-19 21:19:42 -0400 |
|---|---|---|
| committer | Mitch Riedstra <mitch@riedstra.us> | 2021-03-19 21:19:42 -0400 |
| commit | 0e62a3b46b25e7c101b14ed44235f3c276982fc0 (patch) | |
| tree | fe37304d6e36dcb5ebe1e921a20795c38d476165 | |
| parent | 1e435e039d95ad8834dc3f3cd244ff87d5624c73 (diff) | |
| download | steam-export-0e62a3b46b25e7c101b14ed44235f3c276982fc0.tar.gz steam-export-0e62a3b46b25e7c101b14ed44235f3c276982fc0.tar.xz | |
Sweeping changes to switch to bootstrap and make the UI overall a bit more user friendly
| -rw-r--r-- | cmd/web/app.go | 4 | ||||
| -rw-r--r-- | cmd/web/handlers.go | 11 | ||||
| -rw-r--r-- | cmd/web/main.go | 16 | ||||
| -rw-r--r-- | cmd/web/static/main.js | 14 | ||||
| -rw-r--r-- | cmd/web/static/style.css | 81 | ||||
| -rw-r--r-- | cmd/web/templates/index.html | 466 | ||||
| -rw-r--r-- | readme.md | 9 | ||||
| -rw-r--r-- | steam/steam.go | 8 |
8 files changed, 429 insertions, 180 deletions
diff --git a/cmd/web/app.go b/cmd/web/app.go index 8f6df89..b0749b5 100644 --- a/cmd/web/app.go +++ b/cmd/web/app.go @@ -1,8 +1,8 @@ package main import ( - "sync" "html/template" + "sync" "time" "riedstra.dev/mitch/steam-export/steam" @@ -23,7 +23,6 @@ type statusInfo struct { Start *time.Time } - type App struct { Library *steamLib Status *statusInfo @@ -72,4 +71,3 @@ func (a *App) LibraryReload() { a.LibrarySet(cur) return } - diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index b8fa705..b9b863a 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -1,13 +1,13 @@ package main import ( - "fmt" - "strings" - "net/url" - "os" "encoding/json" + "fmt" "io" "net/http" + "net/url" + "os" + "strings" "time" "github.com/gorilla/mux" @@ -197,11 +197,10 @@ func (a *App) HandleSetLib(w http.ResponseWriter, r *http.Request) { func HandleQuit(w http.ResponseWriter, r *http.Request) { Logger.Println("Quit was called, exiting") w.Header().Add("Content-type", "text/plain") - w.Write([]byte("Shutting down...")) + w.Write([]byte("Shutting down... feel free to close this")) go func() { time.Sleep(time.Second * 2) os.Exit(0) }() return } - diff --git a/cmd/web/main.go b/cmd/web/main.go index 45b64ae..da0bebd 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -5,10 +5,10 @@ import ( "flag" "fmt" "log" - "math/rand" "net/http" "os" - "time" + "strconv" + "strings" "github.com/gorilla/mux" ) @@ -66,8 +66,16 @@ func main() { err = s.ListenAndServe() if err != nil { Logger.Printf("Encountered: %s", err) - rand.Seed(time.Now().UnixNano()) - Listen = fmt.Sprintf(":%d", rand.Intn(9000)+1024) + + parts := strings.Split(Listen, ":") + port, err := strconv.Atoi(parts[1]) + if err != nil { + panic(err) + } + port += 1 + + Listen = fmt.Sprintf("%s:%d", parts[0], port) + Logger.Printf("Trying: %s", Listen) s.Addr = Listen } diff --git a/cmd/web/static/main.js b/cmd/web/static/main.js index 55edf0c..b793be4 100644 --- a/cmd/web/static/main.js +++ b/cmd/web/static/main.js @@ -22,7 +22,8 @@ function formatDuration(dur) { } function setStatus(stat) { - var elem = document.getElementById("installBar"); + var barContainer = document.getElementById("installBarContainer"); + var elem = document.getElementById("installBar"); var msg = document.getElementById("message"); // console.log(stat) @@ -42,7 +43,9 @@ function setStatus(stat) { elem.style.width = percent + "%"; elem.innerHTML = percent + "%"; - elem.style.display = ""; + barContainer.style.display = ""; + + elem.ariaValueNow = percent; // in seconds var eta = Math.round(((stat.Size - trans)/1024/1024) / rate); @@ -56,8 +59,11 @@ function setStatus(stat) { if(!stat.Running && stat.Transferred >= 50 && stat.Error == null) { elem.style.width = 100 + "%"; elem.innerHTML = 100 + "%"; + elem.ariaValueNow = 100; + msg.innerHTML = "Completed install for: " + stat.Url; - elem.style.display = ""; + + barContainer.style.display = ""; msg.style.display = ""; } @@ -65,7 +71,7 @@ function setStatus(stat) { msg.innerHTML = "Errors encountered while installing from: \n\n" + stat.Url + "\n\n" + JSON.stringify(stat.Error, undefined, 2); - elem.style.display = "hidden"; + barContainer.style.display = "hidden"; msg.style.display = ""; } } diff --git a/cmd/web/static/style.css b/cmd/web/static/style.css deleted file mode 100644 index ba648e3..0000000 --- a/cmd/web/static/style.css +++ /dev/null @@ -1,81 +0,0 @@ -nav { - border-bottom: 3px solid #000; - padding-bottom: 10px; -} -a { - text-decoration: none; - color: #268bd2; -} -a:visited { - color: #d22653; -} -a:hover { - text-decoration: underline; -} -pre code { - line-height: 1.1; - overflow: auto; -} - -code { - color: #9672d5; - font-size: .8em; - font-family: "Roboto Mono", "Monaco", "Lucida Console", "DejaVu Sans Mono", -"monospace"; -} - -pre code { - color: #000; - background-color: #FFFFEA; - display: block; - padding: 10px; - border: 1px solid; -} - -blockquote { - border-left: 4px solid #aaa; - padding-left: 1em; -} - -body { - margin: 40px auto; - max-width: 80%; - line-height: 1.6; - font-size: 1em; - color: #444; - padding: 0 10px; - /* Added because some browsers don't default to white */ - background-color: #fff; -} - -img { - width: 100%; - height: auto; -} - -h1,h2,h3 { - line-height: 1.2 -} - -@media screen and (min-width: 1200px) { - body { - max-width: 960px; - } -} - - -#status { - width: 100%; - background-color: #ddd; -} - -.installBar { - width: 0%; - height: 30px; - /* background-color: #4CAF50; */ - background-color: #268bd2; - text-align: center; - line-height: 30px; - color: white; -} - diff --git a/cmd/web/templates/index.html b/cmd/web/templates/index.html index 74f2298..eaaf722 100644 --- a/cmd/web/templates/index.html +++ b/cmd/web/templates/index.html @@ -2,115 +2,417 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <link id="maincss" rel="stylesheet" href="/static/style.css" defer> + <link href="/static/css/bootstrap.min.css" rel="stylesheet" defer> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Steam Game index</title> </head> <body> -<nav> - <a href="/">Home</a> - {{ if .Local }} - <div style="display: block; float: right;"> - <a href="/quit">Shutdown Server / Quit</a> - </div> - {{ end }} +<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> + <div class="container-fluid"> + <a class="navbar-brand" href="/">Steam Exporter</a> + <button class="navbar-toggler" + type="button" + data-bs-toggle="collapse" + data-bs-target="#navbarsExampleDefault" + aria-controls="navbarsExampleDefault" + aria-expanded="false" + aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + + <div class="collapse navbar-collapse" id="navbarsExampleDefault"> + <ul class="navbar-nav me-auto mb-2 mb-md-0"> + <li class="nav-item"> + <a class="nav-link" aria-current="page" + href="/steam-export-web.exe"> + Download + </a> + </li> + <li class="nav-item"> + <a href="#" + id="shareLink" + data-clipboard-text="http://{{$.HostIP}}:{{$.Port}}/" + class="nav-link"> + Copy Share link + </a> + </li> + {{ if .Local }} + <li class="nav-item"> + <a class="nav-link" aria-current="page" + data-bs-target="#installModal" + data-bs-toggle="modal" + href="#"> + Install Game + </a> + </li> + <li class="nav-item"> + <a class="nav-link" aria-current="page" + data-bs-target="#libraryModal" + data-bs-toggle="modal" + href="#"> + Change Library Path + </a> + </li> + {{ end }} + </ul> + + <ul class="navbar-nav "> + {{ if .Local }} + <li class="nav-item"> + <a class="nav-link" href="#" + data-bs-target="#versionModal" + data-bs-toggle="modal" + > + Version + </a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/quit">Shutdown Server/Quit</a> + </li> + {{ end }} + </ul> + </div> + </div> </nav> -{{ if .Local }} -<script src="/static/main.js"></script> -<h2>Library: {{.Lib.Folder}}</h2> +<div class="container" style="padding-top: 6rem;"> -<div id="status"> - <div id="installBar" class="installBar" style="display: none;">0%</div> +<div class="position-fixed bottom-0 end-0 p-3" > + <div id="notificationCopied" class="toast" role="alert" aria-live="assertive" aria-atomic="true"> + <div class="toast-body" id="notificationCopiedText"></div> + </div> </div> -<pre><code id="message" style="display: none;"> -</code></pre> -<br /> - -<h3>About</h3> -<p> -The steam exporter is designed to let you export your steam games, either to -another local hard drive or another computer on the network. -</p> -<p> -It also allows you to import games from across the network as well if you -provide an HTTP url from which to download the game file as exported -from this application. -</p> -<p> -<a href="/steam-export-web.exe"> - You can download this application from this UI as well here. -</a> -</p> - -<p> -You can give people this link to view the library remotely and download -games from your computer: -<br /><br /> -<a href="http://{{.HostIP}}:{{.Port}}/">http://{{.HostIP}}:{{.Port}}/</a> -</p> +{{ if .Local }} + <script src="/static/main.js"></script> + <h2>Library: {{.Lib.Folder}}</h2> + + <div class="row"> + <div id="installBarContainer" class="progress" style="display: none;"> + <div id="installBar" class="progress-bar progress-bar-striped progress-bar-animated" + role="progressbar" + aria-valuenow="0" + aria-valuemin="0" + aria-valuemax="100" + style="width: 0%;"> + </div> + </div> + </div> + + <div class="row" style="padding-top: 10px;"> + <pre><code id="message" style="display: none;"></code></pre> + </div> + + <h3>About</h3> + <p> + The steam exporter is designed to let you export your steam games, either to + another local hard drive or another computer on the network. + </p> + <p> + It also allows you to import games from across the network as well if you + provide an HTTP url from which to download the game file as exported + from this application. + </p> + <p> + <a href="/steam-export-web.exe"> + You can download this application from this UI as well here. + </a> + </p> + + <p> + You can give people this link to view the library remotely and download + games from your computer: + <br /><br /> + <a href="http://{{.HostIP}}:{{.Port}}/">http://{{.HostIP}}:{{.Port}}/</a> + </p> {{ else }} -<h2>Remote Steam library access</h2> + + <h2>Remote Steam library access</h2> + + <p> + <a href="/steam-export-web.exe"> + If you need this program to install the games click here. + </a> + </p> + +{{ end }} -<a href="/steam-export-web.exe"> - If you need this program to install the games click here. -</a> +<h3>Installed games:</h3> -<p> -Right click and copy the link address to paste into your local machine -if you do not wish to store the archive or have enough space for it on -your drive. -</p> -{{ end }} +<div class="container"> + <div class="row"> + <div class="col-3"> + <input class="form-control" type="text" id="gameTableSearch" onkeyup="gameTableFilter()" placeholder="Filter by name"> + </div> + <div class="col-3"> + <button class="btn btn-secondary" onClick="clearGameTableSearch()">Clear</button> + </div> + </div> + + <table id="gameTable" class="table dark table-sm table-striped text-center"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Size</th> + <th scope="col" data-sort-method="none">Actions</th> + </tr> + </thead> + <tbody> + {{ range $key, $val := .Lib.Games}} + <tr> -<p> -Installed games: + <td>{{$key}}</td> + <td>{{$val.GetSize}}</td> + <td> + <a href="/download/{{$key}}" class="btn btn-secondary">Download</a> + <button data-clipboard-text="http://{{$.HostIP}}:{{$.Port}}/download/{{$key}}" class="btn btn-primary">Copy link</button> + <button data-bs-target="#delete{{$val.Slug}}Modal" data-bs-toggle="modal" class="btn btn-danger">Delete</button> + </td> + </tr> + {{ end }} + </tbody> + </table> +</div> + +{{ if .Local }} -Tip: You can right click and save link as to specify a save location, e.g. -an external hard drive -</p> -<ul> {{ range $key, $val := .Lib.Games }} -<li> -<a href="/download/{{$key}}">{{$key}} ({{$val.GetSize}})</a> -</li> +<div class="modal fade" id="delete{{$val.Slug}}Modal" tabindex="-1" aria-labelledby="delete{{$val.Slug}}ModalLabel" aria-hidden="true"> + + <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="delete{{$val.Slug}}ModalLabel"> + Delete {{$key}} + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + + + <form class="row row-cols-lg-auto g-3 align-items-center" action="/delete" method="POST"> + <div class="col-12"> + <div class="input-group"> + <div class="input-group-text"> + Are you sure you wish to delete '{{$key}}'? + </div> + <input type="hidden" name="name" value= "{{$key}}" /> + </div> + </div> + + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <input type="submit" class="btn btn-danger" value="Delete" /> + </div> + </form> + </div> + </div> +</div> + {{ end }} -</ul> -{{ if .Local }} -Delete a game: ( Type out exact name, case sensitive ) -<form action="/delete" method="POST"> - <input type="text" name="name" /> - <input type="submit" value="Delete"> -</form> +<div class="modal fade" id="installModal" tabindex="-1" aria-labelledby="installModalLabel" aria-hidden="true"> + + <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="installModalLabel"> + Install Game + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + -Install a game from a URL or local file path: + <form class="row g-3 align-items-center" action="/install" method="GET"> + <div class="col-12"> + <label class="visually-hidden" for="inlineFormInputInstallModal"> + C:\Program Files (x86)\Steam\Steamapps + </label> + <div class="input-group"> + <div class="input-group-text">URI:</div> + <input name="uri" + type="text" + class="form-control" + id="inlineFormInputInstallModal" + placeholder="https://10.0.0.123:8899/download/Some Game"> + </div> + </div> -<form action="/install" method="GET"> - <input type="text" name="uri" /> - <input type="submit" value="Install"> -</form> + <p> + Enter a URI above, typically this will be another machine on your local network. + </p> -<p> -Note that You can also give someone a URL to install a game if they're running -this program, e.g. <br /> -http://127.0.0.1:8899/install?uri=http://my-server-ip-or-hostname/download/My Game -</p> + <p> + The URI should return a stream with the Game's data, typically this + is provided by running another copy of this program on the computer that has + the game already installed. + </p> + <p> + Simply click "copy link" over there and paste it in here to install. + </p> + + <p> + This will force a reload of the entire steam library when complete. + </p> + + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <input type="submit" class="btn btn-primary" value="Submit" /> + </div> + </form> + </div> + </div> +</div> + +<!-- Modal --> +<div class="modal fade" id="libraryModal" tabindex="-1" aria-labelledby="libraryModalLabel" aria-hidden="true"> + + <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="libraryModalLabel"> + Change Library Path + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + + + <form class="row g-3 align-items-center" action="/setLib" method="GET"> + <div class="col-12"> + <label class="visually-hidden" for="inlineFormInputlibraryModal"> + C:\Program Files (x86)\Steam\Steamapps\common + </label> + <div class="input-group"> + <div class="input-group-text">Path:</div> + <input name="path" + type="text" + class="form-control" + id="inlineFormInputlibraryModal" + placeholder="C:\Program Files (x86)\Steam\Steamapps\common"> + </div> + </div> + + <p> + Typically you will not need to change this, however if you have + installed steam games on multiple hard drives you may need to. + </p> + + <p> + This will force a reload of the entire steam library. + </p> + + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <input type="submit" class="btn btn-primary" value="Submit" /> + </div> + </form> + </div> + </div> +</div> -Change library path -<form action="/setLib" method="GET"> - <input type="text" name="path" /> - <input type="submit" value="Update"> -</form> {{ end }} -<h3>Version information</h3> -<pre><code>{{.Version}}</pre></code> +<div class="modal fade" id="versionModal" tabindex="-1" + aria-labelledby="versionModalLabel" aria-hidden="true"> + + <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="versionModalLabel"> + Version Information + </h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + + <pre><code>{{.Version}}</pre></code> + + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + </div> + </div> + </div> +</div> + + +</div> <!-- /container --> + +<script src="static/js/bootstrap.min.js"></script> +<script src="static/js/clipboard.min.js"></script> +<script src="static/js/tablesort/tablesort.min.js"></script> +<script src="static/js/tablesort/sorts/tablesort.filesize.min.js"></script> +<script> +function clipboardNotify(e) { +/* +console.log("Action:", e.action) +console.log("Text:", e.text) +console.log("Trigger:", e.trigger) +*/ + +var elem = document.getElementById("notificationCopied") +var toast = new bootstrap.Toast(elem) + +var content = document.getElementById("notificationCopiedText") +content.innerHTML = "Copied: " + e.text + +toast.show() +} +</script> +<script> + clip = new ClipboardJS('.btn'); + clip1 = new ClipboardJS('#shareLink'); + + clip.on('success', clipboardNotify); + clip1.on('success', clipboardNotify); + + new Tablesort(document.getElementById("gameTable"), { + }) +</script> +<script> +</script> +<script> + function clearGameTableSearch() { + document.getElementById("gameTableSearch").value="" + gameTableFilter() + } + function gameTableFilter() { + var input, filter, table, tr, td, i, txtValue; + input = document.getElementById("gameTableSearch"); + filter = input.value.toUpperCase(); + table = document.getElementById("gameTable"); + tr = table.getElementsByTagName("tr"); + for (i = 0; i < tr.length; i++) { + td = tr[i].getElementsByTagName("td")[0]; + if (td) { + txtValue = td.textContent || td.innerText; + if (txtValue.toUpperCase().indexOf(filter) > -1) { + tr[i].style.display = ""; + } else { + tr[i].style.display = "none"; + } + } + } + } +</script> </body> @@ -38,3 +38,12 @@ Screenshots: ## Building On Windows I'm using https://www.msys2.org/ for my development environment. + + +### External Tools + +Mostly for the frontend: + + * [Bootstrap](https://getbootstrap.com/) + * [Clipboard.js](https://clipboardjs.com/) + * [Tablesort](http://tristen.ca/tablesort/demo/) diff --git a/steam/steam.go b/steam/steam.go index 24e4c54..aca20df 100644 --- a/steam/steam.go +++ b/steam/steam.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "strings" ) @@ -23,6 +24,13 @@ type Game struct { Size int64 } +var slugregexp = regexp.MustCompile(`[^-0-9A-Za-z_:.]`) + +func (g Game) Slug() string { + // return strings.ReplaceAll(g.Name, " ", "-") + return slugregexp.ReplaceAllString(g.Name, "-") +} + func ProcessMultipleLibraries(r []string) ([]*Library, error) { var libs []*Library for _, i := range r { |
