Initial Commit
This commit is contained in:
commit
6bf84d8b54
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Ignore the binary
|
||||||
|
songbook
|
54
admin.go
Normal file
54
admin.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleAdmin(w http.ResponseWriter, req *http.Request) {
|
||||||
|
page := initPageData(w, req)
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
if !page.LoggedIn {
|
||||||
|
page.show("admin-login.html", w)
|
||||||
|
} else {
|
||||||
|
action := vars["action"]
|
||||||
|
switch action {
|
||||||
|
case "dologin":
|
||||||
|
handleAdminDoLogin(w, req)
|
||||||
|
case "dologout":
|
||||||
|
handleAdminDoLogout(w, req)
|
||||||
|
case "build":
|
||||||
|
default:
|
||||||
|
loadAndShowSongs(w, req, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAdminDoLogin(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
username := req.FormValue("un")
|
||||||
|
password := req.FormValue("pw")
|
||||||
|
var success bool
|
||||||
|
if strings.TrimSpace(username) != "" && strings.TrimSpace(password) != "" {
|
||||||
|
}
|
||||||
|
page.session.setFlashMessage("Login Successful!", "success")
|
||||||
|
redirect("/admin", w, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAdminDoLogout(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
fmt.Fprint(w, "Admin Logout")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAndShowSongs(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSongbook(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||||
|
|
||||||
|
}
|
52
helpers.go
Normal file
52
helpers.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pageData struct {
|
||||||
|
Title string
|
||||||
|
SubTitle string
|
||||||
|
Stylesheets []string
|
||||||
|
HeaderScripts []string
|
||||||
|
Scripts []string
|
||||||
|
FlashMessage string
|
||||||
|
FlashClass string
|
||||||
|
LoggedIn bool
|
||||||
|
session *pageSession
|
||||||
|
Site *siteData
|
||||||
|
|
||||||
|
TemplateData interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageData) show(tmplName string, w http.ResponseWriter) error {
|
||||||
|
for _, tmpl := range []string{
|
||||||
|
"htmlheader.html",
|
||||||
|
"header.html",
|
||||||
|
tmplName,
|
||||||
|
"footer.html",
|
||||||
|
"htmlfooter.html",
|
||||||
|
} {
|
||||||
|
if err := outputTemplate(tmpl, p, w); err != nil {
|
||||||
|
fmt.Printf("%s\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputTemplate
|
||||||
|
// Spit out a template
|
||||||
|
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
||||||
|
n := "/templates/" + tmplName
|
||||||
|
l := template.Must(template.New("layout").Parse(FSMustString(site.DevMode, n)))
|
||||||
|
t := template.Must(l.Parse(FSMustString(site.DevMode, n)))
|
||||||
|
return t.Execute(w, tmplData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect can be used only for GET redirects
|
||||||
|
func redirect(url string, w http.ResponseWriter, req *http.Request) {
|
||||||
|
http.Redirect(w, req, url, 303)
|
||||||
|
}
|
159
main.go
Normal file
159
main.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
//go:generate esc -o assets.go assets templates songs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/justinas/alice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var port int
|
||||||
|
var ip string
|
||||||
|
var dir string
|
||||||
|
|
||||||
|
var sessionSecret = "AChKj7na128CrKPPG0Rc"
|
||||||
|
var sessionStore = sessions.NewCookieStore([]byte(sessionSecret))
|
||||||
|
var r *mux.Router
|
||||||
|
var site *siteData
|
||||||
|
var model *model
|
||||||
|
|
||||||
|
type siteData struct {
|
||||||
|
Title string
|
||||||
|
DevMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const (
|
||||||
|
portDefault = 8080
|
||||||
|
portUsage = "the port to listen on"
|
||||||
|
ipDefault = "127.0.0.1"
|
||||||
|
ipUsage = "the ip mask to listen for connections from"
|
||||||
|
dirDefault = "./"
|
||||||
|
dirUsage = "the directory to serve the app out of\nThere should be a 'songs' directory here."
|
||||||
|
)
|
||||||
|
flag.StringVar(&ip, "ip", ipDefault, ipUsage)
|
||||||
|
flag.IntVar(&port, "port", portDefault, portUsage)
|
||||||
|
flag.StringVar(&dir, "dir", dirDefault, dirUsage)
|
||||||
|
if !strings.HasSuffix(dir, "/") {
|
||||||
|
dir = dir + "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
loadModel()
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.StrictSlash(true)
|
||||||
|
|
||||||
|
//r.PathPrefix("/assets/").Handler(http.FileServer(FS(site.DevMode)))
|
||||||
|
// Public Subrouter
|
||||||
|
pub := r.PathPrefix("/").Subrouter()
|
||||||
|
pub.HandleFunc("/", handleMain)
|
||||||
|
|
||||||
|
admin := r.PathPrefix("/admin").Subrouter()
|
||||||
|
admin.HandleFunc("/", handleAdmin)
|
||||||
|
admin.HandleFunc("/dologin", handleAdminDoLogin)
|
||||||
|
admin.HandleFunc("/dologout", handleAdminDoLogout)
|
||||||
|
admin.HandleFunc("/{action}", handleAdmin)
|
||||||
|
|
||||||
|
http.Handle("/", r)
|
||||||
|
|
||||||
|
chain := alice.New(loggingHandler).Then(r)
|
||||||
|
|
||||||
|
listenAt := fmt.Sprintf("%s:%d", ip, port)
|
||||||
|
fmt.Println("Songbook Server listening at", listenAt)
|
||||||
|
log.Fatal(http.ListenAndServe(listenAt, chain))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadModel() {
|
||||||
|
model := NewModel(dir)
|
||||||
|
site = new(siteData)
|
||||||
|
if !model.hasUser() {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
fmt.Println("Create new Admin user")
|
||||||
|
fmt.Print("Username: ")
|
||||||
|
un, _ := reader.ReadString('\n')
|
||||||
|
un = strings.TrimSpace(un)
|
||||||
|
var pw1, pw2 []byte
|
||||||
|
for string(pw1) != string(pw2) || string(pw1) == "" {
|
||||||
|
fmt.Print("Password: ")
|
||||||
|
pw1, _ = terminal.ReadPassword(0)
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Print("Repeat Password: ")
|
||||||
|
pw2, _ = terminal.ReadPassword(0)
|
||||||
|
fmt.Println("")
|
||||||
|
if string(pw1) != string(pw2) {
|
||||||
|
fmt.Println("Entered Passwords don't match!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMain(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
output, err := ioutil.ReadFile(dir + "currentsongs.html")
|
||||||
|
if err != nil {
|
||||||
|
output = []byte("Error reading current songs")
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func initPageData(w http.ResponseWriter, req *http.Request) *pageData {
|
||||||
|
if site.DevMode {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
}
|
||||||
|
p := new(pageData)
|
||||||
|
// Get the session
|
||||||
|
var err error
|
||||||
|
var s *sessions.Session
|
||||||
|
if s, err = sessionStore.Get(req, "songbook"); err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
p.session = new(pageSession)
|
||||||
|
p.session.session = s
|
||||||
|
p.session.req = req
|
||||||
|
p.session.w = w
|
||||||
|
|
||||||
|
// First check if we're logged in
|
||||||
|
userName, _ := p.session.getStringValue("name")
|
||||||
|
_ = userName
|
||||||
|
|
||||||
|
p.Site = site
|
||||||
|
p.SubTitle = "Songbook"
|
||||||
|
p.Stylesheets = make([]string, 0, 0)
|
||||||
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css")
|
||||||
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/grids-responsive-min.css")
|
||||||
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/font-awesome/css/font-awesome.min.css")
|
||||||
|
p.Stylesheets = append(p.Stylesheets, "/assets/css/gjvote.css")
|
||||||
|
|
||||||
|
p.HeaderScripts = make([]string, 0, 0)
|
||||||
|
|
||||||
|
p.Scripts = make([]string, 0, 0)
|
||||||
|
|
||||||
|
p.FlashMessage, p.FlashClass = p.session.getFlashMessage()
|
||||||
|
if p.FlashClass == "" {
|
||||||
|
p.FlashClass = "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggingHandler(h http.Handler) http.Handler {
|
||||||
|
return handlers.LoggingHandler(os.Stdout, h)
|
||||||
|
}
|
61
model.go
Normal file
61
model.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSongbookConfig(dir string) {
|
||||||
|
sc := &SongbookConfig{ConfigPath: dir + "songbook.toml"}
|
||||||
|
err := sc.Load()
|
||||||
|
return sc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type SongbookConfig struct {
|
||||||
|
Username string `toml:"username"`
|
||||||
|
Password string `toml:"enc_pw"`
|
||||||
|
ConfigPath string `toml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *SongbookConfig) Load() error {
|
||||||
|
tomlData, err := ioutil.ReadFile(sc.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
// Couldn't find the file, save a new one
|
||||||
|
if err = sc.Save(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := toml.Decode(string(tomlData), &sc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *SongbookConfig) Save() error {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := toml.NewEncoder(buf).Encode(sc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(sc.ConfigPath, buf.Bytes(), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModel(dir string) *model {
|
||||||
|
m := new(model)
|
||||||
|
m.config = NewSongbookConfig(dir)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
config *SongbookConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) hasUser() bool {
|
||||||
|
// is there a user in the config?
|
||||||
|
if strings.TrimSpace(m.config.Username) == "" || strings.TrimSpace(m.Config.Password) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
54
page_session.go
Normal file
54
page_session.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is basically a convenience struct for
|
||||||
|
// easier session management (hopefully ;)
|
||||||
|
type pageSession struct {
|
||||||
|
session *sessions.Session
|
||||||
|
req *http.Request
|
||||||
|
w http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageSession) getStringValue(key string) (string, error) {
|
||||||
|
val := p.session.Values[key]
|
||||||
|
var retVal string
|
||||||
|
var ok bool
|
||||||
|
if retVal, ok = val.(string); !ok {
|
||||||
|
return "", fmt.Errorf("Unable to create string from %s", key)
|
||||||
|
}
|
||||||
|
return retVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageSession) setStringValue(key, val string) {
|
||||||
|
p.session.Values[key] = val
|
||||||
|
p.session.Save(p.req, p.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageSession) setFlashMessage(msg, status string) {
|
||||||
|
p.setStringValue("flash_message", msg)
|
||||||
|
p.setStringValue("flash_status", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageSession) getFlashMessage() (string, string) {
|
||||||
|
var err error
|
||||||
|
var msg, status string
|
||||||
|
if msg, err = p.getStringValue("flash_message"); err != nil {
|
||||||
|
return "", "hidden"
|
||||||
|
}
|
||||||
|
if status, err = p.getStringValue("flash_status"); err != nil {
|
||||||
|
return "", "hidden"
|
||||||
|
}
|
||||||
|
p.setFlashMessage("", "hidden")
|
||||||
|
return msg, status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageSession) expireSession() {
|
||||||
|
p.session.Options.MaxAge = -1
|
||||||
|
p.session.Save(p.req, p.w)
|
||||||
|
}
|
23
templates/admin-login.html
Normal file
23
templates/admin-login.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<h1>Admin Login</h1>
|
||||||
|
<div class="center">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/admin/dologin" method="POST">
|
||||||
|
<fieldset>
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="un">Username</label>
|
||||||
|
<input id="un" name="un" type="text" placeholder="username" autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="pw">Password</label>
|
||||||
|
<input id="pw" name="pw" type="password" placeholder="password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-controls">
|
||||||
|
<label for="remember" class="pure-checkbox">
|
||||||
|
<input id="remember" name="remember" type="checkbox"> Remember Me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="pure-button pure-button-primary">Submit</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
11
templates/footer.html
Normal file
11
templates/footer.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
</div>
|
||||||
|
<div id="modal-overlay">
|
||||||
|
<div>
|
||||||
|
<h1 id="modal-title"></h1>
|
||||||
|
<h2 id="modal-subtitle"></h2>
|
||||||
|
<div id="modal-body"></div>
|
||||||
|
<div id="modal-buttons">
|
||||||
|
</div>
|
||||||
|
<div class="reset-pull"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
9
templates/header.html
Normal file
9
templates/header.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="flash center {{.FlashClass}}">
|
||||||
|
{{.FlashMessage}}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{{ if .SubTitle }}
|
||||||
|
<div class="header-menu">
|
||||||
|
<h2>{{.SubTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
6
templates/htmlfooter.html
Normal file
6
templates/htmlfooter.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
</div>
|
||||||
|
{{ range $i, $v := .Scripts }}
|
||||||
|
<script src="{{ $v }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
</body>
|
||||||
|
</html>
|
25
templates/htmlheader.html
Normal file
25
templates/htmlheader.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta http-equiv="Cache-control" content="No-Cache">
|
||||||
|
<link rel="apple-touch-icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
<link rel="shortcut icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
|
||||||
|
<title>{{.Site.Title}} - {{.SubTitle}}</title>
|
||||||
|
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="http://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
{{ range $i, $v := .Stylesheets }}
|
||||||
|
<link rel="stylesheet" href="{{ $v }}">
|
||||||
|
{{ end }}
|
||||||
|
{{ range $i, $v := .HeaderScripts }}
|
||||||
|
<script src="{{ $v }}"></script>
|
||||||
|
{{ end }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="layout">
|
74
templates/song-picker.html
Normal file
74
templates/song-picker.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{{ if not .TemplateData.Songs }}
|
||||||
|
<div>No songs are available</div>
|
||||||
|
{{ else }}
|
||||||
|
<form class="pure-form" action="/admin/build" method="POST">
|
||||||
|
<fieldset>
|
||||||
|
<input id="chosen_songs" name="chosen_songs" value="" type="hidden">
|
||||||
|
<button class="pure-button pure-button-primary" type="submit">Build File</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
<label for="filter">Filter</label>
|
||||||
|
<input onkeyup="updateFilter(this.value);" name="filter" value="" placeholder="Filter Songs">
|
||||||
|
<table id="song-table" class="pure-table pure-table-bordered center">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range $i, $v := .TemplateData.Songs }}
|
||||||
|
<tr data-file="{{$v.Name}}>
|
||||||
|
<td>{{$v.Name}}</td>
|
||||||
|
<td>
|
||||||
|
<a class="pure-button pure-button-secondary" onclick="javascript:toggleFile(this, '{{$v.Name}});">Add</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var selectedFiles = [];
|
||||||
|
var tablerows = document.getElementsByTagName("tr");
|
||||||
|
|
||||||
|
function updateFilter(flt) {
|
||||||
|
flt = flt.toLowerCase();
|
||||||
|
for(var i = 0; i < tablerows.length; i++) {
|
||||||
|
if(tablerows[i].dataset.file.toLowerCase().startsWith(flt)) {
|
||||||
|
tablerows[i].classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
tablerows[i].classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFile(btn, sng) {
|
||||||
|
if(!btn.classList.contains("pure-button-warning")) {
|
||||||
|
btn.classList.remove("pure-button-secondary");
|
||||||
|
btn.classList.add("pure-button-warning");
|
||||||
|
for(var i = 0; i < selectedFiles.length; i++) {
|
||||||
|
if(selectedFiles[i] == sng) { return; }
|
||||||
|
}
|
||||||
|
selectedFiles.push(sng);
|
||||||
|
btn.innerText = "Remove";
|
||||||
|
} else {
|
||||||
|
btn.classList.remove("pure-button-warning");
|
||||||
|
btn.classList.add("pure-button-secondary");
|
||||||
|
for(var i = 0; i < selectedFiles.length; i++) {
|
||||||
|
if(selectedFiles[i] == sng) { delete selectedFiles[i]; }
|
||||||
|
}
|
||||||
|
btn.innerText = "Add";
|
||||||
|
}
|
||||||
|
updateFilesInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSong(sng) {
|
||||||
|
for(var i = 0; i < selectedFiles.length; i++ {
|
||||||
|
if(selectedFiles[i] == sng) { delete selectedFiles[i]; }
|
||||||
|
}
|
||||||
|
updateFilesInput();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue
Block a user