Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9959b81fa | |||
| 247dbd056a | |||
| b8e5f8effe | |||
| 48dca86185 | |||
| 219ce85b7f | |||
| a1ce0c8841 | |||
| 5e5d4cb253 | |||
| 3a80de8b9e | |||
| 7c2c55cb3c | |||
| 3f56aac8c1 | |||
| 8e7bcbe057 | |||
| 0197b8466f | |||
| 944ae74ba8 | |||
| d10808c7c1 | |||
| e3ff8095ee | |||
| 901cb84a1c | |||
| 2ae9d52419 | |||
| f5d587819f | |||
| dea0cffc90 | |||
| 917b9c1a53 | |||
| 758ba9a457 | |||
| d1007bb645 | |||
| 35b9973a4a | |||
| 437e1f60e5 | |||
| 3c8631c0d7 | |||
| 3f4d5e2522 | |||
| a087ad2e7b | |||
| a27645772a | |||
| 86cc1cbb2e | |||
| 9ebe6ca2fb | |||
| 117a5fa51e | |||
| 5f2a1b528d | |||
| 559283566b | |||
| add69c5c81 | |||
| f261ca0ece | |||
| 672f0f606c | |||
| cd1e868daf | |||
| e078383e6d | |||
| 8568c4bdc2 | |||
| 7b9aece4d2 | |||
| e0ff64cd22 | |||
| 3f623782b7 | |||
| 92e7064372 | |||
| 149e226729 | |||
| 3c3028b8d1 | |||
| 8a452c0fa6 | |||
| 693f541934 | |||
| e6ed43154c | |||
| 533527c890 | |||
| f28ae17a00 | |||
| cd879798e3 | |||
| f90a6d1dc6 | |||
| ba097e3a9d | |||
| 0dd72a0468 | |||
| d91513c1ac | |||
| 957195ca11 | |||
| bed15bfb33 | |||
| 7641f9d73d |
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
# development
|
||||
cmd/archive-server/archive-server
|
||||
cmd/archive-server/config.json
|
||||
|
||||
# binaries
|
||||
_dist/
|
||||
build/
|
||||
@@ -1,9 +0,0 @@
|
||||
mode:regex
|
||||
|
||||
# development
|
||||
^cmd/archive-server/archive-server$
|
||||
^cmd/archive-server/config.json$
|
||||
|
||||
# binaries
|
||||
^_dist/
|
||||
^build/
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/jehiah/go-strftime"
|
||||
)
|
||||
|
||||
var (
|
||||
SERVER_VERSION = "archive/0.0.0"
|
||||
SERVER_VERSION = "archive/0.0.0-git"
|
||||
)
|
||||
|
||||
type ArchiveServer struct {
|
||||
@@ -16,7 +18,7 @@ type ArchiveServer struct {
|
||||
cfg *Config
|
||||
startup time.Time
|
||||
|
||||
rxViewRoot, rxViewPage, rxSearch, rxSearchRx *regexp.Regexp
|
||||
rxViewRoot, rxViewPage, rxSearch, rxSearchRx, rxStats *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
|
||||
@@ -47,6 +49,7 @@ func NewArchiveServer(cfg *Config) (*ArchiveServer, error) {
|
||||
rxViewPage: regexp.MustCompile(`^/([^/]+)/(\d+)/(\d+)/(?:page-)?(\d+)$`),
|
||||
rxSearch: regexp.MustCompile(`^/([^/]+)/search/(.*)$`),
|
||||
rxSearchRx: regexp.MustCompile(`^/([^/]+)/rx/(.*)$`),
|
||||
rxStats: regexp.MustCompile((`^/([^/]+)/stats/?$`)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -57,7 +60,7 @@ func (this *ArchiveServer) LogFile(ls *LogSource, ym YearMonth) (string, error)
|
||||
|
||||
for _, fl := range ls.FileLocation {
|
||||
if fl.StartMonth.Index() <= ymIndex && (fl.EndMonth == nil || fl.EndMonth.Index() >= ymIndex) {
|
||||
return time.Date(ym.Year, ym.Month, 1, 0, 0, 0, 0, this.timezone).Format(fl.LogFilePath), nil
|
||||
return strftime.Format(fl.LogFilePath, time.Date(ym.Year, ym.Month, 1, 0, 0, 0, 0, this.timezone)), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
306
ArchiveState.go
306
ArchiveState.go
@@ -2,14 +2,18 @@ package archive
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,6 +26,7 @@ type ArchiveState struct {
|
||||
logBestSlug string
|
||||
query string
|
||||
queryIsRegex bool
|
||||
isStats bool
|
||||
ym YearMonth
|
||||
page int
|
||||
highestPage int
|
||||
@@ -34,6 +39,30 @@ func NewArchiveState(svr *ArchiveServer) *ArchiveState {
|
||||
}
|
||||
}
|
||||
|
||||
func (this *ArchiveState) showPageURLs() bool {
|
||||
return this.log != nil && len(this.query) == 0 && !this.isStats
|
||||
}
|
||||
|
||||
func (this *ArchiveState) URL() string {
|
||||
if this.isStats {
|
||||
return fmt.Sprintf(`/%s/stats`, this.logBestSlug)
|
||||
|
||||
} else if len(this.query) > 0 {
|
||||
if this.queryIsRegex {
|
||||
return fmt.Sprintf(`/%s/rx/%s`, this.logBestSlug, url.QueryEscape(this.query))
|
||||
} else {
|
||||
return fmt.Sprintf(`/%s/search/%s`, this.logBestSlug, url.QueryEscape(this.query))
|
||||
}
|
||||
|
||||
} else {
|
||||
if this.page == pageNotSet {
|
||||
return fmt.Sprintf(`/%s/%s/%s`, this.logBestSlug, this.ym.Year, this.ym.Month)
|
||||
} else {
|
||||
return fmt.Sprintf(`/%s/%s/%s/page-%d`, this.logBestSlug, this.ym.Year, this.ym.Month, this.page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *ArchiveState) selectSource(log *LogSource, slug string) {
|
||||
this.log = log
|
||||
this.logBestSlug = slug
|
||||
@@ -57,6 +86,11 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
|
||||
|
||||
lines := strings.Split(string(fc), "\n")
|
||||
|
||||
// Work around #newpage: if the last line is blank, skip
|
||||
if len(lines) > 0 && len(lines[len(lines)-1]) == 0 {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
|
||||
this.highestPage = int(math.Ceil(float64(len(lines))/float64(this.svr.cfg.LinesPerPage))) - 1
|
||||
if this.page == pageNotSet || this.page > this.highestPage {
|
||||
this.page = this.highestPage
|
||||
@@ -73,12 +107,136 @@ func (this *ArchiveState) renderView(w http.ResponseWriter) {
|
||||
|
||||
output := ""
|
||||
for i := startLine; i < endLine; i += 1 {
|
||||
output += template.HTMLEscapeString(lines[i]) + "<br>\n"
|
||||
output += html.EscapeString(lines[i]) + "\n"
|
||||
}
|
||||
|
||||
this.renderTemplate(w, []byte(output))
|
||||
}
|
||||
|
||||
// parseStatsFor loads user post statistics for a single yearMonth in a single log source.
|
||||
func (this *ArchiveState) parseStatsFor(ls *LogSource, ym YearMonth, into map[string]int) error {
|
||||
|
||||
fname, err := this.svr.LogFile(ls, ym)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fc, err := ioutil.ReadFile(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rxUser := regexp.MustCompile(`(?ms)^[^<\r\n]*<([^>\r\n]+)>.+?$`)
|
||||
matches := rxUser.FindAllSubmatch(fc, -1)
|
||||
if matches == nil || len(matches) == 0 {
|
||||
return errors.New("No matches")
|
||||
}
|
||||
|
||||
for _, match := range matches {
|
||||
username := string(match[1])
|
||||
|
||||
if ct, ok := into[username]; ok {
|
||||
into[username] = ct + 1
|
||||
} else {
|
||||
into[username] = 1
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *ArchiveState) renderStats(w http.ResponseWriter) {
|
||||
// Lines per year
|
||||
// Users / posts/year
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
yearsToUsersToPostCount := make(map[int]map[string]int, 0)
|
||||
|
||||
totalErrors := 0
|
||||
var lastError error = nil
|
||||
|
||||
ym := this.log.EarliestDate()
|
||||
orderedYears := make([]int, 0)
|
||||
for {
|
||||
usersToPostCount, ok := yearsToUsersToPostCount[ym.Year]
|
||||
if !ok {
|
||||
usersToPostCount = make(map[string]int)
|
||||
orderedYears = append(orderedYears, ym.Year)
|
||||
}
|
||||
|
||||
err := this.parseStatsFor(this.log, ym, usersToPostCount)
|
||||
if err != nil {
|
||||
//log.Printf("Stats(%s): %s", this.logBestSlug, err.Error())
|
||||
totalErrors += 1
|
||||
lastError = err
|
||||
}
|
||||
|
||||
//fmt.Printf("%#v\n", usersToPostCount)
|
||||
|
||||
yearsToUsersToPostCount[ym.Year] = usersToPostCount
|
||||
|
||||
if ym.Equals(this.log.LatestDate()) {
|
||||
break
|
||||
}
|
||||
|
||||
ym = ym.Next()
|
||||
}
|
||||
|
||||
ret := make([]byte, 0)
|
||||
|
||||
if lastError != nil {
|
||||
ret = append(ret, []byte(fmt.Sprintf("<em>Got %d errors, including: '%s'</em>\n\n", totalErrors, lastError.Error()))...)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
allUsersExistence := make(map[string]struct{})
|
||||
for _, usersMap := range yearsToUsersToPostCount {
|
||||
for username, _ := range usersMap {
|
||||
allUsersExistence[username] = struct{}{}
|
||||
}
|
||||
}
|
||||
allUsernames := make([]string, 0, len(allUsersExistence))
|
||||
for username, _ := range allUsersExistence {
|
||||
allUsernames = append(allUsernames, username)
|
||||
}
|
||||
sort.Strings(allUsernames)
|
||||
|
||||
//
|
||||
|
||||
ret = append(ret, []byte(`<table><thead><tr><th> </th>`)...)
|
||||
for _, year := range orderedYears {
|
||||
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, year))...)
|
||||
}
|
||||
ret = append(ret, []byte("</tr><tbody>\n")...)
|
||||
for _, username := range allUsernames {
|
||||
|
||||
ret = append(ret, []byte(fmt.Sprintf(`<tr><td>%s</td>`, html.EscapeString(username)))...)
|
||||
for _, year := range orderedYears {
|
||||
usersMap := yearsToUsersToPostCount[year]
|
||||
posts, _ /*ok*/ := usersMap[username]
|
||||
ret = append(ret, []byte(fmt.Sprintf(`<td>%d</td>`, posts))...)
|
||||
}
|
||||
ret = append(ret, []byte("</tr>\n")...)
|
||||
}
|
||||
ret = append(ret, []byte(`</tbody><tfoot><th style="text-align:right;">TOTAL:</th>`)...)
|
||||
|
||||
for _, year := range orderedYears {
|
||||
postsTotalForYear := 0
|
||||
for _, userPostCount := range yearsToUsersToPostCount[year] {
|
||||
postsTotalForYear += userPostCount
|
||||
}
|
||||
ret = append(ret, []byte(fmt.Sprintf(`<th>%d</th>`, postsTotalForYear))...)
|
||||
}
|
||||
|
||||
duration := time.Now().Sub(startTime)
|
||||
|
||||
ret = append(ret, []byte(fmt.Sprintf("</tfoot></table>\n\n<em>%d total users\nStatistics generated in %s</em>", len(allUsernames), duration.String()))...)
|
||||
|
||||
this.renderTemplate(w, ret)
|
||||
}
|
||||
|
||||
// renderSearch renders the search results.
|
||||
// - Mandatory: log, query, queryIsRegex
|
||||
func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
|
||||
@@ -101,7 +259,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
|
||||
|
||||
this.renderTemplateHead(w)
|
||||
totalResults := 0
|
||||
w.Write([]byte(`<ul>`))
|
||||
w.Write([]byte(`<ul class="search-results">`))
|
||||
|
||||
limit := this.log.LatestDate().Next() // one off the end
|
||||
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
|
||||
@@ -131,7 +289,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
|
||||
lineNo := i % this.svr.cfg.LinesPerPage
|
||||
url := fmt.Sprintf(`/%s/%d/%d/page-%d#line-%d`, this.logBestSlug, ympair.Year, ympair.Month, page, lineNo)
|
||||
|
||||
w.Write([]byte(`<li><a href="` + template.HTMLEscapeString(url) + `">»</a> ` + template.HTMLEscapeString(scanner.Text()) + `</li>`))
|
||||
w.Write([]byte(`<li><a href="` + html.EscapeString(url) + `">»</a> ` + html.EscapeString(scanner.Text()) + `</li>`))
|
||||
}
|
||||
|
||||
}()
|
||||
@@ -140,7 +298,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
|
||||
w.Write([]byte(`</ul>`))
|
||||
|
||||
if totalResults == 0 {
|
||||
w.Write([]byte(`No search results for "<em>` + template.HTMLEscapeString(this.query) + `</em>"`))
|
||||
w.Write([]byte(`No search results for "<em>` + html.EscapeString(this.query) + `</em>"`))
|
||||
} else {
|
||||
w.Write([]byte(`<br><em>Found ` + fmt.Sprintf("%d", totalResults) + ` total result(s).</em><br><br>`))
|
||||
}
|
||||
@@ -150,7 +308,7 @@ func (this *ArchiveState) renderSearch(w http.ResponseWriter) {
|
||||
|
||||
// renderError renders a plain text string, escaping it for HTML use.
|
||||
func (this *ArchiveState) renderError(w http.ResponseWriter, msg string) {
|
||||
this.renderTemplate(w, []byte(template.HTMLEscapeString(msg)))
|
||||
this.renderTemplate(w, []byte(html.EscapeString(msg)))
|
||||
}
|
||||
|
||||
func (this *ArchiveState) renderTemplate(w http.ResponseWriter, body []byte) {
|
||||
@@ -169,55 +327,70 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
|
||||
title = this.log.Description + ` Archives`
|
||||
}
|
||||
|
||||
showPageURLs := (this.log != nil && len(this.query) == 0)
|
||||
latestUrl := `/`
|
||||
if this.log != nil {
|
||||
latestUrl = fmt.Sprintf(`/%s/%d/%d`, url.PathEscape(this.logBestSlug), this.log.LatestDate().Year, this.log.LatestDate().Month)
|
||||
}
|
||||
statsUrl := fmt.Sprintf(`/%s/stats`, url.PathEscape(this.logBestSlug))
|
||||
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>` + template.HTMLEscapeString(title) + `</title>
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<title>` + html.EscapeString(title) + `</title>
|
||||
<link rel="stylesheet" type="text/css" href="/style.css?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout-top nav">
|
||||
|
||||
<div id="tr1" style="display:none;"></div>
|
||||
<div id="tr2" style="display:none;"></div>
|
||||
<div class="ddmenu" id="spm" style="display:none;">
|
||||
<a href="/">Latest</a>
|
||||
<a onclick="fontSize(1);">Font increase</a>
|
||||
<a onclick="fontSize(-1);">Font decrease</a>
|
||||
<a href="/download" onclick="return confirm('Are you sure you want to download a backup?');">Download backup</a>
|
||||
<div id="menu-container" class="noselect">
|
||||
<div id="tr1"></div>
|
||||
<div id="tr2"></div>
|
||||
<div class="ddmenu">
|
||||
<a href="` + html.EscapeString(latestUrl) + `">Latest</a>
|
||||
<a href="` + html.EscapeString(statsUrl) + `">Statistics</a>
|
||||
<a onclick="fontSize(1);">Font increase</a>
|
||||
<a onclick="fontSize(-1);">Font decrease</a>
|
||||
<a href="/download" onclick="return confirm('Are you sure you want to download a backup?');">Download backup</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a onclick="toggleMenu();"><div id="logo" class="layout-pushdown"></div></a>
|
||||
<div id="logo" class="layout-pushdown"><svg viewBox="0 0 24 24">
|
||||
<path fill="#000000" d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z" />
|
||||
</svg></div>
|
||||
|
||||
<span class="area-nav">
|
||||
|
||||
<form method="GET" id="frmHub">
|
||||
<select name="h" id="selHub">
|
||||
`))
|
||||
<select onchange="window.location.pathname = this.value;">
|
||||
`))
|
||||
|
||||
for i, h := range this.svr.cfg.Logs {
|
||||
slug, _ := this.svr.bestSlugFor(&this.svr.cfg.Logs[i])
|
||||
current := (this.log == &this.svr.cfg.Logs[i])
|
||||
|
||||
w.Write([]byte(`<option value="` + template.HTMLEscapeString(slug) + `" ` + attr(current, "selected") + `>` + template.HTMLEscapeString(h.Description) + `</option>`))
|
||||
targetUri := `/` + url.PathEscape(slug) + `/`
|
||||
if len(this.query) > 0 {
|
||||
if this.queryIsRegex {
|
||||
targetUri += `rx/` + url.PathEscape(this.query)
|
||||
} else {
|
||||
targetUri += `search/` + url.PathEscape(this.query)
|
||||
}
|
||||
} else {
|
||||
targetUri += fmt.Sprintf(`%d/%d`, h.LatestDate().Year, h.LatestDate().Month)
|
||||
}
|
||||
|
||||
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(current, " selected"), html.EscapeString(h.Description))))
|
||||
}
|
||||
|
||||
w.Write([]byte(`
|
||||
</select>
|
||||
</form>
|
||||
</select>
|
||||
`))
|
||||
|
||||
if showPageURLs {
|
||||
if this.showPageURLs() {
|
||||
w.Write([]byte(`
|
||||
|
||||
<form method="GET">
|
||||
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
|
||||
<select id="seldate" onchange="setYM(this);">
|
||||
`))
|
||||
<select onchange="window.location.pathname = this.value;">
|
||||
`))
|
||||
|
||||
// Generate month dropdown options
|
||||
|
||||
@@ -226,14 +399,24 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
|
||||
for ympair := this.log.EarliestDate(); !ympair.Equals(limit); ympair = ympair.Next() {
|
||||
if ympair.Year != lastY {
|
||||
if lastY != -1 {
|
||||
w.Write([]byte(`</optgroup>`))
|
||||
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
|
||||
}
|
||||
w.Write([]byte(`<optgroup label="` + fmt.Sprintf("%d", ympair.Year) + `">`))
|
||||
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t<optgroup label=\"%d\">\n", ympair.Year)))
|
||||
lastY = ympair.Year
|
||||
}
|
||||
|
||||
w.Write([]byte(fmt.Sprintf(`<option value="%d-%d" %s>%s</option>`, ympair.Year, ympair.Month, attr(ympair.Equals(this.ym), "selected"), template.HTMLEscapeString(ympair.Month.String()))))
|
||||
targetUri := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, ympair.Year, ympair.Month)
|
||||
|
||||
w.Write([]byte(fmt.Sprintf("\t\t\t\t\t\t<option value=\"%s\"%s>%s</option>\n", html.EscapeString(targetUri), attr(ympair.Equals(this.ym), " selected"), html.EscapeString(ympair.Month.String()))))
|
||||
}
|
||||
if lastY != -1 {
|
||||
w.Write([]byte("\t\t\t\t\t</optgroup>\n"))
|
||||
}
|
||||
w.Write([]byte(`
|
||||
</select>
|
||||
|
||||
<div class="mini-separator layout-pushdown"></div>
|
||||
`))
|
||||
|
||||
//
|
||||
|
||||
@@ -249,51 +432,46 @@ func (this *ArchiveState) renderTemplateHead(w http.ResponseWriter) {
|
||||
nextPage = this.highestPage
|
||||
}
|
||||
|
||||
prevMonth := this.ym.Prev() // FIXME check bounds
|
||||
prevMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, prevMonth.Year, prevMonth.Month)
|
||||
nextMonth := this.ym.Next() // FIXME check bounds
|
||||
nextMonthLink := fmt.Sprintf(`/%s/%d/%d`, this.logBestSlug, nextMonth.Year, nextMonth.Month)
|
||||
|
||||
w.Write([]byte(`
|
||||
</optgroup>
|
||||
|
||||
</select>
|
||||
<input type="hidden" name="y" id="f_y" value="">
|
||||
<input type="hidden" name="m" id="f_m" value="">
|
||||
<input type="hidden" name="p" value="0" >
|
||||
</form>
|
||||
<a style="display:none;" id="monthprev" accesskey="j" href="` + html.EscapeString(prevMonthLink) + `">
|
||||
<a style="display:none;" id="monthnext" accesskey="k" href="` + html.EscapeString(nextMonthLink) + `">
|
||||
|
||||
<div class="mini-separator layout-pushdown"></div>
|
||||
|
||||
<a class="btn" href="` + pageBase + `/page-0">«</a><a
|
||||
class="btn" id="pgprev" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">‹</a>
|
||||
` + fmt.Sprintf("%d", this.page) + `
|
||||
<a class="btn" id="pgnext" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">›</a><a
|
||||
class="btn" href="` + pageBase + `">»</a>
|
||||
<a class="btn" href="` + pageBase + `/page-0">«</a>
|
||||
<a class="btn" accesskey="h" title="Previous page (Alt+H)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", previousPage) + `">‹</a>
|
||||
<span class="current-page">` + fmt.Sprintf("%d", this.page) + `</span>
|
||||
<a class="btn" accesskey="l" title="Next page (Alt+L)" href="` + pageBase + `/page-` + fmt.Sprintf("%d", nextPage) + `">›</a>
|
||||
<a class="btn" href="` + pageBase + `">»</a>
|
||||
`))
|
||||
}
|
||||
|
||||
w.Write([]byte(`
|
||||
<div class="pad"></div>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
<span class="area-search">
|
||||
<form method="GET" id="search-form">
|
||||
<input type="hidden" name="h" value="` + html.EscapeString(this.logBestSlug) + `">
|
||||
<input type="text" id="searchbox" name="q" value="` + html.EscapeString(this.query) + `" placeholder="Search..." accesskey="m" title="Search (Alt+M)">
|
||||
<input type="submit" value="»">
|
||||
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
|
||||
</form>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<span class="area-search">
|
||||
|
||||
<form method="GET">
|
||||
<input type="hidden" name="h" value="` + template.HTMLEscapeString(this.logBestSlug) + `">
|
||||
<input type="text" id="searchbox" name="q" value="` + template.HTMLEscapeString(this.query) + `" placeholder="Search...">
|
||||
<input type="submit" value="»">
|
||||
<input type="checkbox" class="layout-pushdown" name="rx" value="1" title="PCRE Regular Expression" ` + attr(this.queryIsRegex, "checked") + `>
|
||||
</form>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="layout-body" id="chatarea">
|
||||
`))
|
||||
<div class="layout-body" id="chatarea">`,
|
||||
))
|
||||
|
||||
// Header ends
|
||||
}
|
||||
|
||||
func (this *ArchiveState) renderTemplateFoot(w http.ResponseWriter) {
|
||||
w.Write([]byte(`
|
||||
</div>
|
||||
w.Write([]byte(`</div>
|
||||
|
||||
<script type="text/javascript" src="/archive.js?nonce=` + fmt.Sprintf("%d", this.svr.startup.Unix()) + `"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
79
Makefile
79
Makefile
@@ -1,79 +0,0 @@
|
||||
#
|
||||
# Makefile for archive
|
||||
#
|
||||
|
||||
VERSION:=3.0.0
|
||||
|
||||
SOURCES:=Makefile \
|
||||
static \
|
||||
cmd $(wildcard cmd/archive-server/*.go) \
|
||||
$(wildcard *.go)
|
||||
|
||||
GOFLAGS := -ldflags='-s -w -X code.ivysaur.me/archive.SERVER_VERSION=archive/${VERSION}' -gcflags='-trimpath=$(GOPATH)' -asmflags='-trimpath=$(GOPATH)'
|
||||
|
||||
#
|
||||
# Phony targets
|
||||
#
|
||||
|
||||
.PHONY: all dist clean
|
||||
|
||||
all: build/linux64/archive-server build/win32/archive-server.exe
|
||||
|
||||
dist: \
|
||||
_dist/archive-$(VERSION)-linux64.tar.gz \
|
||||
_dist/archive-$(VERSION)-win32.7z \
|
||||
_dist/archive-$(VERSION)-src.zip
|
||||
|
||||
clean:
|
||||
if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
|
||||
if [ -d ./build ] ; then rm -r ./build ; fi
|
||||
if [ -f ./archive ] ; then rm ./archive ; fi
|
||||
|
||||
#
|
||||
# Generated files
|
||||
#
|
||||
|
||||
staticResources.go: static/ static/*
|
||||
go-bindata -o staticResources.go -prefix static -pkg archive static
|
||||
|
||||
|
||||
#
|
||||
# Release artefacts
|
||||
#
|
||||
|
||||
build/linux64/archive-server: $(SOURCES) staticResources.go
|
||||
mkdir -p build/linux64
|
||||
(cd cmd/archive-server ; \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||
go build $(GOFLAGS) -o ../../build/linux64/archive-server \
|
||||
)
|
||||
|
||||
build/win32/archive-server.exe: $(SOURCES) staticResources.go
|
||||
mkdir -p build/win32
|
||||
(cd cmd/archive-server ; \
|
||||
PATH=/usr/lib/mxe/usr/bin:$(PATH) CC=i686-w64-mingw32.static-gcc \
|
||||
CGO_ENABLED=1 GOOS=windows GOARCH=386 \
|
||||
go build $(GOFLAGS) -o ../../build/win32/archive-server.exe \
|
||||
)
|
||||
|
||||
build/linux64/config.json.SAMPLE: cmd/archive-server/config.json.SAMPLE
|
||||
cp cmd/archive-server/config.json.SAMPLE build/linux64/config.json.SAMPLE
|
||||
|
||||
build/win32/config.json.SAMPLE: cmd/archive-server/config.json.SAMPLE
|
||||
cp cmd/archive-server/config.json.SAMPLE build/win32/config.json.SAMPLE
|
||||
|
||||
_dist/archive-$(VERSION)-linux64.tar.gz: build/linux64/archive-server build/linux64/config.json.SAMPLE
|
||||
mkdir -p _dist
|
||||
tar caf _dist/archive-$(VERSION)-linux64.tar.gz -C build/linux64 archive-server config.json.SAMPLE --owner=0 --group=0
|
||||
|
||||
_dist/archive-$(VERSION)-win32.7z: build/win32/archive-server.exe build/win32/config.json.SAMPLE
|
||||
mkdir -p _dist
|
||||
( cd build/win32 ; \
|
||||
if [ -f dist.7z ] ; then rm dist.7z ; fi ; \
|
||||
7z a dist.7z archive-server.exe config.json.SAMPLE ; \
|
||||
mv dist.7z ../../_dist/archive-$(VERSION)-win32.7z \
|
||||
)
|
||||
|
||||
_dist/archive-$(VERSION)-src.zip: $(SOURCES)
|
||||
hg archive --type=zip _dist/archive-$(VERSION)-src.zip
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
# archive
|
||||
|
||||

|
||||
|
||||
A web interface for browsing chat logs.
|
||||
|
||||
As of the 3.0 release, `archive` is available as a standalone binary for Linux and Windows.
|
||||
|
||||
Written in Golang, PHP
|
||||
## Changelog
|
||||
|
||||
=CHANGELOG=
|
||||
2025-08-20: 3.1.1
|
||||
- Update dependencies, replace go-bindata with `go:embed`
|
||||
|
||||
2018-11-06: (no release tag)
|
||||
- Convert from hg to git
|
||||
- Convert to Go Modules
|
||||
|
||||
2017-12-10: 3.1.0
|
||||
- Feature: Statistics page
|
||||
- Enhancement: Upgrade corner menu image to support high DPI
|
||||
- Enhancement: Mobile-friendly buttons in search results
|
||||
- Enhancement: Explain keyboard shortcuts when hovering over form elements
|
||||
- Enhancement: Improve performance when changing sources and when searching, by removing a network roundtrip
|
||||
- Enhancement: "Latest" link takes you to the latest for the current data source
|
||||
- Change ctrl+alt+[H/J/K/L/M]... keyboard shortcuts to just alt+[...], remove dependency on APL `mousetrap.js`
|
||||
- Fix an issue with accessing the legacy php controller after the Go port
|
||||
- Fix an issue with using plus characters in regular expression searches
|
||||
- Fix an issue with blank pages appearing if there was a divisible number of log entries
|
||||
- Fix an issue with browsers using stale stylesheets
|
||||
- Fix a cosmetic issue with text selection when using dropdown menu items
|
||||
- Fix a cosmetic issue with PCRE text in regular expression hover message (it's no longer PCRE)
|
||||
- Fix a cosmetic issue with search box layout at some screen sizes
|
||||
- Fix a cosmetic issue with animation responsiveness
|
||||
|
||||
2017-09-06: 3.0.1
|
||||
- Breaking: Revert date formatting in filenames back to strftime-compatible
|
||||
|
||||
2017-08-13: 3.0.0
|
||||
- Rewritten in Go.
|
||||
27
Router.go
27
Router.go
@@ -2,6 +2,7 @@ package archive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -10,6 +11,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Static assets
|
||||
|
||||
//go:embed static/*
|
||||
var staticAssets embed.FS
|
||||
|
||||
func (this *ArchiveServer) lookupSourceByNumericString(slug string) *LogSource {
|
||||
intval, err := strconv.Atoi(slug)
|
||||
if err != nil {
|
||||
@@ -71,7 +77,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Handle static assets
|
||||
|
||||
static, err := Asset(r.URL.Path[1:])
|
||||
static, err := staticAssets.ReadFile(`static/` + r.URL.Path[1:])
|
||||
if err == nil {
|
||||
http.ServeContent(w, r, r.URL.Path[1:], this.startup, bytes.NewReader(static))
|
||||
return
|
||||
@@ -121,7 +127,7 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
} else if matches := this.rxSearch.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
|
||||
if ls := this.lookupSource(matches[1]); ls != nil {
|
||||
arc.selectSource(ls, matches[1])
|
||||
arc.query, _ = url.QueryUnescape(matches[2])
|
||||
arc.query, _ = url.PathUnescape(matches[2])
|
||||
arc.queryIsRegex = false
|
||||
arc.renderSearch(w)
|
||||
|
||||
@@ -132,13 +138,24 @@ func (this *ArchiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
} else if matches := this.rxSearchRx.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
|
||||
if ls := this.lookupSource(matches[1]); ls != nil {
|
||||
arc.selectSource(ls, matches[1])
|
||||
arc.query, _ = url.QueryUnescape(matches[2])
|
||||
arc.query, _ = url.PathUnescape(matches[2])
|
||||
arc.queryIsRegex = true
|
||||
arc.renderSearch(w)
|
||||
|
||||
} else {
|
||||
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
|
||||
}
|
||||
|
||||
} else if matches := this.rxStats.FindStringSubmatch(r.URL.Path); len(matches) > 0 {
|
||||
if ls := this.lookupSource(matches[1]); ls != nil {
|
||||
arc.selectSource(ls, matches[1])
|
||||
arc.isStats = true
|
||||
arc.renderStats(w)
|
||||
|
||||
} else {
|
||||
arc.renderError(w, fmt.Sprintf("Unknown source '%s'", matches[1]))
|
||||
}
|
||||
|
||||
} else {
|
||||
arc.renderError(w, "Unknown route.")
|
||||
|
||||
@@ -157,9 +174,9 @@ func (this *ArchiveServer) legacyRoute(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if u.hasGet("q") {
|
||||
if u.hasGet("rx") {
|
||||
u.redirectf(`/%d/rx/%s`, hubid, url.QueryEscape(u.get("q")))
|
||||
u.redirectf(`/%d/rx/%s`, hubid, url.PathEscape(u.get("q")))
|
||||
} else {
|
||||
u.redirectf(`/%d/search/%s`, hubid, url.QueryEscape(u.get("q")))
|
||||
u.redirectf(`/%d/search/%s`, hubid, url.PathEscape(u.get("q")))
|
||||
}
|
||||
|
||||
} else if u.hasGet("y") && u.hasGet("m") {
|
||||
|
||||
@@ -27,6 +27,14 @@ func (ym YearMonth) Next() YearMonth {
|
||||
}
|
||||
}
|
||||
|
||||
func (ym YearMonth) Prev() YearMonth {
|
||||
if ym.Month == time.January {
|
||||
return YearMonth{Year: ym.Year - 1, Month: time.December}
|
||||
} else {
|
||||
return YearMonth{Year: ym.Year, Month: ym.Month - 1}
|
||||
}
|
||||
}
|
||||
|
||||
// Index returns a single int that can be used to compare this YearMonth with
|
||||
// other YearMonth objects.
|
||||
func (ym YearMonth) Index() int {
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
|
||||
/*
|
||||
* The full path to the log file.
|
||||
* Date formatting reference: https://golang.org/pkg/time/#pkg-constants
|
||||
* Date formatting reference: http://strftime.org/
|
||||
*/
|
||||
"LogFilePath": "/path/to/my-log-source/2006-01.log",
|
||||
"LogFilePath": "/path/to/my-log-source/%Y-%m.log",
|
||||
|
||||
/*
|
||||
* The first month for which this path is valid.
|
||||
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module code.ivysaur.me/archive
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4=
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag=
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 537 B |
@@ -1,27 +1,9 @@
|
||||
|
||||
/* mousetrap v1.4.6 craig.is/killing/mice */
|
||||
(function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;g<l[a].length;++g)if(k=
|
||||
l[a][g],!(!c&&k.seq&&n[k.seq]!=k.level||h!=k.action||("keypress"!=h||d.metaKey||d.ctrlKey)&&b.sort().join(",")!==k.modifiers.sort().join(","))){var m=c&&k.seq==c&&k.level==v;(!c&&k.combo==e||m)&&l[a].splice(g,1);f.push(k)}return f}function K(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function x(a,b,d,c){m.stopCallback(b,b.target||b.srcElement,d,c)||!1!==a(b,d)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?
|
||||
b.stopPropagation():b.cancelBubble=!0)}function y(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=A(a);b&&("keyup"==a.type&&z===b?z=!1:m.handleKey(b,K(a),a))}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||"meta"==a}function L(a,b,d,c){function e(b){return function(){u=b;++n[a];clearTimeout(D);D=setTimeout(t,1E3)}}function v(b){x(d,b,a);"keyup"!==c&&(z=A(b));setTimeout(t,10)}for(var g=n[a]=0;g<b.length;++g){var f=g+1===b.length?v:e(c||E(b[g+1]).action);F(b[g],f,c,a,g)}}function E(a,b){var d,
|
||||
c,e,f=[];d="+"===a?["+"]:a.split("+");for(e=0;e<d.length;++e)c=d[e],G[c]&&(c=G[c]),b&&"keypress"!=b&&H[c]&&(c=H[c],f.push("shift")),w(c)&&f.push(c);d=c;e=b;if(!e){if(!p){p={};for(var g in h)95<g&&112>g||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1<f.length?L(a,f,b,d):(d=E(a,d),l[d.key]=l[d.key]||[],C(d.key,d.modifiers,{type:d.action},
|
||||
c,a,e),l[d.key][c?"unshift":"push"]({callback:b,modifiers:d.modifiers,action:d.action,seq:c,level:e,combo:a}))}var h={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},B={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},H={"~":"`","!":"1",
|
||||
"@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c<a.length;++c)F(a[c],b,d);return this},
|
||||
unbind:function(a,b){return m.bind(a,function(){},b)},trigger:function(a,b){if(q[a+":"+b])q[a+":"+b]({},a);return this},reset:function(){l={};q={};return this},stopCallback:function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable},handleKey:function(a,b,d){var c=C(a,b,d),e;b={};var f=0,g=!1;for(e=0;e<c.length;++e)c[e].seq&&(f=Math.max(f,c[e].level));for(e=0;e<c.length;++e)c[e].seq?c[e].level==f&&(g=!0,
|
||||
b[c[e].seq]=1,x(c[e].callback,d,c[e].combo,c[e].seq)):g||x(c[e].callback,d,c[e].combo);c="keypress"==d.type&&I;d.type!=u||w(a)||c||t(b);I=g&&"keydown"==d.type}};J.Mousetrap=m;"function"===typeof define&&define.amd&&define(m)})(window,document);
|
||||
|
||||
/* archive.js */
|
||||
|
||||
function i(s) {
|
||||
return document.getElementById(s);
|
||||
}
|
||||
var alreadyLoaded = false;
|
||||
|
||||
function t(e) {
|
||||
e.style.display = (e.style.display == 'none') ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function urldesc(s) {
|
||||
return decodeURIComponent(s.replace(/\+/g, " "));
|
||||
}
|
||||
var $chatArea = document.getElementById("chatarea");
|
||||
var DEFAULT_FONT_SIZE = 12;
|
||||
|
||||
function cookie_set(key, value) {
|
||||
document.cookie = (key+"="+value+"; expires=Sat, 20 Sep 2059 09:05:12; path=/");
|
||||
@@ -41,7 +23,11 @@ function cookie_get(key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function highlight(str) {
|
||||
function highlight(str) {
|
||||
var urldesc = function(s) {
|
||||
return decodeURIComponent(s.replace(/\+/g, " "));
|
||||
};
|
||||
|
||||
return str
|
||||
.replace(/(\[\d\d\d\d-\d\d-\d\d\s\d\d\:\d\d\:\d\d\] )(\*.+)/g, "$1<span class=\"sys\">$2</span>")
|
||||
.replace(
|
||||
@@ -62,7 +48,7 @@ function highlight(str) {
|
||||
function fontSize(change) {
|
||||
var curSize = cookie_get("fontsize");
|
||||
if (curSize === null) {
|
||||
curSize = 12;
|
||||
curSize = DEFAULT_FONT_SIZE;
|
||||
} else {
|
||||
curSize = + curSize;
|
||||
}
|
||||
@@ -71,25 +57,22 @@ function fontSize(change) {
|
||||
|
||||
cookie_set("fontsize", curSize);
|
||||
|
||||
i("chatarea").style["fontSize"] = ""+curSize+"px";
|
||||
$chatArea.style["fontSize"] = ""+curSize+"px";
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
t(i("tr1"));
|
||||
t(i("tr2"));
|
||||
t(i("spm"));
|
||||
var $container = document.getElementById("menu-container");
|
||||
$container.style.display = ($container.style.display == 'block') ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function highlightLine(no) {
|
||||
var lines = i('chatarea').innerHTML.split('<br>');
|
||||
var lines = $chatArea.innerHTML.split("\n");
|
||||
|
||||
lines[no] = '<span class="line-highlighted">' + lines[no] + '</span>';
|
||||
|
||||
i('chatarea').innerHTML = lines.join('<br>');
|
||||
$chatArea.innerHTML = lines.join("\n");
|
||||
}
|
||||
|
||||
var alreadyLoaded = false;
|
||||
|
||||
function onLoad() {
|
||||
|
||||
if (alreadyLoaded) {
|
||||
@@ -97,9 +80,13 @@ function onLoad() {
|
||||
}
|
||||
alreadyLoaded = true;
|
||||
|
||||
//
|
||||
//
|
||||
|
||||
i('chatarea').innerHTML = highlight(i('chatarea').innerHTML);
|
||||
$chatArea.innerHTML = highlight( $chatArea.innerHTML );
|
||||
|
||||
//
|
||||
|
||||
document.getElementById("logo").addEventListener("click", toggleMenu);
|
||||
|
||||
//
|
||||
|
||||
@@ -108,69 +95,31 @@ function onLoad() {
|
||||
document.location.hash.substr(0, 6) === '#line-'
|
||||
) {
|
||||
highlightLine( parseInt(document.location.hash.substr(6), 10) );
|
||||
document.location.hash = '';
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
|
||||
fontSize(0);
|
||||
|
||||
//
|
||||
|
||||
i('selHub').onchange = function() {
|
||||
if ( /\/search\//.test(window.location.pathname) ) {
|
||||
window.location.pathname = i('selHub').value + "/search/" + encodeURIComponent( i('searchbox').value );
|
||||
|
||||
} else if ( /\/rx\//.test(window.location.pathname) ) {
|
||||
window.location.pathname = i('selHub').value + "/rx/" + encodeURIComponent( i('searchbox').value )
|
||||
|
||||
} else {
|
||||
i('frmHub').submit();
|
||||
var $form = document.getElementById("search-form");
|
||||
$form.addEventListener("submit", function(ev) {
|
||||
var query = $form.elements["q"].value;
|
||||
if (query.length === 0) {
|
||||
alert("No search text entered");
|
||||
ev.preventDefault();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
Mousetrap.bind('ctrl+alt+h', function() { i("pgprev").click(); });
|
||||
Mousetrap.bind('ctrl+alt+l', function() { i("pgnext").click(); });
|
||||
Mousetrap.bind('ctrl+alt+j', function() {
|
||||
YMgoto( YMmod(i("seldate").value, 1) )
|
||||
});
|
||||
Mousetrap.bind('ctrl+alt+k', function() {
|
||||
YMgoto( YMmod(i("seldate").value, -1) )
|
||||
|
||||
var prefix = ($form.elements["rx"].checked ? "/rx/" : "/search/");
|
||||
window.location.hash = "";
|
||||
window.location.pathname = "/" + $form.elements["h"].value + prefix + encodeURIComponent(query);
|
||||
|
||||
ev.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
Mousetrap.bind('ctrl+alt+m', function() {
|
||||
i("searchbox").focus();
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
}
|
||||
|
||||
function YMmod(str, change) {
|
||||
var t = str.split('-').map(function(x) { return +x; });
|
||||
t[1] += change;
|
||||
if (t[1] == 13) {
|
||||
t[0] += 1;
|
||||
t[1] = 1;
|
||||
}
|
||||
if (t[1] == 0) {
|
||||
t[0] -= 1;
|
||||
t[1] = 12;
|
||||
}
|
||||
return t.join('-');
|
||||
}
|
||||
|
||||
function YMgoto(str) {
|
||||
var t = str.split("-");
|
||||
i("f_y").value = t[0];
|
||||
i("f_m").value = t[1];
|
||||
i("seldate").form.submit();
|
||||
}
|
||||
|
||||
function setYM(el) {
|
||||
YMgoto(el.value);
|
||||
}
|
||||
|
||||
window.addEventListener('load', onLoad);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
Chat Archives
|
||||
`````````````
|
||||
Requires PHP 5.4 (short-array syntax and ENT_SUBSTITUTE) with short_open_tag
|
||||
URL rewriting for nginx;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.php?$args;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
require __DIR__.'/../includes/bootstrap.php';
|
||||
URLRouter::routeRequest();
|
||||
@@ -61,6 +61,8 @@ select {
|
||||
|
||||
#chatarea {
|
||||
word-break:break-word;
|
||||
white-space: pre; /* Safari 1/2, IE 6/7 */
|
||||
white-space: pre-wrap; /* Chrome, Firefox, IE8++ */
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
@@ -76,14 +78,6 @@ select {
|
||||
font-style:italic;
|
||||
color: darkgreen;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 102px;
|
||||
height: 37px;
|
||||
display: block;
|
||||
border:0;
|
||||
padding-bottom: 1.0em;
|
||||
}
|
||||
|
||||
.gt {
|
||||
color:#0A0;
|
||||
@@ -104,11 +98,18 @@ select {
|
||||
.nav {
|
||||
background: #DDD;
|
||||
box-shadow: 0px 4px 24px #CCC;
|
||||
|
||||
font-size:0; /* remove phantom spaces between elements */
|
||||
}
|
||||
.nav form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.nav a, .nav .current-page, .nav select {
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.nav .btn {
|
||||
background: white;
|
||||
color:black;
|
||||
@@ -117,8 +118,14 @@ select {
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
transition:all 0.1s linear;
|
||||
}
|
||||
|
||||
.nav a:hover {
|
||||
border-color: grey black black grey;
|
||||
|
||||
transition:all 0s linear;
|
||||
}
|
||||
|
||||
.area-search {
|
||||
@@ -130,16 +137,33 @@ select {
|
||||
}
|
||||
|
||||
#logo {
|
||||
cursor:pointer;
|
||||
|
||||
display:inline-block;
|
||||
width:16px;
|
||||
height:16px;
|
||||
background:transparent url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbtJREFUOMt9kb1qAlEQhacJ6QJBTJEiTSDpEqw1jYWIpQ9gIYIWghamELGysRE0Eo0GRRQrCyslIKIi/hUawVIEfYW8wGTOjbssyZqFbznMOeeyO5cOhwOdotls+sB/mZNGvV73tdttBtAnD9jv96aUy+XNZDJhAH0qR7vd7g+VSuWuVCrxZrNRQGNmlqXtdkvFYtFeKBR8uVzuTWgIH/j01WqlgMbs6L0hiw66JAN7NpvlfD7P1WoV/8uyOB4OhzydThXQmMFDBll00KVMJvOYSqV4Pp/zbDbjfr//L8ggiw66tF6vKZlMvkejUe71emppnU7HFHjIIIsOurRcLhWJROI5HA7v8KmtVssUeMggq/VosVjoiGEPBoNcq9VMgYeMsUOyJJ1QKPQai8VYNqyurtvtKqAxg4eMsUPj8VjH6/V+xuNxbjQaamHpdFoBjRk8ZIwdGo1GOg6H40u7Lo/Hg/AEQGvXi4yxQ4PBQBGJRC5sNhv7/X52u93bQCAQ1TxozOAhg6zm/byIboVHi8XidLlcr6KdgucXTjnkxWq1Pol+QEd15TQdec4Ei3AlXAv3R26Os0vh3Nj5Bg36USLeWI8KAAAAAElFTkSuQmCC') no-repeat 0 0;
|
||||
margin-left:2px;
|
||||
margin-right:4px;
|
||||
}
|
||||
|
||||
/* Utility class */
|
||||
|
||||
.noselect {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome and Opera */
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
|
||||
#menu-container {
|
||||
display:none; /* default */
|
||||
}
|
||||
|
||||
.ddmenu {
|
||||
display:block;
|
||||
position:absolute;
|
||||
@@ -165,8 +189,6 @@ select {
|
||||
|
||||
.ddmenu a:hover {
|
||||
background:#FFF;
|
||||
-moz-transition:all 0.1s linear;
|
||||
-webkit-transition:all 0.1s linear;
|
||||
}
|
||||
|
||||
#tr1 {
|
||||
@@ -202,7 +224,7 @@ select {
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.nav, .nav select {
|
||||
.nav a, .nav .current-page, .nav select {
|
||||
font-size: 16px;
|
||||
line-height:16px;
|
||||
}
|
||||
@@ -227,12 +249,29 @@ select {
|
||||
}
|
||||
|
||||
.area-search {
|
||||
display:block;
|
||||
float:none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#searchbox {
|
||||
width:auto;
|
||||
}
|
||||
|
||||
ul.search-results {
|
||||
list-style-type: none;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.search-results li a:first-child {
|
||||
/* convert mini link to touchable */
|
||||
display:inline-block;
|
||||
padding: 2px 6px;
|
||||
margin: 2px;
|
||||
border: 1px solid grey;
|
||||
background: lightgrey;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user