Generate static community page

This commit is contained in:
Trevor Slocum 2020-10-20 13:03:53 -07:00
parent 7b2b4e6aa5
commit a3ef891f45
12 changed files with 1227 additions and 294 deletions

View File

@ -1,5 +1,6 @@
0.1.2:
- Add status page
- Generate static community page
0.1.1:
- Add TimeZone preference

View File

@ -5,6 +5,23 @@
Session repository and community portal for [Meditation Assistant](https://gitlab.com/tslocum/meditationassistant)
## Configure
Example `medinet.conf`:
```yaml
timezone: "America/Los_Angeles"
om: "localhost:10800"
dbdriver: "sqlite3"
dbsource: "/home/medinet/data/medinet.db"
```
## Run
```bash
medinet -c ~/.config/medinet/medinet.conf
```
## Get Support
[Open an issue](https://gitlab.com/tslocum/meditationassistant/issues) describing your problem.

View File

@ -92,6 +92,13 @@ type Session struct {
Modified int `json:"modified"`
}
type RecentSession struct {
Session
AccountID int
AccountName string
AccountEmail string
}
func generateKey() string {
b := make([]rune, AccountKeyLength)
for i := range b {
@ -408,6 +415,16 @@ func (d *Database) scanSession(rows *sql.Rows) (*Session, error) {
return s, nil
}
func (d *Database) scanRecentSession(rows *sql.Rows) (*RecentSession, error) {
s := new(RecentSession)
err := rows.Scan(&s.ID, &s.Posted, &s.Started, &s.StreakDay, &s.Length, &s.Completed, &s.Message, &s.Modified, &s.AccountID, &s.AccountName, &s.AccountEmail)
if err != nil {
return nil, fmt.Errorf("failed to scan session: %s", err)
}
return s, nil
}
func (d *Database) addSession(s Session, updateSessionStarted int, accountID int, appVer string, appMarket string) (bool, error) {
var (
existingSession *Session
@ -510,8 +527,8 @@ func (d *Database) sessionExistsByDate(date time.Time, accountID int, streakBuff
return sessionid > 0, nil
}
func (d *Database) getAllSessions(accountID int) ([]Session, error) {
sessions := []Session{}
func (d *Database) getAllSessions(accountID int) ([]*Session, error) {
var sessions []*Session
rows, err := d.db.Query("SELECT `id`, `posted`, `started`, `streakday`, `length`, `completed`, `message`, `modified` FROM sessions WHERE `account`=?", accountID)
if err != nil {
@ -525,7 +542,28 @@ func (d *Database) getAllSessions(accountID int) ([]Session, error) {
return nil, fmt.Errorf("failed to scan session: %s", err)
}
sessions = append(sessions, *s)
sessions = append(sessions, s)
}
return sessions, nil
}
func (d *Database) getRecentSessions() ([]*RecentSession, error) {
var sessions []*RecentSession
rows, err := d.db.Query("SELECT `sessions`.`id`, `sessions`.`posted`, `sessions`.`started`, `sessions`.`streakday`, `sessions`.`length`, `sessions`.`completed`, `sessions`.`message`, `sessions`.`modified`, `accounts`.`id` AS `accountid`, `accounts`.`name`, `accounts`.`email` FROM `sessions` LEFT OUTER JOIN `accounts` ON `sessions`.`account` = `accounts`.`id` WHERE `accounts`.`sessionspublic` = 1 AND `sessions`.`length` > 110 ORDER BY `sessions`.`completed` DESC LIMIT 50")
if err != nil {
return nil, fmt.Errorf("failed to fetch recent sessions: %s", err)
}
defer rows.Close()
for rows.Next() {
rs, err := d.scanRecentSession(rows)
if err != nil {
return nil, fmt.Errorf("failed to scan recent session: %s", err)
}
sessions = append(sessions, rs)
}
return sessions, nil

2
go.mod
View File

@ -5,6 +5,6 @@ go 1.15
require (
github.com/go-sql-driver/mysql v1.5.0
github.com/jessevdk/go-flags v1.4.0
github.com/mattn/go-sqlite3 v1.14.1
github.com/mattn/go-sqlite3 v1.14.4
gopkg.in/yaml.v2 v2.3.0
)

13
go.sum
View File

@ -1,18 +1,9 @@
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/mattn/go-sqlite3 v1.14.1 h1:AHx9Ra40wIzl+GelgX2X6AWxmT5tfxhI1PL0523HcSw=
github.com/mattn/go-sqlite3 v1.14.1/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=

284
main.go
View File

@ -17,17 +17,12 @@
package main
import (
"encoding/json"
"fmt"
"html"
"io/ioutil"
"log"
"math/rand"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/jessevdk/go-flags"
@ -39,6 +34,7 @@ type Config struct {
DBDriver string
DBSource string
Om string
Web string
}
type Statistics struct {
@ -145,6 +141,8 @@ func main() {
log.Fatal("Specify database driver in configuration file")
} else if config.Om == "" {
log.Fatal("Specify Om host:port in configuration file")
} else if config.Web == "" {
log.Fatal("Specify Web directory in configuration file")
}
tz := "Local"
@ -163,278 +161,6 @@ func main() {
db, err = Connect(config.DBDriver, config.DBSource)
failOnError(err)
http.HandleFunc("/om/community", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/sessions", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/account", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/forum", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/groups", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/status", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "MediNET ONLINE<br><br>Accounts: %d (%d new)<br>Sessions: %d<br>Top streak: %d", len(stats.ActiveAccounts), stats.AccountsCreated, stats.SessionsPosted, stats.TopStreak)
})
http.HandleFunc("/om", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = r.ParseForm()
// Authenticate (signin)
// Send key, client will then call (connect)
token := r.FormValue("token")
if token != "" {
a, err := db.authenticate(token)
if err != nil {
log.Printf("ERROR! failed to authenticate token: %s", err)
return
} else if a == nil {
log.Printf("ERROR! failed to retrieve authenticated account")
return
}
go trackActiveAccount(a.ID)
w.Header().Set("x-MediNET", "connected")
w.Header().Set("x-MediNET-Key", a.Key)
return
}
key := r.URL.Query().Get("x")
if key == "" {
key = r.FormValue("x")
}
a, err := db.getAccount(key)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
if a == nil {
w.Header().Set("x-MediNET", "signin")
logDebugf("Asking to sign in %s %q", key, html.EscapeString(r.URL.RequestURI()))
return
}
go trackActiveAccount(a.ID)
data := make(map[string]interface{})
data["status"] = "success"
action := r.FormValue("action")
tz, err := time.LoadLocation(r.URL.Query().Get("tz"))
if err != nil {
tz, err = time.LoadLocation("UTC")
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
apiver, err := strconv.Atoi(r.URL.Query().Get("v"))
if err != nil {
apiver = 0
}
appver := 0
streakbuffer := -1
av := r.URL.Query().Get("av")
match := regexpMarket.FindStringSubmatch(av)
if len(match) == 2 {
match := regexpNumbers.FindAllString(av[0:len(av)-len(match[1])], -1)
if match != nil {
appver, err = strconv.Atoi(strings.Join(match, ""))
if err != nil {
appver = 0
}
}
} else {
av = "x-" + av
}
appmarket := r.URL.Query().Get("am")
sb := r.URL.Query().Get("buf")
if sb != "" {
streakbuffer, err = strconv.Atoi(sb)
if err != nil {
streakbuffer = -1
}
}
if streakbuffer >= 0 {
if a.StreakBuffer != streakbuffer {
a.StreakBuffer = streakbuffer
go db.updateStreakBuffer(a.ID, streakbuffer)
}
}
// TODO: read from announcement table on successful connect
//data["announce"] = "First line\n\nSecond line"
switch action {
case "deletesession":
data["result"] = "notdeleted"
st := r.FormValue("session")
if st == "" {
break
}
started, err := strconv.Atoi(st)
if err != nil || started == 0 {
log.Printf("failed to read session started when deleting session: %s", err)
return
}
deleted, err := db.deleteSession(started, a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
} else if deleted {
data["result"] = "deleted"
}
case "downloadsessions":
sessions, err := db.getAllSessions(a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
data["downloadsessions"] = sessions
case "uploadsessions":
data["result"] = "corrupt"
u := r.FormValue("uploadsessions")
if u == "" {
break
}
postsession := r.FormValue("postsession")
updateSessionStarted, _ := strconv.Atoi(r.FormValue("editstarted"))
var uploadsessions []Session
err = json.Unmarshal([]byte(u), &uploadsessions)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
data["result"] = "uploaded"
uploaded := false
sessionsuploaded := 0
for _, session := range uploadsessions {
uploaded, err = db.addSession(session, updateSessionStarted, a.ID, av, appmarket)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
if uploaded {
sessionsuploaded++
}
}
data["sessionsuploaded"] = sessionsuploaded
if sessionsuploaded > 0 {
started := 0
streakday := 0
for _, session := range uploadsessions {
if session.Started > started {
started = session.Started
if action != "editposting" {
streakday = session.StreakDay
}
}
}
t := time.Now().In(tz)
if beforeWindowStart(t, streakbuffer) {
t = t.AddDate(0, 0, -1)
}
t = atWindowStart(t, streakbuffer)
if int64(started) >= t.Unix() { // A session was recorded after the start of today's streak window
streak, err := db.calculateStreak(a.ID, a.StreakBuffer, tz)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
logDebugf("NEW SESSION %v - CALCULATED: %d, SUBMITTED: %d", t, streak, streakday)
if streak < streakday {
streak = streakday
} else if streak > streakday {
err = db.setSessionStreakDay(started, streak, a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
err = db.setStreak(streak, a.ID, a.StreakBuffer, tz)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
stats.SessionsPosted++
if streakday > stats.TopStreak {
stats.TopStreak = streakday
}
}
if postsession != "" {
if sessionsuploaded > 0 {
data["result"] = "posted"
} else {
data["result"] = "alreadyposted"
}
}
}
w.Header().Set("x-MediNET", "connected")
// Send streak
if action == "connect" || action == "downloadsessions" || action == "uploadsessions" {
streakday, streakend, topstreak, err := db.getStreak(a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
w.Header().Set("x-MediNET-Streak", fmt.Sprintf("%d,%d", streakday, streakend))
w.Header().Set("x-MediNET-MaxStreak", fmt.Sprintf("%d", topstreak))
}
err = json.NewEncoder(w).Encode(data)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
j, err := json.Marshal(data)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
logDebugf("App: %d API: %d Action: %s - %q", appver, apiver, action, html.EscapeString(r.URL.RequestURI()))
logDebugf("Account ID: %d, JSON: %s", a.ID, string(j))
})
log.Fatal(http.ListenAndServe(config.Om, nil))
initWeb()
listenWeb()
}

83
public/css/holo-dark.css Executable file
View File

@ -0,0 +1,83 @@
button, input[type="button"], input[type="submit"], input[type="reset"] {
background-color:#424343;
border-top-color:#828283;
border-bottom-color:#313132;
color:#FFFFFF;
}
button:focus, input[type="button"]:focus, input[type="submit"]:focus, input[type="reset"]:focus {
-webkit-box-shadow:0px 0px 0px 3px rgba(51,181,229,0.4);
-moz-box-shadow:0px 0px 0px 3px rgba(51,181,229,0.4);
box-shadow:0px 0px 0px 3px rgba(51,181,229,0.4);
}
button:enabled:active, input[type="button"]:enabled:active, input[type="submit"]:enabled:active, input[type="reset"]:enabled:active,
button:enabled.active, input[type="button"]:enabled.active, input[type="submit"]:enabled.active, input[type="reset"]:enabled.active {
background-color:#32A5CF;
background-color:rgba(51,181,229,0.9);
border-top-color:#68BCD9;
border-top-color:rgba(107,201,237,0.9);
border-bottom-color:#2882A2;
border-bottom-color:rgba(41,114,183,0.9);
-webkit-box-shadow:0px 0px 0px 4px rgba(51,181,229,0.4);
-moz-box-shadow:0px 0px 0px 4px rgba(51,181,229,0.4);
box-shadow:0px 0px 0px 4px rgba(51,181,229,0.4);
}
input[type="radio"], input[type="checkbox"] {
border-color:#3C3C3C;
border-color:rgba(204,204,204,0.4);
}
input[type="radio"]:focus, input[type="checkbox"]:focus {
-webkit-box-shadow:0px 0px 0px 2px rgba(51,181,229,0.9);
-moz-box-shadow:0px 0px 0px 2px rgba(51,181,229,0.9);
box-shadow:0px 0px 0px 2px rgba(51,181,229,0.9);
}
input[type="radio"]:enabled:active, input[type="checkbox"]:enabled:active,
input[type="radio"]:enabled.active, input[type="checkbox"]:enabled.active {
-webkit-box-shadow:0px 0px 0px 6px rgba(51,181,229,0.4);
-moz-box-shadow:0px 0px 0px 6px rgba(51,181,229,0.4);
box-shadow:0px 0px 0px 6px rgba(51,181,229,0.4);
background-color:#33B5E5;
background-color:rgba(51,181,229,0.5);
}
input[type="radio"]:enabled:active,
input[type="radio"]:enabled.active {
background:-webkit-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: -moz-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: -ms-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: -o-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
}
input[type="radio"]:checked {
background-color:#0099CC;
background:-webkit-radial-gradient(#33B5E5 3px, /*#94C7DA*/ #2FA3CE 4px, #306172 5px, transparent 6px);
background: -moz-radial-gradient(#33B5E5 3px, /*#94C7DA*/ #2FA3CE 4px, #306172 5px, transparent 6px);
background: -ms-radial-gradient(#33B5E5 3px, /*#94C7DA*/ #2FA3CE 4px, #306172 5px, transparent 6px);
background: -o-radial-gradient(#33B5E5 3px, /*#94C7DA*/ #2FA3CE 4px, #306172 5px, transparent 6px);
background: radial-gradient(#33B5E5 3px, /*#94C7DA*/ #2FA3CE 4px, #306172 5px, transparent 6px);
}
input[type="checkbox"]:checked {
background-color:#0099CC;
/*background:url(check-dark.png) left bottom;*/
background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAARCAMAAAAIRmf1AAAAAXNSR0IArs4c6QAAAmdQTFRFM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7PjKpW9MazaM7XlzMzMzMzMzMzMy8zMxsvNucnPpcbSjMLWdr/aZ73dUbrgM7TkLYKiTI+nOH6YMKrXM7XlzMzMy8zMyszMyszMyczMx8vNwsvNu8nPsMjQpMbTmcTUfcDYLYqsR5u6TLvjTKrNMnuXMKvYM7Xlx8vNM7XlM7XlM7TkLo6yRpu6RLjjObTiLoutJIGjM7XlvcrOxsvNM7XlM7XlM7XlLpC0Rpq5MrLhKpa9IXSSMa3bM7Xls8jQwcrOM7XlM7XlM7XlM7XlM7XlLpK3KpW9InmZMa7dqsfSvcrOMrPiLIGgLYmrM7TkM7XlM7XlM7XlL5S6RZm4RLjiKpW9I3ydMrDeM7XlM7Xlo8bTucnPMrPjLH2bT5y5TZu3Lo+zM7XlM7XlL5i+RZm4I36fMrDfM7Xln8XTtsnQJX6eQZi4RLnjRbjjRZq5L5i/L5rCRJm4RLjiMrHhI36fWrfaQLfjnsXTtcnQM7XlIXKQLZa9M7LhR5q4Rpi3KpW8JH6fMrDflMPVYLzeocbTt8nQMKzZIXWUKpa+QbjkJH6gMrHgM7XloMXTcL7bp8bSusnPMa7cInqaKpe+JH6grMfRhcHYr8jRvsrOM7XlMa/dI3ucKpW8I32fMrHguMnPncXUucrPw8vNMa/dInmZKpS7KZO6InqbMrDfxMvNx8zNM7XlMa3bIHKQIXSSMa/ew8vNM7XlM7XlM7XlzMzMyMzMv8rOyMzNv8rOusrOt8nPNLXlM7XlT49zIwAAAAF0Uk5TAEDm2GYAAAAJcEhZcwAACxMAAAsTAQCanBgAAABmSURBVBjTY2AgCBQwhQyNjNGFbGxP29mjCnnYnvb0whQKQxVKTjntmQrj5OWDyBJkoeqa2joGhsam081wIYbuntMpvX1AoX4kg2Z4np45E1WIgWGx55kzaEIgweY1GH7auo2BSgAALNIh9xzOtlkAAAAASUVORK5CYII=) left bottom;
}
input[type="radio"]:checked:enabled:active,
input[type="radio"]:checked:enabled.active {
background-color:#FFFFFF;
background:-webkit-radial-gradient(#FFFFFF 4px, rgba(51,181,229,0.4) 6px);
background: -moz-radial-gradient(#FFFFFF 4px, rgba(51,181,229,0.4) 6px);
background: -ms-radial-gradient(#FFFFFF 4px, rgba(51,181,229,0.4) 6px);
background: -o-radial-gradient(#FFFFFF 4px, rgba(51,181,229,0.4) 6px);
background: radial-gradient(#FFFFFF 4px, rgba(51,181,229,0.4) 6px);
}
input[type="checkbox"]:checked:enabled:active,
input[type="checkbox"]:checked:enabled.active {
background-color:#FFFFFF;
/*background:rgba(51,181,229,0.4) url(check-active-dark.png) left bottom;*/
background:rgba(51,181,229,0.4) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAARCAYAAAA/mJfHAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAZlJREFUOMvd001LAkEYB/ARkwixg+EHsIKOdY6OfQgxjC5ZURF0CqK3rxCBgdcOWVEdIqOy0C22F22NoLItTah0HF3c1dx1d2e3Q1lLCKVeov99fvPwn2cA+AuZCGWWPbGcr5eA3TVBM2FmM56XXiSsSATkgw4CdlYFzYaZ7XheSirqewRZEeZvspsVQ9NhZvcxJ8ESJCmqHEjylIOAXRVBUxRzEM1JqS9IkQnIXw2QqaWKoMkLxv/ASUgLBSB/PXSCVsseGD9P71GZ4tPCLRu0E7AVAAB6CNgxSTHH95yY/oSwIvuThcjgCdr4btQBAMDoKfIOtzW2W00Gi9VU19Sg162bz9KspV5fb7caW5pNBrMOACArKj5CQtQTy9OqDrjKTjV3yRxmi/j143KVFbGwFs/TNCcy2okOEvz9AIm8P/ayGGHPWRHz6lcvWANhX6Lw4CTR1q9KtgWg0X3HXWhBDRR1kmi7olezE7DFTXNUCRSxIu+/FKL9JFqpasNtfmh0RdgQEuTcznOB7jtOjdT8kcfO0j7wb/IGCSVN6j2UdLQAAAAASUVORK5CYII=) left bottom;
}

83
public/css/holo-light.css Executable file
View File

@ -0,0 +1,83 @@
button, input[type="button"], input[type="submit"], input[type="reset"] {
background-color:#CBCBCB;
border-top-color:#DDDDDD;
border-bottom-color:#959595;
color:#333333;
}
button:focus, input[type="button"]:focus, input[type="submit"]:focus, input[type="reset"]:focus {
-webkit-box-shadow:0px 0px 0px 3px rgba(51,181,229,0.4);
-moz-box-shadow:0px 0px 0px 3px rgba(51,181,229,0.4);
box-shadow:0px 0px 0px 3px rgba(51,181,229,0.4);
}
button:enabled:active, input[type="button"]:enabled:active, input[type="submit"]:enabled:active, input[type="reset"]:enabled:active,
button:enabled.active, input[type="button"]:enabled.active, input[type="submit"]:enabled.active, input[type="reset"]:enabled.active {
background-color:#4BBDE8;
background-color:rgba(51,181,229,0.9);
border-top-color:#7CCFEE;
border-top-color:rgba(107,201,237,0.9);
border-bottom-color:#3B95B5;
border-bottom-color:rgba(41,114,183,0.9);
-webkit-box-shadow:0px 0px 0px 4px rgba(51,181,229,0.4);
-moz-box-shadow:0px 0px 0px 4px rgba(51,181,229,0.4);
box-shadow:0px 0px 0px 4px rgba(51,181,229,0.4);
}
input[type="radio"], input[type="checkbox"] {
border-color:#828282;
border-color:rgba(61,61,61,0.7);
}
input[type="radio"]:focus, input[type="checkbox"]:focus {
-webkit-box-shadow:0px 0px 0px 2px rgba(51,181,229,0.9);
-moz-box-shadow:0px 0px 0px 2px rgba(51,181,229,0.9);
box-shadow:0px 0px 0px 2px rgba(51,181,229,0.9);
}
input[type="radio"]:enabled:active, input[type="checkbox"]:enabled:active,
input[type="radio"]:enabled.active, input[type="checkbox"]:enabled.active {
-webkit-box-shadow:0px 0px 0px 6px rgba(51,181,229,0.4);
-moz-box-shadow:0px 0px 0px 6px rgba(51,181,229,0.4);
box-shadow:0px 0px 0px 6px rgba(51,181,229,0.4);
background-color:#33B5E5;
background-color:rgba(51,181,229,0.5);
}
input[type="radio"]:enabled:active,
input[type="radio"]:enabled.active {
background:-webkit-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: -moz-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: -ms-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: -o-radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
background: radial-gradient(rgba(51,181,229,0.5), rgba(51,181,229,0.4));
}
input[type="radio"]:checked {
background-color:#0099CC;
background:-webkit-radial-gradient(#0099CC 3px, /*#81BDD1*/ #028AB7 4px, rgba(204,204,204,0.4) 5px, transparent 6px);
background: -moz-radial-gradient(#0099CC 3px, /*#81BDD1*/ #028AB7 4px, rgba(204,204,204,0.4) 5px, transparent 6px);
background: -ms-radial-gradient(#0099CC 3px, /*#81BDD1*/ #028AB7 4px, rgba(204,204,204,0.4) 5px, transparent 6px);
background: -o-radial-gradient(#0099CC 3px, /*#81BDD1*/ #028AB7 4px, rgba(204,204,204,0.4) 5px, transparent 6px);
background: radial-gradient(#0099CC 3px, /*#81BDD1*/ #028AB7 4px, rgba(204,204,204,0.4) 5px, transparent 6px);
}
input[type="checkbox"]:checked {
background-color:#0099CC;
/*background:url(check-light.png) left bottom;*/
background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAARCAMAAADnhAzLAAAAAXNSR0IArs4c6QAAAk9QTFRFM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7XlM7PjKpW9MazaM7XlPT09PT09PT09PT9APERIPE1UO1tnOWp8OHaNN4ysM7TkJ3+fN4OdKnaRMKrXM7XlPT09PT0+PT4+PT4/PUBCPUJFPEZKPEtQPFBXOmJxKIepKousIaPOKJa7I3OQMKvYM7XlPT8/M7XlM7XlM7TkKYywKYusFp/MCJrKDHidIX+iM7XlPUNFPT9AM7XlM7XlM7XlKo6yKousFp/NAJbIAX+pEWuLMa3bM7XlPEdLPUBCM7XlM7XlM7XlM7XlM7XlKpC1KoqsF5/NAn+pFHGSMa7dPEpQPUFEMrPiKH+eKoepM7TkM7XlM7XlM7XlK5K4KoqrAn+pFXWXMrDeM7XlM7XlPE5VPUNFMrPjJXqYNI2rM4yqKY2xM7XlM7XlLJa9KoqrF5/MFnaYMrDfM7XlO1FYPERGHnmaIYepF6DNGJ/NKousLJa9LJnAKourF5/MFnaYN36ZNaLLPERHM7XlFWyLBoCpAZfJLIurLIqqFneZMrDfPFJaOH2WPE9WPURGMKzZEW2NAYCqAJbJEp7NMrHgM7XlPE1TOW+DPExSMa7cFHKTAoCqFneZPEdMOl9tPEhNPUFDM7XlMa/dFHSVFnaYMrHgPUNFO1FaPT9BMa/dE3GSAX6nAX2mFHKUMrDfPUBBM7XlMa3bE2qJFG2MMa/ePUBCM7XlM7XlM7XlPT09PT4/PUBDPT4/PUFEPENHPEVJAprMAJnM+n0QmAAAAAF0Uk5TAEDm2GYAAAAJcEhZcwAACxMAAAsTAQCanBgAAABmSURBVBjTY2AgBOQwRHT19NFELCwPW1mjiLi4HnZzRxEJDjnsFooikgQUSYayc3JBZFExQqSquqaWgaGhESHC0NF5OKSrG1mEgWHK1MPTpqGIMDDMm3rkCKoISAzNdiBYt56BCgAA/IUgFE2dD/IAAAAASUVORK5CYII=) left bottom;
}
input[type="radio"]:checked:enabled:active,
input[type="radio"]:checked:enabled.active {
background-color:#3D3D3D;
background:-webkit-radial-gradient(#3D3D3D 4px, rgba(51,181,229,0.4) 6px);
background: -moz-radial-gradient(#3D3D3D 4px, rgba(51,181,229,0.4) 6px);
background: -ms-radial-gradient(#3D3D3D 4px, rgba(51,181,229,0.4) 6px);
background: -o-radial-gradient(#3D3D3D 4px, rgba(51,181,229,0.4) 6px);
background: radial-gradient(#3D3D3D 4px, rgba(51,181,229,0.4) 6px);
}
input[type="checkbox"]:checked:enabled:active,
input[type="checkbox"]:checked:enabled.active {
background-color:#3D3D3D;
/*background:rgba(51,181,229,0.4) url(check-active-light.png) left bottom;*/
background:rgba(51,181,229,0.4) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAARCAYAAADQWvz5AAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAW9JREFUOMvV0ztLglEYB/D/8bylEQR9hGgpupE31Kx0cWmJhpKooaKhljYbIhHMbGnoqqW4hH4Eacuw4gWhXUpKqEgio6QgzJ4GRRO7KA7Rfzzn8IPncoC/TPdOaEFtWaWqELnncEY3NEq9BiNppi15TFIR4g2bZPuebX53BWTeIMROoXTsEQCwshHfcbMs6D7niTgAAriAdIsWonuZAQAvF2qqzSR54rKAtPZAdDtYRaXphieIJy4KSJseomupqBoJACi8R0rt+CypVgIlk9CNTBK/jQGUQ9r7IG7ZS1rC5L6TxrqgKylJxEH1DXg1jiEyb2YAoDNPEb85+4T0Q9y0fdlXQZK6T7LUAwACe36E9MAPtW2XhGgE/DqaQ2qQ7jBA3LB+OxwGACqnn6ShANjLU/aUC8B7poB0GiCuW3+ccP5S5QyQNOQvYEAW6TJCXFv8dU2KHhRhFSBfRuUMkH5gkDRzdqr6Uyq8YRP+fT4ATkiPMbwnoLkAAAAASUVORK5CYII=) left bottom;
}

567
public/css/holo.css Executable file
View File

@ -0,0 +1,567 @@
/*
Holo
inspired by Android 4.0 Ice Cream Sandwich
Dark & Light
5 colors: red, green, blue, purple, orange
---
The MIT License (MIT)
Copyright (c) 2012 Vezquex
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.holo, .holo input, .holo textarea, .holo select {
margin: 0;
font: 16px sans-serif, 'Roboto';
/*line-height: 150%;*/
}
.holo { overflow-x:hidden; }
.holo::-webkit-scrollbar, .holo ::-webkit-scrollbar { width: 16px; }
.holo a { text-decoration:none; }
.holo p, .holo .p, .holo-content h1 { padding: 0 24px 8px; margin: 0; }
.holo-content h1 { padding-top: 24px; padding-bottom: 16px;}
.holo p:first-child { /*padding-top: 16px;*/ }
.holo .holo-buttons { /*padding: 0 16px;*/ }
.holo button { cursor:pointer; }
.holo .button { background:0; border:0; }
.holo .holo-basic-button { }
.holo .holo-borderless-button { }
.holo-pane { }
.holo-grid-list { }
.holo-slider { }
.holo-progress { }
.holo-label { display:block; height: 48px; cursor:pointer; margin: 0 -16px; padding: 0 32px; }
/*.holo-switch input { display:none; }
.holo-switch label:before,
.holo-switch label:after
{
height: 24px;
line-height: 24px;
margin: 12px 0;
font-size: 12px;
width: 48px;
text-align:center;
display:block;
float:right;
}
.holo-switch label:after { content:"OFF"; }
.holo-switch label:before { content:""; }
.holo-switch :checked + label:after { content:""; }
.holo-switch :checked + label:before { content:"ON"; }*/
.holo-radio label { }
.holo-radio input { display:none; }
.holo-radio label:before
{
height: 24px;
line-height: 24px;
margin: 10px 0 0;
width: 24px;
border: 2px solid;
border-radius: 24px;
display:block;
float:right;
content:"";
}
.holo-radio :checked + label:before { background:#888; }
.holo-popup { }
.holo-toast { }
.holo-picker { }
.holo-divider { border-bottom: 1px solid; font-size: 16px; line-height: 36px; height: 38px; padding: 7px 7px 0; text-transform:uppercase; margin: 0px; }
.holo-micro { font-size:12px; }
.holo-small { font-size:14px; }
.holo-medium { font-size:16px; }
.holo-large { font-size:18px; }
.holo-icon { height:48px; width: 48px; }
.holo-app-icon { }
.holo-action-buttons { padding: 0; position: absolute; top:0; right: 0; background-repeat:no-repeat; background-position: center center; }
.holo-action-overflow { background-image: url(images/black/ic_action_overflow.png); }
.holo-search { background-image: url(images/black/ic_action_search.png); }
.holo-action-overflow, .holo-search { color:transparent!important; }
.holo-action-buttons { margin:0; }
.holo-action-buttons li { display:inline; }
.holo-action-buttons a { float:left; line-height:48px; width: 48px; text-align:center; font-size: 32px; }
.holo-field {
margin: 8px 17px;
position: relative;
padding: 0 1px;
}
.holo-field input, .holo-field textarea {
border-width:0 0 1px;
border-style: solid;
line-height:36px;
padding: 0 4px;
box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box;
width:100%;
position:relative;
margin: 0 1px;
display:block;
}
.holo-field input {
height: 32px;
}
.holo-field-bracket { height: 8px; position:absolute; bottom:0px; width:100%; background: #000; }
.holo-grid-row { height:48px; }
.holo-status-bar { height: 24px; }
.holo-notification-icon { height:24px; width: 24px; }
.holo-action-bar { border-bottom: 2px solid; overflow: hidden; width: 100%; z-index: 100; }
.holo-action-bar h1 { margin: 0; line-height: 48px; font-size: 24px; font-weight: normal; }
.holo-action-bar h1 a { display: inline-block; padding: 0 16px; }
.holo-action-bar h1 a.holo-up:before { content:""; width:24px; height: 48px; margin-left: -16px; display:block; float:left; background:url(images/black/ic_up.png) no-repeat center center; }
.holo-action-bar li { list-style:none; }
.holo-fixed-tabs {
height: 48px;
}
.holo-fixed-tabs ul {
margin:0; padding:0;
}
.holo-fixed-tabs li {
text-align: center;
display:inline; list-style: none;
min-width: 120px;
float:left;
}
.holo-fixed-tabs li + li {
border-left: rgba(128,128,128,.5) 1px solid;
}
.holo-fixed-tabs a {
min-width: 120px;
border-width: 0 0 6px;
padding-top: 6px;
padding-bottom: 4px;
border-style: solid;
border-color: transparent;
display:block;
font-weight: bold;
text-transform:uppercase;
line-height: 32px;
margin-right: -1px;
}
.holo-fixed-tabs a.current-tab {
border-color: inherit;
}
.holo-scrollable-tabs {
overflow-x: auto;
}
.holo-list { padding:0; margin:0; min-width: 120px; }
.holo-list li { list-style:none; margin: 0px; padding: 0px; padding-top: 7px;}
.holo-list li:nth-of-type(1) { padding-top: 0px;}
.holo-list li + li { border-top: 1px solid; }
.holo-list li a { display:block; padding: 0 24px; margin: 0 -24px; }
.holo-list li > div { margin: 0 -16px; }
.holo-top { border-bottom: 2px solid; }
.holo-spinner, select.holo-spinner { margin: 0 16px; }
select.holo-spinner, .holo-spinner > a { height:31px; min-width: 120px; display:inline-block; border: #444 solid; border-width:0 0 1px; height: 36px; }
select.holo-spinner { background:0; padding: 0; }
select.holo-spinner option { height: 32px; border-top: 1px #444 solid; }
.holo-spinner > a { position:relative; padding-right: 1em; line-height: 36px; }
.holo-spinner > a:after {
border-right: 6px solid;
border-bottom: 6px solid;
border-top: transparent 6px solid;
border-left: transparent 6px solid;
width:0; height:0;
right: 0; bottom: 0;
position:absolute;
content:"";
}
.holo-spinner ul { display:none; position:absolute; left: 0; }
/*.holo-spinner > a:focus + ul, .holo-spinner:hover ul { display:block; }*/
.holo-spinner li { min-width: 128px; }
.holo-spinner li a { display:block; padding: 0 8px; margin: 0 -8px; }
.holo-buttons button { /*display:table-cell; min-width: 120px;*/ }
.holo-buttons.holo-plain button { border: 0; margin: 0; /*line-height: 48px;*/ }
.holo-buttons.holo-plain button + button { border-left: 1px #444 solid; }
.holo-button {
/*display: block;*/
/*margin:10px 0;*/
/*padding: 12px 40px;*/
border-top: 1px solid rgba(255,255,255,0.34);
border-right: 1px solid rgba(0,0,0,0.04);
border-bottom: 1px solid rgba(0,0,0,0.43);
border-left: 1px solid rgba(0,0,0,0.04);
box-shadow: 0 1px 1px rgba(0,0,0,0.25);
border-radius: 1px;
color: white;
text-decoration: none;
text-align: center;
}
.holo-buttons :active,
.holo-buttons button:hover,
.holo-buttons button:focus,
.holo-button:focus,
.holo-button:hover
{
box-shadow: 0 0 0 4px rgba(50,165,207,0.5);
outline:0;
}
.holo-button.active,
.holo-buttons :active
{
background-color: #32a5cf!important;
}
/*@media screen and (min-height: 45em) { 720px
.holo-action-bar { position:fixed; top: 0; }
.holo-content { margin-top: 96px; }
body { padding-top: 96px; }
}
@media screen and (min-width: 45em) { 720px
.holo-action-bar h1, .holo-fixed-tabs { float:left; }
.holo-fixed-tabs { width: auto; }
}
@media screen and (min-height: 45em) and (min-width: 45em) { 720px
.holo-content { margin-top: 48px; }
body { padding-top: 48px; }
}*/
/* Dark */
.holo-dark,
.holo-dark .holo-list,
.holo-dark input,
.holo-dark textarea,
.holo-dark select,
.holo-dark button,
.holo-dark .holo-action-bar
{
/*background: #000;*/
}
.holo-dark,
.holo-dark a,
.holo-dark input,
.holo-dark textarea,
.holo-dark button,
.holo-dark select.holo-spinner
{
color: #FFF;
}
.holo-dark .holo-field input:disabled { color:#444; }
.holo-dark .holo-button {
background-color: rgba(255,255,255,0.205);
}
.holo-dark .holo-list li { border-color: #303030; }
.holo-dark .holo-divider { border-color: #303030; color: #C6C6C6; }
.holo-dark .holo-label:hover,
.holo-dark .holo-spinner a:hover
{ background: #111; }
.holo-dark .holo-spinner .holo-list
{ background: #222; }
.holo-dark .holo-field input, .holo-dark .holo-field textarea { border-color: #444; }
.holo-dark .holo-field-bracket { background: #444; }
.holo-dark ::-webkit-input-placeholder {
font-style:italic;
margin-bottom: -4px;
}
.holo-dark :-moz-placeholder {
font-style:italic;
margin-bottom: -4px;
}
.holo-dark .holo-switch label:before { background:#444; }
.holo-dark .holo-switch label:after { background:#333; }
.holo-dark .holo-switch :checked + label:before { background:#888; }
.holo-dark .holo-field input, .holo-dark .holo-field textarea { background: #000; }
.holo-dark .holo-radio label:before { border-color:#444; }
.holo-dark .holo-spinner > a:after {
border-right-color: #444;
border-bottom-color: #444;
}
.holo-dark .holo-action-buttons,
.holo-dark .holo-action-bar h1 a.holo-up:before {
opacity:.8;
}
.holo-dark .holo-action-overflow { background-image: url(images/white/ic_action_overflow.png); }
.holo-dark .holo-search { background-image: url(images/white/ic_action_search.png); }
.holo-dark .holo-action-bar h1 a.holo-up:before { background-image:url(images/white/ic_up.png); }
/*.holo-dark::-webkit-scrollbar-track-piece, .holo-dark::-webkit-scrollbar-corner,
.holo-dark ::-webkit-scrollbar-track-piece, .holo-dark ::-webkit-scrollbar-corner
{ background:#000; }
.holo-dark::-webkit-scrollbar-thumb,
.holo-dark ::-webkit-scrollbar-thumb
{ background:#222; }*/
/* Light */
.holo-light,
.holo-light .holo-list,
.holo-light input,
.holo-light textarea,
.holo-light select,
.holo-light button,
.holo-light .holo-action-bar {
/*background-color: #EEE;*/
}
.holo-light, .holo-light a, .holo-light input, .holo-light button {
color: #222;
}
.holo-light .holo-field input:disabled { color:#BBB; }
.holo-light .holo-button.active,
.holo-light .holo-button:hover {
box-shadow: 0 0 0 4px rgba(50,165,207,0.4);
}
.holo-light .holo-switch label:before { background:#CCC; }
.holo-light .holo-switch :checked + label:before { background:#BBB; }
.holo-light .holo-switch label:after { background:#BBB; }
.holo-light .holo-switch :checked + label:before { background:#888; }
.holo-light .holo-radio label:before { border-color:#BBB; }
.holo-light .holo-radio :checked + label:before { background:#33B5E5; }
.holo-light .holo-action-bar-icon { color: #333; opacity: .6; }
.holo-light .holo-button {
background-color: rgba(0,0,0,0.15);
color: #333;
}
.holo-light .holo-list li,
.holo-light .holo-divider,
.holo-light .holo-field input,
.holo-light .holo-field textarea,
.holo-light select.holo-spinner,
.holo-light .holo-spinner > a
{
border-color: #BBB;
}
.holo-light .holo-spinner > a:after {
border-right-color: #BBB;
border-bottom-color: #BBB;
}
.holo-light .holo-field-bracket {
background: #BBB;
}
.holo-light .holo-spinner .holo-list
{ background: #E5E5E5; }
.holo-light .holo-divider { color: #555; }
.holo-light .holo-label:hover,
.holo-light .holo-spinner a:hover
{ background: #DDD; }
.holo-light input::-webkit-input-placeholder {
font-style: italic;
margin-bottom: -4px;
}
.holo-light input:-moz-placeholder {
font-style: italic;
margin-bottom: -4px;
}
.holo-light .holo-action-buttons,
.holo-light .holo-action-bar h1 a.holo-up:before {
opacity:.6;
}
/*.holo-light::-webkit-scrollbar-track-piece, .holo-light::-webkit-scrollbar-corner { background:#EEE; }
.holo-light::-webkit-scrollbar-thumb { background:#CCC; }*/
/* Colors */
.holo-red { color: #FF4444; }
.holo-green { color: #99CC00; }
.holo-blue { color: #33B5E5; }
.holo-purple { color: #AA66CC; }
.holo-orange { color: #FFBB33; }
.holo-accent-red .holo-action-bar,
.holo-accent-red .holo-fixed-tabs a.current-tab
{
border-color: #FF4444;
}
.holo-accent-red a:hover{
background-color:rgba(255,68,68,.25);
}
.holo-accent-red .holo-switch :checked + label:before,
.holo-accent-red .holo-radio :checked + label:before
{
background-color:rgba(255,68,68,.5);
}
.holo-accent-red .holo-fixed-tabs a:hover {
border-color: rgba(255,68,68,.5);
}
.holo-accent-green a:hover{
background-color:rgba(153,204,0,.25);
}
.holo-accent-green .holo-action-bar,
.holo-accent-green .holo-fixed-tabs a.current-tab
{
border-color: #99CC00;
}
.holo-accent-green .holo-switch :checked + label:before,
.holo-accent-green .holo-radio :checked + label:before
{
background-color:rgba(153,204,0,.5);
}
.holo-accent-green .holo-fixed-tabs a:hover {
border-color: rgba(153,204,0,.5);
}
.holo-accent-blue a:hover{
background-color:rgba(51,181,229,.25);
}
.holo-accent-blue .holo-action-bar,
.holo-accent-blue .holo-fixed-tabs a.current-tab
{
border-color: #33B5E5;
}
.holo-accent-blue .holo-switch :checked + label:before,
.holo-accent-blue .holo-radio :checked + label:before
{
background-color:rgba(51,181,229,.67);
}
.holo-accent-blue .holo-fixed-tabs a:hover {
border-color: rgba(51,181,229,.5);
}
.holo-accent-purple a:hover{
background-color:rgba(170,102,204,.25);
}
.holo-accent-purple .holo-action-bar,
.holo-accent-purple .holo-fixed-tabs a.current-tab
{
border-color: #AA66CC;
}
.holo-accent-purple .holo-switch :checked + label:before,
.holo-accent-purple .holo-radio :checked + label:before
{
background-color:rgba(170,102,204,.5);
}
.holo-accent-purple .holo-fixed-tabs a:hover {
border-color: rgba(170,102,204,.5);
}
.holo-accent-orange a:hover{
background-color:rgba(255,187,51,.25);
}
.holo-accent-orange .holo-action-bar,
.holo-accent-orange .holo-fixed-tabs a.current-tab
{
border-color: #FFBB33;
}
.holo-accent-orange .holo-switch :checked + label:before,
.holo-accent-orange .holo-radio :checked + label:before
{
background-color:rgba(255,187,51,.5) !important;
}
.holo-accent-orange .holo-fixed-tabs a:hover {
border-color: rgba(255,187,51,.5) !important;
}
/* holo-web */
button, input[type="button"], input[type="submit"], input[type="reset"], input[type="radio"], input[type="checkbox"] {
outline-style:none;
outline-width:0px;
}
button, input[type="button"], input[type="submit"], input[type="reset"] {
margin:6px;
padding:4px 12px;
border-width:1px;
border-style:solid;
border-color:transparent;
-webkit-border-radius:1px;
-moz-border-radius:1px;
border-radius:1px;
outline-style:none;
outline-width:0px;
font-family:Roboto, "Droid Sans", Arial, Helvetica, sans-serif;
font-size:11pt;
/*-webkit-box-shadow:0px 0px 1px 0px rgba(0,0,0,0.2);
-moz-box-shadow:0px 0px 1px 0px rgba(0,0,0,0.2);
box-shadow:0px 0px 1px 0px rgba(0,0,0,0.2);*/
}
input[type="radio"], input[type="checkbox"] {
-webkit-appearance:none;
-o-appearance:none;
appearance:none;
margin:10px;
width:18px;
height:18px;
border-style:solid;
border-width:1px;
background-color:transparent;
}
input[type="radio"] {
-webkit-border-radius:26px;
-moz-border-radius:26px;
border-radius:26px;
}
input[type="checkbox"] {
-webkit-border-radius:1px;
-moz-border-radius:1px;
border-radius:1px;
}
*:disabled {
opacity:0.3;
}
/* CUSTOM */
/*.holo-dark .holo-field input[type="checkbox"] { color: rgba(51,181,229,.67); border-color: rgba(51,181,229,.67); }*/

7
public/css/medinet.css Executable file
View File

@ -0,0 +1,7 @@
html, body {
margin: 0px;
padding: 2px;
}
td {
white-space: normal;
}

298
web.go Normal file
View File

@ -0,0 +1,298 @@
package main
import (
"encoding/json"
"fmt"
"html"
"log"
"net/http"
"strconv"
"strings"
"time"
)
var updateCommunity = make(chan struct{})
func handleMediNET(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = r.ParseForm()
// Authenticate (signin)
// Send key, client will then call (connect)
token := r.FormValue("token")
if token != "" {
a, err := db.authenticate(token)
if err != nil {
log.Printf("ERROR! failed to authenticate token: %s", err)
return
} else if a == nil {
log.Printf("ERROR! failed to retrieve authenticated account")
return
}
go trackActiveAccount(a.ID)
w.Header().Set("x-MediNET", "connected")
w.Header().Set("x-MediNET-Key", a.Key)
return
}
key := r.URL.Query().Get("x")
if key == "" {
key = r.FormValue("x")
}
a, err := db.getAccount(key)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
if a == nil {
w.Header().Set("x-MediNET", "signin")
logDebugf("Asking to sign in %s %q", key, html.EscapeString(r.URL.RequestURI()))
return
}
go trackActiveAccount(a.ID)
data := make(map[string]interface{})
data["status"] = "success"
action := r.FormValue("action")
tz, err := time.LoadLocation(r.URL.Query().Get("tz"))
if err != nil {
tz, err = time.LoadLocation("UTC")
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
apiver, err := strconv.Atoi(r.URL.Query().Get("v"))
if err != nil {
apiver = 0
}
appver := 0
streakbuffer := -1
av := r.URL.Query().Get("av")
match := regexpMarket.FindStringSubmatch(av)
if len(match) == 2 {
match := regexpNumbers.FindAllString(av[0:len(av)-len(match[1])], -1)
if match != nil {
appver, err = strconv.Atoi(strings.Join(match, ""))
if err != nil {
appver = 0
}
}
} else {
av = "x-" + av
}
appmarket := r.URL.Query().Get("am")
sb := r.URL.Query().Get("buf")
if sb != "" {
streakbuffer, err = strconv.Atoi(sb)
if err != nil {
streakbuffer = -1
}
}
if streakbuffer >= 0 {
if a.StreakBuffer != streakbuffer {
a.StreakBuffer = streakbuffer
go db.updateStreakBuffer(a.ID, streakbuffer)
}
}
// TODO: read from announcement table on successful connect
//data["announce"] = "First line\n\nSecond line"
switch action {
case "deletesession":
data["result"] = "notdeleted"
st := r.FormValue("session")
if st == "" {
break
}
started, err := strconv.Atoi(st)
if err != nil || started == 0 {
log.Printf("failed to read session started when deleting session: %s", err)
return
}
deleted, err := db.deleteSession(started, a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
} else if deleted {
data["result"] = "deleted"
}
case "downloadsessions":
sessions, err := db.getAllSessions(a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
data["downloadsessions"] = sessions
case "uploadsessions":
data["result"] = "corrupt"
u := r.FormValue("uploadsessions")
if u == "" {
break
}
postsession := r.FormValue("postsession")
updateSessionStarted, _ := strconv.Atoi(r.FormValue("editstarted"))
var uploadsessions []Session
err = json.Unmarshal([]byte(u), &uploadsessions)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
data["result"] = "uploaded"
uploaded := false
sessionsuploaded := 0
for _, session := range uploadsessions {
uploaded, err = db.addSession(session, updateSessionStarted, a.ID, av, appmarket)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
if uploaded {
sessionsuploaded++
}
}
data["sessionsuploaded"] = sessionsuploaded
if sessionsuploaded > 0 {
started := 0
streakday := 0
for _, session := range uploadsessions {
if session.Started > started {
started = session.Started
if action != "editposting" {
streakday = session.StreakDay
}
}
}
t := time.Now().In(tz)
if beforeWindowStart(t, streakbuffer) {
t = t.AddDate(0, 0, -1)
}
t = atWindowStart(t, streakbuffer)
if int64(started) >= t.Unix() { // A session was recorded after the start of today's streak window
streak, err := db.calculateStreak(a.ID, a.StreakBuffer, tz)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
logDebugf("NEW SESSION %v - CALCULATED: %d, SUBMITTED: %d", t, streak, streakday)
if streak < streakday {
streak = streakday
} else if streak > streakday {
err = db.setSessionStreakDay(started, streak, a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
err = db.setStreak(streak, a.ID, a.StreakBuffer, tz)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
stats.SessionsPosted++
if streakday > stats.TopStreak {
stats.TopStreak = streakday
}
}
if postsession != "" {
if sessionsuploaded > 0 {
data["result"] = "posted"
} else {
data["result"] = "alreadyposted"
}
}
}
w.Header().Set("x-MediNET", "connected")
// Send streak
if action == "connect" || action == "downloadsessions" || action == "uploadsessions" {
streakday, streakend, topstreak, err := db.getStreak(a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
w.Header().Set("x-MediNET-Streak", fmt.Sprintf("%d,%d", streakday, streakend))
w.Header().Set("x-MediNET-MaxStreak", fmt.Sprintf("%d", topstreak))
}
err = json.NewEncoder(w).Encode(data)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
j, err := json.Marshal(data)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
logDebugf("App: %d API: %d Action: %s - %q", appver, apiver, action, html.EscapeString(r.URL.RequestURI()))
logDebugf("Account ID: %d, JSON: %s", a.ID, string(j))
if action == "uploadsessions" || action == "deletesession" {
updateCommunity <- struct{}{}
}
}
func initWeb() {
http.HandleFunc("/om/sessions", func(w http.ResponseWriter, r *http.Request) {
// TODO
})
http.HandleFunc("/om/account", func(w http.ResponseWriter, r *http.Request) {
// TODO
})
http.HandleFunc("/om/forum", func(w http.ResponseWriter, r *http.Request) {
// TODO
})
http.HandleFunc("/om/groups", func(w http.ResponseWriter, r *http.Request) {
// TODO
})
http.HandleFunc("/om/status", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "MediNET ONLINE<br><br>Accounts: %d (%d new)<br>Sessions: %d<br>Top streak: %d", len(stats.ActiveAccounts), stats.AccountsCreated, stats.SessionsPosted, stats.TopStreak)
})
http.HandleFunc("/om", handleMediNET)
go handleUpdateCommunity()
updateCommunity <- struct{}{}
}
func listenWeb() {
log.Fatal(http.ListenAndServe(config.Om, nil))
}

122
webpage.go Normal file
View File

@ -0,0 +1,122 @@
package main
import (
"bytes"
"crypto/md5"
"fmt"
"html"
"io/ioutil"
"log"
"net/url"
"path"
"time"
)
func pageHeader(light bool, title string) []byte {
theme := "dark"
if light {
theme = "light"
}
return []byte(`<!DOCTYPE HTML>
<html>
<head>
<title>` + title + `</title>
<link rel="stylesheet" type="text/css" href="css/medinet.css">
<link rel="stylesheet" type="text/css" href="css/holo.css">
<link rel="stylesheet" type="text/css" href="css/holo-` + theme + `.css">
</head>
<body class="holo-` + theme + `">
<div class="holo-content">
`)
}
func pageFooter() []byte {
return []byte(`</div>
</body>
</html>`)
}
func gravatar(light bool, size int, email string) string {
d := "https://medinet.ftp.sh/images/ic_om_sq_small_dark.png"
if light {
d = "https://medinet.ftp.sh/images/ic_om_sq_small_light.png"
}
return fmt.Sprintf("https://www.gravatar.com/avatar/%x?s=%d&d=%s", md5.Sum([]byte(email)), size, url.QueryEscape(d))
}
func formatLength(length int) string {
d := time.Duration(time.Duration(length) * time.Second)
return fmt.Sprintf("%d:%02d", int(d.Hours()), int(d.Minutes())%60)
}
func formatCompleted(completed int) string {
c := time.Unix(int64(completed), 0)
return c.Format("2006-01-02 3:04")
}
func formatRecentCompleted(completed int) string {
c := time.Unix(int64(completed), 0)
return c.Format("3:04 Jan 2")
}
func formatStreak(streakday int) string {
if streakday == 0 {
return ""
}
s := ""
if streakday > 1 {
s = "s"
}
return fmt.Sprintf("<p>%d day%s of meditation</p>", streakday, s)
}
func formatMessage(message string) string {
if message == "" {
return ""
}
return "<p><small>" + html.EscapeString(message) + "</small></p>"
}
func communityPage(light bool) []byte {
var b bytes.Buffer
b.Write(pageHeader(light, "Community"))
recentSessions, err := db.getRecentSessions()
if err != nil {
log.Fatal(err)
}
format := `<li onclick="goToAccount('%d');">
<table border="0" cellspacing="3px" cellpadding="0px" width="100%%">
<tr>
<td width="57px" height="57px" style="margin: 0px;padding: 0px;">
<img src="%s" width="57px" height="57px" style="margin: 0px;padding: 0px;">
</td>
<td>
<div style="padding-left: 4px;"><span style="font-size: 2em;font-weight: bold;">%s</span>
%s
</div></td>
<td align="right" style="vertical-align:top;font-size: 0.85em;">
%s
</td></tr>
</table>
%s
%s
</li>`
for _, rc := range recentSessions {
b.WriteString(fmt.Sprintf(format, rc.AccountID, gravatar(light, 104, rc.AccountEmail), formatLength(rc.Length), html.EscapeString(rc.AccountName), formatRecentCompleted(rc.Completed), formatStreak(rc.StreakDay), formatMessage(rc.Message)))
}
b.Write(pageFooter())
return b.Bytes()
}
func handleUpdateCommunity() {
for range updateCommunity {
ioutil.WriteFile(path.Join(config.Web, "community.html"), communityPage(false), 0655)
ioutil.WriteFile(path.Join(config.Web, "community_light.html"), communityPage(true), 0655)
}
}