commit
facd70e094
@ -19,6 +19,11 @@ func init() {
|
||||
return js
|
||||
})
|
||||
|
||||
app.Get("/scripts.js", func(ctx *aero.Context) string {
|
||||
ctx.SetResponseHeader("Content-Type", "application/javascript")
|
||||
return js
|
||||
})
|
||||
|
||||
// Web manifest
|
||||
app.Get("/manifest.json", func(ctx *aero.Context) string {
|
||||
return ctx.JSON(app.Config.Manifest)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import "github.com/aerogo/aero"
|
||||
import "github.com/animenotifier/notify.moe/utils"
|
||||
|
||||
// Install ...
|
||||
func Install(app *aero.Application) {
|
||||
@ -10,6 +11,12 @@ func Install(app *aero.Application) {
|
||||
// Logout
|
||||
app.Get("/logout", func(ctx *aero.Context) string {
|
||||
if ctx.HasSession() {
|
||||
user := utils.GetUser(ctx)
|
||||
|
||||
if user != nil {
|
||||
authLog.Info("User logged out", user.ID, ctx.RealIP(), user.Email, user.RealName())
|
||||
}
|
||||
|
||||
ctx.Session().Set("userId", nil)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/animenotifier/arn"
|
||||
"github.com/animenotifier/notify.moe/utils"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
@ -88,22 +89,86 @@ func InstallGoogleAuth(app *aero.Application) {
|
||||
return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err)
|
||||
}
|
||||
|
||||
// Try to find an existing user by the Google user ID
|
||||
user, getErr := arn.GetUserFromTable("GoogleToUser", googleUser.Sub)
|
||||
// Is this an existing user connecting another social account?
|
||||
user := utils.GetUser(ctx)
|
||||
|
||||
if user != nil {
|
||||
println("Connected")
|
||||
|
||||
// Add GoogleToUser reference
|
||||
err = user.ConnectGoogle(googleUser.Sub)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err)
|
||||
}
|
||||
|
||||
return ctx.Redirect("/")
|
||||
}
|
||||
|
||||
var getErr error
|
||||
|
||||
// Try to find an existing user via the Google user ID
|
||||
user, getErr = arn.GetUserFromTable("GoogleToUser", googleUser.Sub)
|
||||
|
||||
if getErr == nil && user != nil {
|
||||
authLog.Info("User logged in via Google ID", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
|
||||
|
||||
user.LastLogin = arn.DateTimeUTC()
|
||||
user.Save()
|
||||
|
||||
session.Set("userId", user.ID)
|
||||
return ctx.Redirect("/")
|
||||
}
|
||||
|
||||
// Try to find an existing user by the associated e-mail address
|
||||
// Try to find an existing user via the associated e-mail address
|
||||
user, getErr = arn.GetUserByEmail(googleUser.Email)
|
||||
|
||||
if getErr == nil && user != nil {
|
||||
authLog.Info("User logged in via Email", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
|
||||
|
||||
user.LastLogin = arn.DateTimeUTC()
|
||||
user.Save()
|
||||
|
||||
session.Set("userId", user.ID)
|
||||
return ctx.Redirect("/")
|
||||
}
|
||||
|
||||
return ctx.Error(http.StatusForbidden, "Account does not exist", getErr)
|
||||
// Register new user
|
||||
user = arn.NewUser()
|
||||
user.Nick = "g" + googleUser.Sub
|
||||
user.Email = googleUser.Email
|
||||
user.FirstName = googleUser.GivenName
|
||||
user.LastName = googleUser.FamilyName
|
||||
user.Gender = googleUser.Gender
|
||||
user.LastLogin = arn.DateTimeUTC()
|
||||
|
||||
// Save basic user info already to avoid data inconsistency problems
|
||||
user.Save()
|
||||
|
||||
// Register user
|
||||
err = arn.RegisterUser(user)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "Could not register a new user", err)
|
||||
}
|
||||
|
||||
// Connect account to a Google account
|
||||
err = user.ConnectGoogle(googleUser.Sub)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err)
|
||||
}
|
||||
|
||||
// Save user object again with updated data
|
||||
user.Save()
|
||||
|
||||
// Login
|
||||
session.Set("userId", user.ID)
|
||||
|
||||
// Log
|
||||
authLog.Info("Registered new user", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
|
||||
|
||||
// Redirect to frontpage
|
||||
return ctx.Redirect("/")
|
||||
})
|
||||
}
|
||||
|
14
auth/log.go
Normal file
14
auth/log.go
Normal file
@ -0,0 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/aerogo/log"
|
||||
)
|
||||
|
||||
var authLog = log.New()
|
||||
|
||||
func init() {
|
||||
authLog.AddOutput(os.Stdout)
|
||||
authLog.AddOutput(log.File("logs/auth.log"))
|
||||
}
|
@ -14,17 +14,15 @@
|
||||
"layout",
|
||||
"navigation",
|
||||
"headers",
|
||||
"forms",
|
||||
"input",
|
||||
"grid",
|
||||
"colors",
|
||||
"animelist",
|
||||
"forum",
|
||||
"settings",
|
||||
"user",
|
||||
"video",
|
||||
"loading",
|
||||
"fade",
|
||||
"mobile"
|
||||
"mobile",
|
||||
"extension"
|
||||
],
|
||||
"scripts": {
|
||||
"main": "main"
|
||||
|
@ -24,7 +24,15 @@ func main() {
|
||||
|
||||
// Sort
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].Registered < users[j].Registered
|
||||
if users[i].LastSeen < users[j].LastSeen {
|
||||
return false
|
||||
}
|
||||
|
||||
if users[i].LastSeen > users[j].LastSeen {
|
||||
return true
|
||||
}
|
||||
|
||||
return users[i].Registered > users[j].Registered
|
||||
})
|
||||
|
||||
// Add users to list
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
|
||||
const maxPopularAnime = 10
|
||||
|
||||
// Note this is using the airing-anime as a template with modfications
|
||||
@ -32,7 +31,7 @@ func main() {
|
||||
sort.Slice(animeList, func(i, j int) bool {
|
||||
return animeList[i].Rating.Overall > animeList[j].Rating.Overall
|
||||
})
|
||||
|
||||
|
||||
// Change size of anime list to 10
|
||||
animeList = animeList[:maxPopularAnime]
|
||||
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/flow"
|
||||
"github.com/animenotifier/arn"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
@ -12,7 +12,7 @@ import (
|
||||
func main() {
|
||||
color.Yellow("Updating search index")
|
||||
|
||||
aero.Parallel(updateAnimeIndex, updateUserIndex)
|
||||
flow.Parallel(updateAnimeIndex, updateUserIndex)
|
||||
|
||||
color.Green("Finished.")
|
||||
}
|
||||
|
@ -9,5 +9,5 @@ import (
|
||||
// Render layout.
|
||||
func Render(ctx *aero.Context, content string) string {
|
||||
user := utils.GetUser(ctx)
|
||||
return components.Layout(ctx.App, user, content)
|
||||
return components.Layout(ctx.App, ctx, user, content)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
component Layout(app *aero.Application, user *arn.User, content string)
|
||||
component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, content string)
|
||||
html(lang="en")
|
||||
head
|
||||
title= app.Config.Title
|
||||
@ -6,7 +6,7 @@ component Layout(app *aero.Application, user *arn.User, content string)
|
||||
meta(name="theme-color", content=app.Config.Manifest.ThemeColor)
|
||||
link(rel="manifest", href="/manifest.json")
|
||||
body
|
||||
#container
|
||||
#container(class=utils.GetContainerClass(ctx))
|
||||
#header
|
||||
Navigation(user)
|
||||
#content-container
|
||||
|
6
main.go
6
main.go
@ -14,6 +14,7 @@ import (
|
||||
"github.com/animenotifier/notify.moe/pages/animelist"
|
||||
"github.com/animenotifier/notify.moe/pages/animelistitem"
|
||||
"github.com/animenotifier/notify.moe/pages/dashboard"
|
||||
"github.com/animenotifier/notify.moe/pages/embed"
|
||||
"github.com/animenotifier/notify.moe/pages/forum"
|
||||
"github.com/animenotifier/notify.moe/pages/forums"
|
||||
"github.com/animenotifier/notify.moe/pages/login"
|
||||
@ -26,7 +27,6 @@ import (
|
||||
"github.com/animenotifier/notify.moe/pages/user"
|
||||
"github.com/animenotifier/notify.moe/pages/users"
|
||||
"github.com/animenotifier/notify.moe/pages/webdev"
|
||||
"github.com/animenotifier/notify.moe/utils"
|
||||
)
|
||||
|
||||
var app = aero.New()
|
||||
@ -71,19 +71,21 @@ func configure(app *aero.Application) *aero.Application {
|
||||
app.Ajax("/login", login.Get)
|
||||
app.Ajax("/airing", airing.Get)
|
||||
app.Ajax("/webdev", webdev.Get)
|
||||
app.Ajax("/extension/embed", embed.Get)
|
||||
// app.Ajax("/genres", genres.Get)
|
||||
// app.Ajax("/genres/:name", genre.Get)
|
||||
|
||||
// Middleware
|
||||
app.Use(middleware.Log())
|
||||
app.Use(middleware.Session())
|
||||
app.Use(middleware.UserInfo())
|
||||
|
||||
// API
|
||||
api := api.New("/api/", arn.DB)
|
||||
api.Install(app)
|
||||
|
||||
// Domain
|
||||
if utils.IsDevelopment() {
|
||||
if arn.IsDevelopment() {
|
||||
app.Config.Domain = "beta.notify.moe"
|
||||
}
|
||||
|
||||
|
43
middleware/IPToHost.go
Normal file
43
middleware/IPToHost.go
Normal file
@ -0,0 +1,43 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var ipToHosts = cache.New(60*time.Minute, 30*time.Minute)
|
||||
|
||||
// GetHostsForIP returns all host names for the given IP (if cached).
|
||||
func GetHostsForIP(ip string) []string {
|
||||
hosts, found := ipToHosts.Get(ip)
|
||||
|
||||
if !found {
|
||||
hosts = findHostsForIP(ip)
|
||||
}
|
||||
|
||||
if hosts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return hosts.([]string)
|
||||
}
|
||||
|
||||
// Finds all host names for the given IP
|
||||
func findHostsForIP(ip string) []string {
|
||||
hosts, err := net.LookupAddr(ip)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cache host names
|
||||
ipToHosts.Set(ip, hosts, cache.DefaultExpiration)
|
||||
|
||||
return hosts
|
||||
}
|
@ -9,40 +9,66 @@ import (
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/log"
|
||||
"github.com/animenotifier/notify.moe/utils"
|
||||
)
|
||||
|
||||
var request = log.New()
|
||||
var err = log.New()
|
||||
|
||||
// Initialize log files
|
||||
func init() {
|
||||
request.AddOutput(log.File("logs/request.log"))
|
||||
|
||||
err.AddOutput(log.File("logs/error.log"))
|
||||
err.AddOutput(os.Stderr)
|
||||
}
|
||||
|
||||
// Log middleware logs every request into logs/request.log and errors into logs/error.log.
|
||||
func Log() aero.Middleware {
|
||||
request := log.New()
|
||||
request.AddOutput(log.File("logs/request.log"))
|
||||
|
||||
err := log.New()
|
||||
err.AddOutput(log.File("logs/error.log"))
|
||||
err.AddOutput(os.Stderr)
|
||||
|
||||
return func(ctx *aero.Context, next func()) {
|
||||
start := time.Now()
|
||||
next()
|
||||
responseTime := time.Since(start)
|
||||
responseTimeString := strconv.Itoa(int(responseTime.Nanoseconds()/1000000)) + " ms"
|
||||
responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString
|
||||
|
||||
// Log every request
|
||||
request.Info(ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI())
|
||||
|
||||
// Log all requests that failed
|
||||
switch ctx.StatusCode {
|
||||
case http.StatusOK, http.StatusFound, http.StatusMovedPermanently, http.StatusPermanentRedirect, http.StatusTemporaryRedirect:
|
||||
// Ok.
|
||||
|
||||
default:
|
||||
err.Error(http.StatusText(ctx.StatusCode), ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI())
|
||||
}
|
||||
|
||||
// Notify us about long requests.
|
||||
// However ignore requests under /auth/ because those depend on 3rd party servers.
|
||||
if responseTime >= 200*time.Millisecond && !strings.HasPrefix(ctx.URI(), "/auth/") {
|
||||
err.Error("Long response time", ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI())
|
||||
}
|
||||
go logRequest(ctx, responseTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Logs a single request
|
||||
func logRequest(ctx *aero.Context, responseTime time.Duration) {
|
||||
responseTimeString := strconv.Itoa(int(responseTime.Nanoseconds()/1000000)) + " ms"
|
||||
responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString
|
||||
|
||||
user := utils.GetUser(ctx)
|
||||
ip := ctx.RealIP()
|
||||
|
||||
hostName := "<unknown host>"
|
||||
hostNames := GetHostsForIP(ip)
|
||||
|
||||
if len(hostNames) != 0 {
|
||||
hostName = hostNames[0]
|
||||
hostName = strings.TrimSuffix(hostName, ".")
|
||||
}
|
||||
|
||||
// Log every request
|
||||
if user != nil {
|
||||
request.Info(user.Nick, ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI())
|
||||
} else {
|
||||
request.Info("[guest]", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI())
|
||||
}
|
||||
|
||||
// Log all requests that failed
|
||||
switch ctx.StatusCode {
|
||||
case http.StatusOK, http.StatusFound, http.StatusMovedPermanently, http.StatusPermanentRedirect, http.StatusTemporaryRedirect:
|
||||
// Ok.
|
||||
|
||||
default:
|
||||
err.Error(http.StatusText(ctx.StatusCode), ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI())
|
||||
}
|
||||
|
||||
// Notify us about long requests.
|
||||
// However ignore requests under /auth/ because those depend on 3rd party servers.
|
||||
if responseTime >= 200*time.Millisecond && !strings.HasPrefix(ctx.URI(), "/auth/") {
|
||||
err.Error("Long response time", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI())
|
||||
}
|
||||
}
|
||||
|
114
middleware/UserInfo.go
Normal file
114
middleware/UserInfo.go
Normal file
@ -0,0 +1,114 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/animenotifier/arn"
|
||||
"github.com/animenotifier/notify.moe/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mssola/user_agent"
|
||||
"github.com/parnurzeal/gorequest"
|
||||
)
|
||||
|
||||
var apiKeys arn.APIKeys
|
||||
|
||||
func init() {
|
||||
data, _ := ioutil.ReadFile("security/api-keys.json")
|
||||
err := json.Unmarshal(data, &apiKeys)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// UserInfo updates user related information after each request.
|
||||
func UserInfo() aero.Middleware {
|
||||
return func(ctx *aero.Context, next func()) {
|
||||
next()
|
||||
|
||||
// Ignore non-HTML requests
|
||||
if ctx.IsMediaResponse() {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore API requests
|
||||
if strings.HasPrefix(ctx.URI(), "/api/") {
|
||||
return
|
||||
}
|
||||
|
||||
user := utils.GetUser(ctx)
|
||||
|
||||
// When there's no user logged in, nothing to update
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// This works asynchronously so it doesn't block the response
|
||||
go updateUserInfo(ctx, user)
|
||||
}
|
||||
}
|
||||
|
||||
// Update browser and OS data
|
||||
func updateUserInfo(ctx *aero.Context, user *arn.User) {
|
||||
newIP := ctx.RealIP()
|
||||
newUserAgent := ctx.UserAgent()
|
||||
|
||||
if user.UserAgent != newUserAgent {
|
||||
user.UserAgent = newUserAgent
|
||||
|
||||
// Parse user agent
|
||||
parsed := user_agent.New(user.UserAgent)
|
||||
|
||||
// Browser
|
||||
user.Browser.Name, user.Browser.Version = parsed.Browser()
|
||||
|
||||
// OS
|
||||
os := parsed.OSInfo()
|
||||
user.OS.Name = os.Name
|
||||
user.OS.Version = os.Version
|
||||
}
|
||||
|
||||
if user.IP != newIP {
|
||||
updateUserLocation(user, newIP)
|
||||
}
|
||||
|
||||
user.LastSeen = arn.DateTimeUTC()
|
||||
user.Save()
|
||||
}
|
||||
|
||||
// Updates the location of the user.
|
||||
func updateUserLocation(user *arn.User, newIP string) {
|
||||
user.IP = newIP
|
||||
locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json"
|
||||
|
||||
response, data, err := gorequest.New().Get(locationAPI).EndBytes()
|
||||
|
||||
if len(err) > 0 && err[0] != nil {
|
||||
color.Red("Couldn't fetch location data | Error: %s | IP: %s", err[0].Error(), user.IP)
|
||||
return
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
color.Red("Couldn't fetch location data | Status: %d | IP: %s", response.StatusCode, user.IP)
|
||||
return
|
||||
}
|
||||
|
||||
newLocation := arn.IPInfoDBLocation{}
|
||||
json.Unmarshal(data, &newLocation)
|
||||
|
||||
if newLocation.CountryName != "-" {
|
||||
user.Location.CountryName = newLocation.CountryName
|
||||
user.Location.CountryCode = newLocation.CountryCode
|
||||
user.Location.Latitude, _ = strconv.ParseFloat(newLocation.Latitude, 64)
|
||||
user.Location.Longitude, _ = strconv.ParseFloat(newLocation.Longitude, 64)
|
||||
user.Location.CityName = newLocation.CityName
|
||||
user.Location.RegionName = newLocation.RegionName
|
||||
user.Location.TimeZone = newLocation.TimeZone
|
||||
user.Location.ZipCode = newLocation.ZipCode
|
||||
}
|
||||
}
|
@ -2,4 +2,4 @@ component AnimeGrid(animeList []*arn.Anime)
|
||||
.anime-grid
|
||||
each anime in animeList
|
||||
a.anime-grid-cell.ajax(href="/anime/" + toString(anime.ID))
|
||||
img.anime-grid-image(src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")")
|
||||
img.anime-grid-image.lazy(data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")")
|
@ -4,7 +4,7 @@ component Avatar(user *arn.User)
|
||||
|
||||
component AvatarNoLink(user *arn.User)
|
||||
if user.HasAvatar()
|
||||
img.user-image(src=user.SmallAvatar(), alt=user.Nick)
|
||||
img.user-image.lazy(data-src=user.SmallAvatar(), alt=user.Nick)
|
||||
else
|
||||
SVGAvatar
|
||||
|
||||
|
@ -1,14 +1,19 @@
|
||||
component InputText(id string, value string, label string, placeholder string)
|
||||
.widget-input
|
||||
label(for=id)= label + ":"
|
||||
input.widget-element.action(id=id, type="text", value=value, placeholder=placeholder, data-action="save", data-trigger="change")
|
||||
input.widget-element.action(id=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")
|
||||
|
||||
component InputTextArea(id string, value string, label string, placeholder string)
|
||||
.widget-input
|
||||
label(for=id)= label + ":"
|
||||
textarea.widget-element.action(id=id, placeholder=placeholder, data-action="save", data-trigger="change")= value
|
||||
textarea.widget-element.action(id=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")= value
|
||||
|
||||
component InputNumber(id string, value int, label string, placeholder string, min string, max string)
|
||||
.widget-input
|
||||
label(for=id)= label + ":"
|
||||
input.widget-element.action(id=id, type="number", value=value, min=min, max=max, placeholder=placeholder, data-action="save", data-trigger="change")
|
||||
input.widget-element.action(id=id, type="number", value=value, min=min, max=max, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")
|
||||
|
||||
component InputSelection(id string, value string, label string, placeholder string)
|
||||
.widget-input
|
||||
label(for=id)= label + ":"
|
||||
select.widget-element.action(id=id, value=value, title=placeholder, data-action="save", data-trigger="change")
|
||||
|
@ -39,7 +39,7 @@ component LoggedInMenu(user *arn.User)
|
||||
NavigationButtonNoAJAX("Logout", "/logout", "sign-out")
|
||||
|
||||
component FuzzySearch
|
||||
input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: Ctrl + Q")
|
||||
input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: F")
|
||||
|
||||
component NavigationButton(name string, target string, icon string)
|
||||
a.navigation-link.ajax(href=target, aria-label=name, title=name)
|
||||
|
@ -159,8 +159,11 @@ component Anime(anime *arn.Anime, user *arn.User)
|
||||
//- if providers.AnimePlanet
|
||||
//- a.light-button(href="http://www.anime-planet.com/anime/" + providers.AnimePlanet.providerId, target="_blank") AnimePlanet
|
||||
|
||||
.sources
|
||||
p Powered by Kitsu.
|
||||
.footer
|
||||
//- if user != nil && user.Role == "admin"
|
||||
//- a(href="/api/anime/" + anime.ID) Anime API
|
||||
//- span |
|
||||
span Powered by Kitsu.
|
||||
//- if descriptionSource
|
||||
//- span= " Summary by " + summarySource + "."
|
||||
//- //-
|
||||
|
@ -87,9 +87,9 @@
|
||||
.anime-rating-categories
|
||||
vertical
|
||||
|
||||
.sources
|
||||
.footer
|
||||
font-size 0.8rem
|
||||
opacity 0.5
|
||||
opacity 0.7
|
||||
margin-top 0.5rem
|
||||
|
||||
.relations
|
||||
|
@ -21,7 +21,7 @@ func Get(ctx *aero.Context) string {
|
||||
animeList := viewUser.AnimeList()
|
||||
|
||||
if animeList == nil {
|
||||
return ctx.Error(http.StatusNotFound, "Anime list not found", err)
|
||||
return ctx.Error(http.StatusNotFound, "Anime list not found", nil)
|
||||
}
|
||||
|
||||
sort.Slice(animeList.Items, func(i, j int) bool {
|
||||
|
@ -10,5 +10,8 @@ component AnimeList(animeList *arn.AnimeList)
|
||||
tr.anime-list-item.mountable(title=item.Notes)
|
||||
td.anime-list-item-name
|
||||
a.ajax(href=item.Anime().Link())= item.Anime().Title.Canonical
|
||||
td.anime-list-item-episodes= toString(item.Episodes) + " / " + item.Anime().EpisodeCountString()
|
||||
td.anime-list-item-episodes
|
||||
span.anime-list-item-episodes-watched= item.Episodes
|
||||
span.anime-list-item-episodes-separator /
|
||||
span.anime-list-item-episodes-max= item.Anime().EpisodeCountString()
|
||||
td.anime-list-item-rating= item.FinalRating()
|
@ -7,6 +7,9 @@
|
||||
|
||||
.anime-list-item-name
|
||||
flex 0.8
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
|
||||
.anime-list-item-episodes
|
||||
flex 0.1
|
||||
|
31
pages/embed/embed.go
Normal file
31
pages/embed/embed.go
Normal file
@ -0,0 +1,31 @@
|
||||
package embed
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/animenotifier/notify.moe/components"
|
||||
"github.com/animenotifier/notify.moe/utils"
|
||||
)
|
||||
|
||||
// Get anime list in the browser extension.
|
||||
func Get(ctx *aero.Context) string {
|
||||
user := utils.GetUser(ctx)
|
||||
|
||||
if user == nil {
|
||||
return ctx.Error(http.StatusUnauthorized, "Not logged in", nil)
|
||||
}
|
||||
|
||||
animeList := user.AnimeList()
|
||||
|
||||
if animeList == nil {
|
||||
return ctx.Error(http.StatusNotFound, "Anime list not found", nil)
|
||||
}
|
||||
|
||||
sort.Slice(animeList.Items, func(i, j int) bool {
|
||||
return animeList.Items[i].FinalRating() < animeList.Items[j].FinalRating()
|
||||
})
|
||||
|
||||
return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(animeList)))
|
||||
}
|
@ -11,5 +11,8 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int)
|
||||
span Load more
|
||||
|
||||
component ThreadList(threads []*arn.Thread)
|
||||
each thread in threads
|
||||
ThreadLink(thread)
|
||||
if len(threads) == 0
|
||||
p No threads found.
|
||||
else
|
||||
each thread in threads
|
||||
ThreadLink(thread)
|
16
pages/popularanime/old/popular.pixy
Normal file
16
pages/popularanime/old/popular.pixy
Normal file
@ -0,0 +1,16 @@
|
||||
component OldPopularAnime(popularAnime []*arn.Anime, titleCount int, animeCount int)
|
||||
//- h2 Anime
|
||||
|
||||
//- #search-container
|
||||
//- input#search(type="text", placeholder="Search...", onkeyup="$.searchAnime();", onfocus="this.select();", disabled="disabled", data-count=titleCount, data-anime-count=animeCount)
|
||||
|
||||
//- #search-results-container
|
||||
//- #search-results
|
||||
|
||||
//- if popularAnime != nil
|
||||
//- h3.popular-title Popular
|
||||
|
||||
//- .popular-anime-list
|
||||
//- each anime in popularAnime
|
||||
//- a.popular-anime.ajax(href="/anime/" + toString(anime.ID), title=anime.Title.Romaji + " (" + arn.Plural(anime.Watching(), "user") + " watching)")
|
||||
//- img.anime-image.popular-anime-image(src=anime.Image, alt=anime.Title.Romaji)
|
19
pages/popularanime/old/popular.scarlet
Normal file
19
pages/popularanime/old/popular.scarlet
Normal file
@ -0,0 +1,19 @@
|
||||
// .popular-title
|
||||
// text-align center
|
||||
|
||||
// .popular-anime-list
|
||||
// display flex
|
||||
// flex-flow row wrap
|
||||
// justify-content center
|
||||
|
||||
// .popular-anime
|
||||
// padding 0.5em
|
||||
// display block
|
||||
|
||||
// .popular-anime-image
|
||||
// width 100px !important
|
||||
// height 141px !important
|
||||
// border-radius 3px
|
||||
// object-fit cover
|
||||
// default-transition
|
||||
// shadow-up
|
@ -1,6 +1,8 @@
|
||||
package popularanime
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/animenotifier/arn"
|
||||
"github.com/animenotifier/notify.moe/components"
|
||||
@ -8,29 +10,10 @@ import (
|
||||
|
||||
// Get search page.
|
||||
func Get(ctx *aero.Context) string {
|
||||
// titleCount := 0
|
||||
// animeCount := 0
|
||||
|
||||
// // let info: any = await bluebird.props({
|
||||
// // popular: arn.db.get('Cache', 'popularAnime'),
|
||||
// // stats: arn.db.get('Cache', 'animeStats')
|
||||
// // })
|
||||
|
||||
// // return response.render({
|
||||
// // user,
|
||||
// // popularAnime: info.popular.anime,
|
||||
// // animeCount: info.stats.animeCount,
|
||||
// // titleCount: info.stats.titleCount,
|
||||
// // anime: null
|
||||
// // })
|
||||
|
||||
// popular, _ := arn.GetPopularCache()
|
||||
|
||||
// return ctx.HTML(components.Search(popular.Anime, titleCount, animeCount))
|
||||
animeList, err := arn.GetPopularAnimeCached()
|
||||
|
||||
if err != nil {
|
||||
return ctx.HTML("There was a problem listing anime!")
|
||||
return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err)
|
||||
}
|
||||
|
||||
return ctx.HTML(components.AnimeGrid(animeList))
|
||||
|
@ -1,16 +0,0 @@
|
||||
component Search(popularAnime []*arn.Anime, titleCount int, animeCount int)
|
||||
h2 Anime
|
||||
|
||||
#search-container
|
||||
input#search(type="text", placeholder="Search...", onkeyup="$.searchAnime();", onfocus="this.select();", disabled="disabled", data-count=titleCount, data-anime-count=animeCount)
|
||||
|
||||
#search-results-container
|
||||
#search-results
|
||||
|
||||
if popularAnime != nil
|
||||
h3.popular-title Popular
|
||||
|
||||
.popular-anime-list
|
||||
each anime in popularAnime
|
||||
a.popular-anime.ajax(href="/anime/" + toString(anime.ID), title=anime.Title.Romaji + " (" + arn.Plural(anime.Watching(), "user") + " watching)")
|
||||
img.anime-image.popular-anime-image(src=anime.Image, alt=anime.Title.Romaji)
|
@ -1,19 +0,0 @@
|
||||
.popular-title
|
||||
text-align center
|
||||
|
||||
.popular-anime-list
|
||||
display flex
|
||||
flex-flow row wrap
|
||||
justify-content center
|
||||
|
||||
.popular-anime
|
||||
padding 0.5em
|
||||
display block
|
||||
|
||||
.popular-anime-image
|
||||
width 100px !important
|
||||
height 141px !important
|
||||
border-radius 3px
|
||||
object-fit cover
|
||||
default-transition
|
||||
shadow-up
|
@ -2,6 +2,7 @@ package profile
|
||||
|
||||
import (
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/flow"
|
||||
"github.com/animenotifier/arn"
|
||||
"github.com/animenotifier/notify.moe/components"
|
||||
"github.com/animenotifier/notify.moe/utils"
|
||||
@ -28,7 +29,7 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string {
|
||||
var animeList *arn.AnimeList
|
||||
var posts []*arn.Post
|
||||
|
||||
aero.Parallel(func() {
|
||||
flow.Parallel(func() {
|
||||
user = utils.GetUser(ctx)
|
||||
}, func() {
|
||||
animeList = viewUser.AnimeList()
|
||||
|
@ -57,7 +57,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList,
|
||||
else
|
||||
each item in animeList.Items
|
||||
a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")")
|
||||
img.anime-cover-image.profile-watching-list-item-image(src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical)
|
||||
img.anime-cover-image.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical)
|
||||
|
||||
.profile-category.mountable
|
||||
h3
|
||||
@ -79,7 +79,11 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList,
|
||||
.post-author
|
||||
Avatar(post.Author())
|
||||
.post-content
|
||||
.mountable!= aero.Markdown(post.Text)
|
||||
div!= aero.Markdown(post.Text)
|
||||
.post-toolbar.active
|
||||
.spacer
|
||||
.post-likes= len(post.Likes)
|
||||
|
||||
//- if user != nil && user.Role == "admin"
|
||||
//- .footer
|
||||
//- a(href="/api/user/" + viewUser.ID) User API
|
@ -6,4 +6,8 @@
|
||||
|
||||
.profile-watching-list-item-image
|
||||
width 55px !important
|
||||
border-radius 2px
|
||||
border-radius 2px
|
||||
|
||||
< 380px
|
||||
.profile-watching-list
|
||||
justify-content center
|
@ -14,5 +14,5 @@ func Get(ctx *aero.Context) string {
|
||||
term := ctx.Get("term")
|
||||
|
||||
userResults, animeResults := arn.Search(term, maxUsers, maxAnime)
|
||||
return ctx.HTML(components.Search(userResults, animeResults))
|
||||
return ctx.HTML(components.SearchResults(userResults, animeResults))
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
component Search(users []*arn.User, animeResults []*arn.Anime)
|
||||
component SearchResults(users []*arn.User, animeResults []*arn.Anime)
|
||||
.widgets
|
||||
.widget
|
||||
h3 Users
|
||||
|
@ -16,5 +16,5 @@ func Get(ctx *aero.Context) string {
|
||||
return ctx.Error(http.StatusForbidden, "Not logged in", nil)
|
||||
}
|
||||
|
||||
return ctx.HTML(components.Settings(user))
|
||||
return utils.AllowEmbed(ctx, ctx.HTML(components.Settings(user)))
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
component Settings(user *arn.User)
|
||||
h2.page-title Settings
|
||||
.widgets(data-api="/api/user/" + user.ID)
|
||||
.widget.mountable
|
||||
.widgets
|
||||
.widget.mountable(data-api="/api/user/" + user.ID)
|
||||
h3.widget-title
|
||||
Icon("user")
|
||||
span Personal
|
||||
@ -10,11 +10,19 @@ component Settings(user *arn.User)
|
||||
InputText("Tagline", user.Tagline, "Tagline", "Text that appears below your username")
|
||||
InputText("Website", user.Website, "Website", "Your homepage")
|
||||
|
||||
.widget.mountable
|
||||
.widget.mountable(data-api="/api/user/" + user.ID)
|
||||
h3.widget-title
|
||||
Icon("cubes")
|
||||
span Accounts
|
||||
|
||||
InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co")
|
||||
InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net")
|
||||
InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io")
|
||||
InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io")
|
||||
InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com")
|
||||
|
||||
//- .widget.mountable(data-api="/api/settings/" + user.ID)
|
||||
//- h3.widget-title
|
||||
//- Icon("cogs")
|
||||
//- span Settings
|
||||
|
||||
//- InputText("TitleLanguage", user.Settings().TitleLanguage, "Title language", "Language of anime titles")
|
29
patches/add-last-seen/main.go
Normal file
29
patches/add-last-seen/main.go
Normal file
@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/animenotifier/arn"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Get a stream of all users
|
||||
allUsers, err := arn.AllUsers()
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Iterate over the stream
|
||||
for user := range allUsers {
|
||||
if user.LastSeen != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
user.LastSeen = user.LastLogin
|
||||
|
||||
if user.LastSeen == "" {
|
||||
user.LastSeen = user.Registered
|
||||
}
|
||||
|
||||
user.Save()
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ func main() {
|
||||
count++
|
||||
println(count, user.Nick)
|
||||
|
||||
user.SetNick(user.Nick)
|
||||
user.ForceSetNick(user.Nick)
|
||||
|
||||
if user.Email != "" {
|
||||
user.SetEmail(user.Email)
|
||||
|
@ -5,9 +5,34 @@ import * as actions from "./actions"
|
||||
|
||||
export class AnimeNotifier {
|
||||
app: Application
|
||||
visibilityObserver: IntersectionObserver
|
||||
|
||||
constructor(app: Application) {
|
||||
this.app = app
|
||||
|
||||
if("IntersectionObserver" in window) {
|
||||
// Enable lazy load
|
||||
this.visibilityObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
for(let entry of entries) {
|
||||
if(entry.intersectionRatio > 0) {
|
||||
entry.target["became visible"]()
|
||||
this.visibilityObserver.unobserve(entry.target)
|
||||
}
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
} else {
|
||||
// Disable lazy load feature
|
||||
this.visibilityObserver = {
|
||||
disconnect: () => {},
|
||||
observe: (elem: HTMLElement) => {
|
||||
elem["became visible"]()
|
||||
},
|
||||
unobserve: (elem: HTMLElement) => {}
|
||||
} as IntersectionObserver
|
||||
}
|
||||
}
|
||||
|
||||
onReadyStateChange() {
|
||||
@ -24,12 +49,22 @@ export class AnimeNotifier {
|
||||
this.app.run()
|
||||
}
|
||||
|
||||
onContentLoaded() {
|
||||
this.visibilityObserver.disconnect()
|
||||
|
||||
// Update each of these asynchronously
|
||||
Promise.resolve().then(() => this.updateMountables())
|
||||
Promise.resolve().then(() => this.updateActions())
|
||||
Promise.resolve().then(() => this.lazyLoadImages())
|
||||
}
|
||||
|
||||
reloadContent() {
|
||||
return fetch("/_" + this.app.currentPath, {
|
||||
credentials: "same-origin"
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => Diff.innerHTML(this.app.content, html))
|
||||
.then(() => this.app.emit("DOMContentLoaded"))
|
||||
}
|
||||
|
||||
loading(isLoading: boolean) {
|
||||
@ -42,32 +77,48 @@ export class AnimeNotifier {
|
||||
|
||||
updateActions() {
|
||||
for(let element of findAll("action")) {
|
||||
if(element["action assigned"]) {
|
||||
continue
|
||||
}
|
||||
|
||||
let actionName = element.dataset.action
|
||||
|
||||
element.addEventListener(element.dataset.trigger, e => {
|
||||
actions[actionName](this, element, e)
|
||||
})
|
||||
|
||||
element.classList.remove("action")
|
||||
// Use "action assigned" flag instead of removing the class.
|
||||
// This will make sure that DOM diffs which restore the class name
|
||||
// will not assign the action multiple times to the same element.
|
||||
element["action assigned"] = true
|
||||
}
|
||||
}
|
||||
|
||||
updateAvatars() {
|
||||
for(let element of findAll("user-image")) {
|
||||
let img = element as HTMLImageElement
|
||||
lazyLoadImages() {
|
||||
for(let element of findAll("lazy")) {
|
||||
this.lazyLoadImage(element as HTMLImageElement)
|
||||
}
|
||||
}
|
||||
|
||||
lazyLoadImage(img: HTMLImageElement) {
|
||||
// Once the image becomes visible, load it
|
||||
img["became visible"] = () => {
|
||||
img.src = img.dataset.src
|
||||
|
||||
if(img.naturalWidth === 0) {
|
||||
img.onload = function() {
|
||||
this.classList.add("user-image-found")
|
||||
this.classList.add("image-found")
|
||||
}
|
||||
|
||||
img.onerror = function() {
|
||||
this.classList.add("user-image-not-found")
|
||||
this.classList.add("image-not-found")
|
||||
}
|
||||
} else {
|
||||
img.classList.add("user-image-found")
|
||||
img.classList.add("image-found")
|
||||
}
|
||||
}
|
||||
|
||||
this.visibilityObserver.observe(img)
|
||||
}
|
||||
|
||||
updateMountables() {
|
||||
@ -89,13 +140,6 @@ export class AnimeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
onContentLoaded() {
|
||||
// Update each of these asynchronously
|
||||
Promise.resolve().then(() => this.updateMountables())
|
||||
Promise.resolve().then(() => this.updateAvatars())
|
||||
Promise.resolve().then(() => this.updateActions())
|
||||
}
|
||||
|
||||
onPopState(e: PopStateEvent) {
|
||||
if(e.state) {
|
||||
this.app.load(e.state, {
|
||||
@ -109,8 +153,15 @@ export class AnimeNotifier {
|
||||
}
|
||||
|
||||
onKeyDown(e: KeyboardEvent) {
|
||||
// Ctrl + Q = Search
|
||||
if(e.ctrlKey && e.keyCode == 81) {
|
||||
// Ignore hotkeys on input elements
|
||||
switch(document.activeElement.tagName) {
|
||||
case "INPUT":
|
||||
case "TEXTAREA":
|
||||
return
|
||||
}
|
||||
|
||||
// F = Search
|
||||
if(e.keyCode == 70) {
|
||||
let search = this.app.find("search") as HTMLInputElement
|
||||
|
||||
search.focus()
|
||||
|
@ -25,6 +25,10 @@ export class Diff {
|
||||
}
|
||||
|
||||
if(a.nodeType === Node.ELEMENT_NODE) {
|
||||
if(a.tagName === "IFRAME") {
|
||||
continue
|
||||
}
|
||||
|
||||
let removeAttributes: Attr[] = []
|
||||
|
||||
for(let x = 0; x < a.attributes.length; x++) {
|
||||
@ -48,6 +52,11 @@ export class Diff {
|
||||
a.setAttribute(attrib.name, b.getAttribute(attrib.name))
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: Apply state of input elements
|
||||
if(a !== document.activeElement && a instanceof HTMLInputElement && b instanceof HTMLInputElement) {
|
||||
a.value = b.value
|
||||
}
|
||||
}
|
||||
|
||||
Diff.childNodes(a, b)
|
||||
@ -57,7 +66,7 @@ export class Diff {
|
||||
static innerHTML(aRoot: HTMLElement, html: string) {
|
||||
let bRoot = document.createElement("main")
|
||||
bRoot.innerHTML = html
|
||||
|
||||
|
||||
Diff.childNodes(aRoot, bRoot)
|
||||
}
|
||||
}
|
@ -48,6 +48,8 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE
|
||||
.then(() => {
|
||||
arn.loading(false)
|
||||
input.disabled = false
|
||||
|
||||
return arn.reloadContent()
|
||||
})
|
||||
}
|
||||
|
||||
|
36
styles/extension.scarlet
Normal file
36
styles/extension.scarlet
Normal file
@ -0,0 +1,36 @@
|
||||
.embedded
|
||||
flex-direction column-reverse !important
|
||||
|
||||
.anime-list
|
||||
max-width 500px
|
||||
margin -1.1rem
|
||||
|
||||
thead
|
||||
display none
|
||||
|
||||
.anime-list-item
|
||||
// ui-element
|
||||
// margin-bottom 0.5rem
|
||||
|
||||
.anime-list-item-episodes
|
||||
horizontal
|
||||
text-align right
|
||||
white-space nowrap
|
||||
flex 0.2
|
||||
|
||||
.anime-list-item-episodes-watched
|
||||
flex 0.4
|
||||
|
||||
.anime-list-item-episodes-max
|
||||
opacity 0.5
|
||||
flex 0.4
|
||||
|
||||
.anime-list-item-episodes-separator
|
||||
opacity 0.5
|
||||
flex 0.2
|
||||
|
||||
.anime-list-item-rating
|
||||
display none
|
||||
|
||||
#navigation
|
||||
font-size 0.9rem
|
@ -3,9 +3,19 @@
|
||||
// margin-bottom 1rem
|
||||
|
||||
.thread-link
|
||||
horizontal
|
||||
vertical
|
||||
margin 0.25rem 0
|
||||
|
||||
.post-author
|
||||
margin-bottom 0.25rem
|
||||
|
||||
> 330px
|
||||
.thread-link
|
||||
horizontal
|
||||
|
||||
.post-author
|
||||
margin-bottom 0
|
||||
|
||||
.post-content
|
||||
ui-element
|
||||
flex-grow 1
|
||||
|
@ -32,6 +32,7 @@ mixin grid-image
|
||||
height 100%
|
||||
border-radius 3px
|
||||
object-fit cover
|
||||
default-transition
|
||||
|
||||
mixin grid-icon
|
||||
font-size 2.5rem
|
||||
|
11
styles/images.scarlet
Normal file
11
styles/images.scarlet
Normal file
@ -0,0 +1,11 @@
|
||||
.lazy
|
||||
visibility hidden
|
||||
opacity 0
|
||||
|
||||
.image-found
|
||||
visibility visible
|
||||
opacity 1 !important
|
||||
|
||||
.image-not-found
|
||||
visibility hidden
|
||||
opacity 0 !important
|
@ -1,3 +1,10 @@
|
||||
mixin input-focus
|
||||
:focus
|
||||
color black
|
||||
border 1px solid main-color
|
||||
// TODO: Replace with alpha(main-color, 20%) function
|
||||
box-shadow 0 0 6px rgba(248, 165, 130, 0.2)
|
||||
|
||||
input, textarea, button, select
|
||||
font-family inherit
|
||||
font-size 1em
|
||||
@ -9,16 +16,15 @@ input, textarea
|
||||
border ui-border
|
||||
background white
|
||||
box-shadow none
|
||||
|
||||
:focus
|
||||
color black
|
||||
border 1px solid main-color
|
||||
// TODO: Replace with alpha(main-color, 20%) function
|
||||
box-shadow 0 0 6px rgba(248, 165, 130, 0.2)
|
||||
input-focus
|
||||
|
||||
:disabled
|
||||
ui-disabled
|
||||
|
||||
// We need this to have a selector with a higher priority than .widget-element:focus
|
||||
input.widget-element
|
||||
input-focus
|
||||
|
||||
button, .button
|
||||
ui-element
|
||||
horizontal
|
@ -4,13 +4,6 @@
|
||||
height avatar-size
|
||||
border-radius 100%
|
||||
object-fit cover
|
||||
opacity 0
|
||||
default-transition
|
||||
:hover
|
||||
box-shadow outline-shadow-heavy
|
||||
|
||||
.user-image-found
|
||||
opacity 1 !important
|
||||
|
||||
.user-image-not-found
|
||||
opacity 0 !important
|
||||
box-shadow outline-shadow-heavy
|
12
tests.go
12
tests.go
@ -10,6 +10,10 @@ var tests = map[string][]string{
|
||||
"/+Akyoto/threads",
|
||||
},
|
||||
|
||||
"/user/:nick/posts": []string{
|
||||
"/+Akyoto/posts",
|
||||
},
|
||||
|
||||
"/user/:nick/animelist": []string{
|
||||
"/+Akyoto/animelist",
|
||||
},
|
||||
@ -56,6 +60,14 @@ var tests = map[string][]string{
|
||||
"/api/animelist/4J6qpK1ve",
|
||||
},
|
||||
|
||||
"/api/animelist/:id/get/:item": []string{
|
||||
"/api/animelist/4J6qpK1ve/get/7929",
|
||||
},
|
||||
|
||||
"/api/animelist/:id/get/:item/:property": []string{
|
||||
"/api/animelist/4J6qpK1ve/get/7929/Episodes",
|
||||
},
|
||||
|
||||
"/api/settings/:id": []string{
|
||||
"/api/settings/4J6qpK1ve",
|
||||
},
|
||||
|
10
utils/allowembed.go
Normal file
10
utils/allowembed.go
Normal file
@ -0,0 +1,10 @@
|
||||
package utils
|
||||
|
||||
import "github.com/aerogo/aero"
|
||||
|
||||
// AllowEmbed allows the page to be called by the browser extension.
|
||||
func AllowEmbed(ctx *aero.Context, response string) string {
|
||||
// This is a bit of a hack.
|
||||
ctx.SetResponseHeader("X-Frame-Options", "ALLOW-FROM chrome-extension://hjfcooigdelogjmniiahfiilcefdlpha/options.html")
|
||||
return response
|
||||
}
|
15
utils/container.go
Normal file
15
utils/container.go
Normal file
@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/aerogo/aero"
|
||||
)
|
||||
|
||||
// GetContainerClass returns the class for the "container" element.
|
||||
// In the browser extension it will get the "embedded" class.
|
||||
func GetContainerClass(ctx *aero.Context) string {
|
||||
if ctx.URI() == "/extension/embed" {
|
||||
return "embedded"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsProduction returns true if the hostname contains "arn".
|
||||
func IsProduction() bool {
|
||||
host, _ := os.Hostname()
|
||||
return strings.Contains(host, "arn")
|
||||
}
|
||||
|
||||
// IsDevelopment returns true if the hostname does not contain "arn".
|
||||
func IsDevelopment() bool {
|
||||
return !IsProduction()
|
||||
}
|
@ -5,23 +5,7 @@ import (
|
||||
"github.com/animenotifier/arn"
|
||||
)
|
||||
|
||||
// GetUser ...
|
||||
// GetUser returns the logged in user for the given context.
|
||||
func GetUser(ctx *aero.Context) *arn.User {
|
||||
if !ctx.HasSession() {
|
||||
return nil
|
||||
}
|
||||
|
||||
userID := ctx.Session().GetString("userId")
|
||||
|
||||
if userID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := arn.GetUser(userID)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return user
|
||||
return arn.GetUserFromContext(ctx)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user