commit
b1e9740a83
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@ -6,10 +6,15 @@
|
|||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/*.js": {
|
"**/*.js": {
|
||||||
"when": "$(basename).ts"
|
"when": "$(basename).ts"
|
||||||
}
|
},
|
||||||
|
"components/": true
|
||||||
},
|
},
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.pixy": "jade",
|
"*.pixy": "jade",
|
||||||
"*.scarlet": "stylus"
|
"*.scarlet": "stylus"
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"components/": true,
|
||||||
|
"**/*.svg": true
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,9 @@
|
|||||||
# Anime Notifier
|
# Anime Notifier
|
||||||
|
|
||||||
|
## Info
|
||||||
|
|
||||||
|
notify.moe is powered by the [Aero framework](https://github.com/aerogo/aero) from the same author. The project also uses Go and Aerospike.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
@ -7,21 +7,21 @@ import (
|
|||||||
|
|
||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/aero"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
"github.com/animenotifier/notify.moe/components"
|
"github.com/animenotifier/notify.moe/components/js"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Scripts
|
// Scripts
|
||||||
js := components.JS()
|
scripts := js.Bundle()
|
||||||
|
|
||||||
app.Get("/scripts", func(ctx *aero.Context) string {
|
app.Get("/scripts", func(ctx *aero.Context) string {
|
||||||
ctx.SetResponseHeader("Content-Type", "application/javascript")
|
ctx.SetResponseHeader("Content-Type", "application/javascript")
|
||||||
return js
|
return scripts
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Get("/scripts.js", func(ctx *aero.Context) string {
|
app.Get("/scripts.js", func(ctx *aero.Context) string {
|
||||||
ctx.SetResponseHeader("Content-Type", "application/javascript")
|
ctx.SetResponseHeader("Content-Type", "application/javascript")
|
||||||
return js
|
return scripts
|
||||||
})
|
})
|
||||||
|
|
||||||
// Web manifest
|
// Web manifest
|
||||||
|
27
benchmarks/Components_test.go
Normal file
27
benchmarks/Components_test.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package benchmarks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/animenotifier/arn"
|
||||||
|
"github.com/animenotifier/notify.moe/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkThread(b *testing.B) {
|
||||||
|
thread, _ := arn.GetThread("HJgS7c2K")
|
||||||
|
thread.HTML() // Pre-render markdown
|
||||||
|
|
||||||
|
replies, _ := arn.FilterPosts(func(post *arn.Post) bool {
|
||||||
|
post.HTML() // Pre-render markdown
|
||||||
|
return post.ThreadID == thread.ID
|
||||||
|
})
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
components.Thread(thread, replies, nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -22,7 +22,7 @@
|
|||||||
"loading",
|
"loading",
|
||||||
"fade",
|
"fade",
|
||||||
"mobile",
|
"mobile",
|
||||||
"extension"
|
"embedded"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"main": "main"
|
"main": "main"
|
||||||
|
BIN
images/elements/extension-screenshot.png
Normal file
BIN
images/elements/extension-screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
@ -29,7 +30,14 @@ func main() {
|
|||||||
color.Yellow("Generating user avatars")
|
color.Yellow("Generating user avatars")
|
||||||
|
|
||||||
// Switch to main directory
|
// Switch to main directory
|
||||||
os.Chdir("../../")
|
exe, err := os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := path.Dir(exe)
|
||||||
|
os.Chdir(path.Join(root, "../../"))
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
avatarLog.AddOutput(log.File("logs/avatar.log"))
|
avatarLog.AddOutput(log.File("logs/avatar.log"))
|
||||||
|
123
jobs/main.go
Normal file
123
jobs/main.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aerogo/log"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
var colorPool = []*color.Color{
|
||||||
|
color.New(color.FgHiGreen),
|
||||||
|
color.New(color.FgHiYellow),
|
||||||
|
color.New(color.FgHiBlue),
|
||||||
|
color.New(color.FgHiMagenta),
|
||||||
|
color.New(color.FgHiCyan),
|
||||||
|
color.New(color.FgGreen),
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs = map[string]time.Duration{
|
||||||
|
"active-users": 1 * time.Minute,
|
||||||
|
"avatars": 1 * time.Hour,
|
||||||
|
"sync-anime": 10 * time.Hour,
|
||||||
|
"popular-anime": 11 * time.Hour,
|
||||||
|
"airing-anime": 12 * time.Hour,
|
||||||
|
"search-index": 13 * time.Hour,
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Start all jobs defined in the map above
|
||||||
|
startJobs()
|
||||||
|
|
||||||
|
// Wait for program termination
|
||||||
|
wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startJobs() {
|
||||||
|
// Get the directory the executable is in
|
||||||
|
exe, err := os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := path.Dir(exe)
|
||||||
|
|
||||||
|
// Log paths
|
||||||
|
logsPath := path.Join(root, "../", "logs")
|
||||||
|
jobLogsPath := path.Join(root, "../", "logs", "jobs")
|
||||||
|
os.Mkdir(jobLogsPath, 0777)
|
||||||
|
|
||||||
|
// Scheduler log
|
||||||
|
mainLog := log.New()
|
||||||
|
mainLog.AddOutput(os.Stdout)
|
||||||
|
mainLog.AddOutput(log.File(path.Join(logsPath, "scheduler.log")))
|
||||||
|
schedulerLog := mainLog
|
||||||
|
|
||||||
|
// Color index
|
||||||
|
colorIndex := 0
|
||||||
|
|
||||||
|
// Start each job
|
||||||
|
for job, interval := range jobs {
|
||||||
|
jobName := job
|
||||||
|
jobInterval := interval
|
||||||
|
executable := path.Join(root, jobName, jobName)
|
||||||
|
jobColor := colorPool[colorIndex].SprintFunc()
|
||||||
|
|
||||||
|
jobLog := log.New()
|
||||||
|
jobLog.AddOutput(log.File(path.Join(jobLogsPath, jobName+".log")))
|
||||||
|
|
||||||
|
fmt.Printf("Registered job %s for execution every %v\n", jobColor(jobName), interval)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(jobInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Wait for the given interval first
|
||||||
|
<-ticker.C
|
||||||
|
|
||||||
|
// Now start
|
||||||
|
schedulerLog.Info("Starting " + jobColor(jobName))
|
||||||
|
|
||||||
|
cmd := exec.Command(executable)
|
||||||
|
cmd.Stdout = jobLog
|
||||||
|
cmd.Stderr = jobLog
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
schedulerLog.Error("Error starting job", jobColor(jobName), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
schedulerLog.Error("Job exited with error", jobColor(jobName), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulerLog.Info("Finished " + jobColor(jobName))
|
||||||
|
jobLog.Info("--------------------------------------------------------------------------------")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
colorIndex = (colorIndex + 1) % len(colorPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finished job registration
|
||||||
|
println("--------------------------------------------------------------------------------")
|
||||||
|
}
|
||||||
|
|
||||||
|
func wait() {
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-stop
|
||||||
|
}
|
@ -4,6 +4,7 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, conte
|
|||||||
title= app.Config.Title
|
title= app.Config.Title
|
||||||
meta(name="viewport", content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes")
|
meta(name="viewport", content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes")
|
||||||
meta(name="theme-color", content=app.Config.Manifest.ThemeColor)
|
meta(name="theme-color", content=app.Config.Manifest.ThemeColor)
|
||||||
|
link(rel="chrome-webstore-item", href="https://chrome.google.com/webstore/detail/hajchfikckiofgilinkpifobdbiajfch")
|
||||||
link(rel="manifest", href="/manifest.json")
|
link(rel="manifest", href="/manifest.json")
|
||||||
body
|
body
|
||||||
#container(class=utils.GetContainerClass(ctx))
|
#container(class=utils.GetContainerClass(ctx))
|
||||||
|
4
main.go
4
main.go
@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/aerogo/api"
|
"github.com/aerogo/api"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
"github.com/animenotifier/notify.moe/auth"
|
"github.com/animenotifier/notify.moe/auth"
|
||||||
"github.com/animenotifier/notify.moe/components"
|
"github.com/animenotifier/notify.moe/components/css"
|
||||||
"github.com/animenotifier/notify.moe/layout"
|
"github.com/animenotifier/notify.moe/layout"
|
||||||
"github.com/animenotifier/notify.moe/middleware"
|
"github.com/animenotifier/notify.moe/middleware"
|
||||||
"github.com/animenotifier/notify.moe/pages/admin"
|
"github.com/animenotifier/notify.moe/pages/admin"
|
||||||
@ -41,7 +41,7 @@ func configure(app *aero.Application) *aero.Application {
|
|||||||
app.Security.Load("security/fullchain.pem", "security/privkey.pem")
|
app.Security.Load("security/fullchain.pem", "security/privkey.pem")
|
||||||
|
|
||||||
// CSS
|
// CSS
|
||||||
app.SetStyle(components.CSS())
|
app.SetStyle(css.Bundle())
|
||||||
|
|
||||||
// Sessions
|
// Sessions
|
||||||
app.Sessions.Duration = 3600 * 24
|
app.Sessions.Duration = 3600 * 24
|
||||||
|
13
main_test.go
13
main_test.go
@ -9,10 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestRoutes(t *testing.T) {
|
func TestRoutes(t *testing.T) {
|
||||||
expectedStatus := http.StatusOK
|
|
||||||
|
|
||||||
app := configure(aero.New())
|
app := configure(aero.New())
|
||||||
request, err := http.NewRequest("GET", "/", nil)
|
|
||||||
|
for _, examples := range tests {
|
||||||
|
for _, example := range examples {
|
||||||
|
request, err := http.NewRequest("GET", example, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -21,7 +22,9 @@ func TestRoutes(t *testing.T) {
|
|||||||
responseRecorder := httptest.NewRecorder()
|
responseRecorder := httptest.NewRecorder()
|
||||||
app.Handler().ServeHTTP(responseRecorder, request)
|
app.Handler().ServeHTTP(responseRecorder, request)
|
||||||
|
|
||||||
if status := responseRecorder.Code; status != expectedStatus {
|
if status := responseRecorder.Code; status != http.StatusOK {
|
||||||
t.Errorf("Wrong status code: %v instead of %v", status, expectedStatus)
|
t.Errorf("%s | Wrong status code | %v instead of %v", example, status, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,6 @@ component ForumTags
|
|||||||
ForumTag("Bugs", "bug", "list")
|
ForumTag("Bugs", "bug", "list")
|
||||||
|
|
||||||
component ForumTag(title string, category string, icon string)
|
component ForumTag(title string, category string, icon string)
|
||||||
a.button.forum-tag.ajax(href=strings.TrimSuffix("/forum/" + category, "/"))
|
a.button.forum-tag.action(href=strings.TrimSuffix("/forum/" + category, "/"), data-action="diff", data-trigger="click")
|
||||||
Icon(arn.GetForumIcon(category))
|
Icon(arn.GetForumIcon(category))
|
||||||
span.forum-tag-text= title
|
span.forum-tag-text= title
|
@ -8,10 +8,10 @@ component InputTextArea(id string, value string, label string, placeholder strin
|
|||||||
label(for=id)= label + ":"
|
label(for=id)= label + ":"
|
||||||
textarea.widget-element.action(id=id, placeholder=placeholder, title=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)
|
component InputNumber(id string, value float64, label string, placeholder string, min string, max string, step string)
|
||||||
.widget-input
|
.widget-input
|
||||||
label(for=id)= label + ":"
|
label(for=id)= label + ":"
|
||||||
input.widget-element.action(id=id, type="number", value=value, min=min, max=max, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")
|
input.widget-element.action(id=id, type="number", value=value, min=min, max=max, step=step, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")
|
||||||
|
|
||||||
component InputSelection(id string, value string, label string, placeholder string)
|
component InputSelection(id string, value string, label string, placeholder string)
|
||||||
.widget-input
|
.widget-input
|
||||||
|
@ -20,10 +20,13 @@ component LoggedOutMenu
|
|||||||
|
|
||||||
component LoggedInMenu(user *arn.User)
|
component LoggedInMenu(user *arn.User)
|
||||||
nav#navigation.logged-in
|
nav#navigation.logged-in
|
||||||
NavigationButton("Dash", "/", "inbox")
|
.extension-navigation
|
||||||
NavigationButton("Anime", "/anime", "television")
|
NavigationButton("Watching list", "/extension/embed", "home")
|
||||||
|
|
||||||
|
NavigationButton("Dash", "/", "dashboard")
|
||||||
NavigationButton("Profile", "/+", "user")
|
NavigationButton("Profile", "/+", "user")
|
||||||
NavigationButton("Forum", "/forum", "comment")
|
NavigationButton("Forum", "/forum", "comment")
|
||||||
|
NavigationButton("Anime", "/anime", "television")
|
||||||
|
|
||||||
FuzzySearch
|
FuzzySearch
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ component Postable(post arn.Postable, highlightAuthorID string)
|
|||||||
//- a.user.post-recipient(href="/+" + post.recipient.nick, title=post.recipient.nick)
|
//- a.user.post-recipient(href="/+" + post.recipient.nick, title=post.recipient.nick)
|
||||||
//- img.user-image(src=post.recipient.avatar ? (post.recipient.avatar + "?s=100&r=x&d=mm") : "/images/elements/no-gravatar.svg", alt=post.recipient.nick)
|
//- img.user-image(src=post.recipient.avatar ? (post.recipient.avatar + "?s=100&r=x&d=mm") : "/images/elements/no-gravatar.svg", alt=post.recipient.nick)
|
||||||
.post-content
|
.post-content
|
||||||
div(id="render-" + post.ID())!= aero.Markdown(post.Text())
|
div(id="render-" + post.ID())!= post.HTML()
|
||||||
|
|
||||||
//- if user && user.ID === post.authorId
|
//- if user && user.ID === post.authorId
|
||||||
//- textarea.post-input.hidden(id="source-" + post.ID)= post.text
|
//- textarea.post-input.hidden(id="source-" + post.ID)= post.text
|
||||||
|
@ -41,9 +41,9 @@ component Anime(anime *arn.Anime, user *arn.User)
|
|||||||
.anime-rating-category(title=toString(anime.Rating.Visuals / 10))
|
.anime-rating-category(title=toString(anime.Rating.Visuals / 10))
|
||||||
.anime-rating-category-name Visuals
|
.anime-rating-category-name Visuals
|
||||||
Rating(anime.Rating.Visuals)
|
Rating(anime.Rating.Visuals)
|
||||||
.anime-rating-category(title=toString(anime.Rating.Music / 10))
|
.anime-rating-category(title=toString(anime.Rating.Soundtrack / 10))
|
||||||
.anime-rating-category-name Music
|
.anime-rating-category-name Soundtrack
|
||||||
Rating(anime.Rating.Music)
|
Rating(anime.Rating.Soundtrack)
|
||||||
|
|
||||||
if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].VideoID != ""
|
if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].VideoID != ""
|
||||||
h3.anime-section-name Video
|
h3.anime-section-name Video
|
||||||
|
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/aero"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
"github.com/animenotifier/notify.moe/components"
|
"github.com/animenotifier/notify.moe/components"
|
||||||
|
"github.com/animenotifier/notify.moe/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get anime list.
|
// Get anime list.
|
||||||
func Get(ctx *aero.Context) string {
|
func Get(ctx *aero.Context) string {
|
||||||
nick := ctx.Get("nick")
|
nick := ctx.Get("nick")
|
||||||
|
user := utils.GetUser(ctx)
|
||||||
viewUser, err := arn.GetUserByNick(nick)
|
viewUser, err := arn.GetUserByNick(nick)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -25,8 +27,8 @@ func Get(ctx *aero.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(animeList.Items, func(i, j int) bool {
|
sort.Slice(animeList.Items, func(i, j int) bool {
|
||||||
return animeList.Items[i].FinalRating() < animeList.Items[j].FinalRating()
|
return animeList.Items[i].FinalRating() > animeList.Items[j].FinalRating()
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx.HTML(components.AnimeList(animeList))
|
return ctx.HTML(components.AnimeList(animeList, user))
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,26 @@
|
|||||||
component AnimeList(animeList *arn.AnimeList)
|
component AnimeList(animeList *arn.AnimeList, user *arn.User)
|
||||||
table.anime-list
|
table.anime-list
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
th.anime-list-item-name Anime
|
th.anime-list-item-name Anime
|
||||||
th.anime-list-item-episodes Progress
|
th.anime-list-item-episodes Episodes
|
||||||
th.anime-list-item-rating Rating
|
th.anime-list-item-rating Rating
|
||||||
|
if user != nil
|
||||||
|
th.anime-list-item-actions Actions
|
||||||
tbody
|
tbody
|
||||||
each item in animeList.Items
|
each item in animeList.Items
|
||||||
tr.anime-list-item.mountable(title=item.Notes)
|
tr.anime-list-item.mountable(title=item.Notes)
|
||||||
td.anime-list-item-name
|
td.anime-list-item-name
|
||||||
a.ajax(href=item.Anime().Link())= item.Anime().Title.Canonical
|
a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical
|
||||||
td.anime-list-item-episodes
|
td.anime-list-item-episodes
|
||||||
span.anime-list-item-episodes-watched= item.Episodes
|
.anime-list-item-episodes-watched= item.Episodes
|
||||||
span.anime-list-item-episodes-separator /
|
.anime-list-item-episodes-separator /
|
||||||
span.anime-list-item-episodes-max= item.Anime().EpisodeCountString()
|
.anime-list-item-episodes-max= item.Anime().EpisodeCountString()
|
||||||
|
//- .anime-list-item-episodes-edit
|
||||||
|
//- a.ajax(href=, title="Edit anime")
|
||||||
|
//- RawIcon("pencil")
|
||||||
td.anime-list-item-rating= item.FinalRating()
|
td.anime-list-item-rating= item.FinalRating()
|
||||||
|
if user != nil
|
||||||
|
td.anime-list-item-actions
|
||||||
|
a(href=arn.Nyaa.GetLink(item.Anime()), title="Search on Nyaa", target="_blank", rel="noopener")
|
||||||
|
RawIcon("download")
|
@ -5,16 +5,52 @@
|
|||||||
tr
|
tr
|
||||||
horizontal
|
horizontal
|
||||||
|
|
||||||
|
thead
|
||||||
|
display none
|
||||||
|
|
||||||
.anime-list-item-name
|
.anime-list-item-name
|
||||||
flex 0.8
|
flex 1
|
||||||
white-space nowrap
|
white-space nowrap
|
||||||
text-overflow ellipsis
|
text-overflow ellipsis
|
||||||
overflow hidden
|
overflow hidden
|
||||||
|
|
||||||
.anime-list-item-episodes
|
.anime-list-item-episodes
|
||||||
flex 0.1
|
horizontal
|
||||||
text-align center
|
justify-content flex-end
|
||||||
|
text-align right
|
||||||
|
white-space nowrap
|
||||||
|
flex-basis 120px
|
||||||
|
|
||||||
|
.anime-list-item-episodes-watched
|
||||||
|
flex 0.4
|
||||||
|
|
||||||
|
.anime-list-item-episodes-separator
|
||||||
|
flex 0.2
|
||||||
|
opacity 0.5
|
||||||
|
|
||||||
|
.anime-list-item-episodes-max
|
||||||
|
flex 0.4
|
||||||
|
opacity 0.5
|
||||||
|
|
||||||
|
// .anime-list-item-episodes-edit
|
||||||
|
// flex 0.5
|
||||||
|
|
||||||
|
// // Beautify icon alignment
|
||||||
|
// .raw-icon
|
||||||
|
// margin-bottom -2px
|
||||||
|
|
||||||
.anime-list-item-rating
|
.anime-list-item-rating
|
||||||
flex 0.1
|
flex-basis 100px
|
||||||
text-align center
|
text-align center
|
||||||
|
|
||||||
|
.anime-list-item-actions
|
||||||
|
flex-basis 40px
|
||||||
|
text-align right
|
||||||
|
|
||||||
|
// Beautify icon alignment
|
||||||
|
.raw-icon
|
||||||
|
margin-bottom -4px
|
||||||
|
|
||||||
|
< 1100px
|
||||||
|
.anime-list-item-rating
|
||||||
|
display none
|
@ -3,8 +3,15 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.
|
|||||||
.widget.anime-list-item-view(data-api="/api/animelist/" + viewUser.ID + "/update/" + anime.ID)
|
.widget.anime-list-item-view(data-api="/api/animelist/" + viewUser.ID + "/update/" + anime.ID)
|
||||||
h2= anime.Title.Canonical
|
h2= anime.Title.Canonical
|
||||||
|
|
||||||
InputNumber("Episodes", item.Episodes, "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount))
|
InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1")
|
||||||
InputNumber("RewatchCount", item.RewatchCount, "Rewatched", "How often you rewatched this anime", "0", "100")
|
|
||||||
|
.anime-list-item-rating-edit
|
||||||
|
InputNumber("Rating.Overall", item.Rating.Overall, "Overall", "Overall rating on a scale of 0 to 10", "0", "10", "0.1")
|
||||||
|
InputNumber("Rating.Story", item.Rating.Story, "Story", "Story rating on a scale of 0 to 10", "0", "10", "0.1")
|
||||||
|
InputNumber("Rating.Visuals", item.Rating.Visuals, "Visuals", "Visuals rating on a scale of 0 to 10", "0", "10", "0.1")
|
||||||
|
InputNumber("Rating.Soundtrack", item.Rating.Soundtrack, "Soundtrack", "Soundtrack rating on a scale of 0 to 10", "0", "10", "0.1")
|
||||||
|
|
||||||
|
InputNumber("RewatchCount", float64(item.RewatchCount), "Rewatched", "How often you rewatched this anime", "0", "100", "1")
|
||||||
|
|
||||||
InputTextArea("Notes", item.Notes, "Notes", "Your notes")
|
InputTextArea("Notes", item.Notes, "Notes", "Your notes")
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
.anime-list-item-view
|
|
||||||
textarea
|
|
||||||
height 10rem
|
|
||||||
|
|
||||||
.anime-list-item-view-image
|
.anime-list-item-view-image
|
||||||
max-width 55px
|
max-width 55px
|
||||||
margin-bottom 1rem
|
margin-bottom 1rem
|
||||||
|
|
||||||
|
.anime-list-item-rating-edit
|
||||||
|
horizontal-wrap
|
||||||
|
justify-content space-between
|
||||||
|
width 100%
|
||||||
|
.widget-input
|
||||||
|
max-width 120px
|
@ -19,7 +19,7 @@ func Get(ctx *aero.Context) string {
|
|||||||
return frontpage.Get(ctx)
|
return frontpage.Get(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err := arn.GetPosts()
|
posts, err := arn.AllPostsSlice()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Error(500, "Error fetching posts", err)
|
return ctx.Error(500, "Error fetching posts", err)
|
||||||
@ -32,6 +32,7 @@ func Get(ctx *aero.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
followIDList := user.Following
|
followIDList := user.Following
|
||||||
|
var followingList []*arn.User
|
||||||
|
|
||||||
if len(followIDList) > maxFollowing {
|
if len(followIDList) > maxFollowing {
|
||||||
followIDList = followIDList[:maxFollowing]
|
followIDList = followIDList[:maxFollowing]
|
||||||
@ -43,7 +44,7 @@ func Get(ctx *aero.Context) string {
|
|||||||
return ctx.Error(500, "Error fetching followed users", err)
|
return ctx.Error(500, "Error fetching followed users", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
followingList := userList.([]*arn.User)
|
followingList = userList.([]*arn.User)
|
||||||
|
|
||||||
return ctx.HTML(components.Dashboard(posts, followingList))
|
return ctx.HTML(components.Dashboard(posts, followingList))
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ func Get(ctx *aero.Context) string {
|
|||||||
user := utils.GetUser(ctx)
|
user := utils.GetUser(ctx)
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ctx.Error(http.StatusUnauthorized, "Not logged in", nil)
|
return utils.AllowEmbed(ctx, ctx.HTML(components.Login()))
|
||||||
}
|
}
|
||||||
|
|
||||||
animeList := user.AnimeList()
|
animeList := user.AnimeList()
|
||||||
@ -24,8 +24,8 @@ func Get(ctx *aero.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(animeList.Items, func(i, j int) bool {
|
sort.Slice(animeList.Items, func(i, j int) bool {
|
||||||
return animeList.Items[i].FinalRating() < animeList.Items[j].FinalRating()
|
return animeList.Items[i].FinalRating() > animeList.Items[j].FinalRating()
|
||||||
})
|
})
|
||||||
|
|
||||||
return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(animeList)))
|
return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(animeList, user)))
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,18 @@
|
|||||||
|
|
||||||
.forum-tag
|
.forum-tag
|
||||||
color text-color !important
|
color text-color !important
|
||||||
:hover
|
|
||||||
color white !important
|
:hover,
|
||||||
&.active
|
&.active
|
||||||
color white !important
|
color white !important
|
||||||
background-color link-hover-color
|
background-color forum-tag-hover-color !important
|
||||||
|
|
||||||
|
// color text-color !important
|
||||||
|
// :hover
|
||||||
|
// color white !important
|
||||||
|
// &.active
|
||||||
|
// color white !important
|
||||||
|
// background-color link-hover-color
|
||||||
|
|
||||||
< 920px
|
< 920px
|
||||||
.forum-tag
|
.forum-tag
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
component FrontPage
|
component FrontPage
|
||||||
p Anime Notifier 4.0 is currently under construction.
|
.frontpage
|
||||||
p
|
h2 notify.moe
|
||||||
a(href="https://paypal.me/blitzprog", target="_blank", rel="noopener") Support the development
|
|
||||||
|
|
||||||
p
|
img.action.screenshot(src="/images/elements/extension-screenshot.png", alt="Screenshot of the browser extension", title="Click to install the Chrome Extension", data-action="installExtension", data-trigger="click")
|
||||||
|
|
||||||
|
Login
|
||||||
|
|
||||||
|
.footer
|
||||||
|
a(href="https://paypal.me/blitzprog", target="_blank", rel="noopener") Support the development
|
||||||
|
span |
|
||||||
a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub
|
a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub
|
@ -1,11 +1,22 @@
|
|||||||
.login-buttons
|
.frontpage
|
||||||
horizontal-wrap
|
vertical
|
||||||
width 100%
|
align-items center
|
||||||
justify-content center
|
|
||||||
|
|
||||||
.login-button
|
h2
|
||||||
//
|
font-size 2.5rem
|
||||||
|
font-weight normal
|
||||||
|
letter-spacing 3px
|
||||||
|
text-transform uppercase
|
||||||
|
|
||||||
.login-button-image
|
.footer
|
||||||
max-width 236px
|
text-align center
|
||||||
max-height 44px
|
margin-top content-padding
|
||||||
|
|
||||||
|
.screenshot
|
||||||
|
max-width 100%
|
||||||
|
border-radius 3px
|
||||||
|
box-shadow shadow-medium
|
||||||
|
margin-bottom 2rem
|
||||||
|
|
||||||
|
:hover
|
||||||
|
cursor pointer
|
11
pages/login/login.scarlet
Normal file
11
pages/login/login.scarlet
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.login-buttons
|
||||||
|
horizontal-wrap
|
||||||
|
width 100%
|
||||||
|
justify-content center
|
||||||
|
|
||||||
|
.login-button
|
||||||
|
//
|
||||||
|
|
||||||
|
.login-button-image
|
||||||
|
max-width 236px
|
||||||
|
max-height 44px
|
@ -16,5 +16,5 @@ func Get(ctx *aero.Context) string {
|
|||||||
return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err)
|
return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.HTML(components.AnimeGrid(animeList))
|
return ctx.HTML(components.PopularAnime(animeList))
|
||||||
}
|
}
|
||||||
|
6
pages/popularanime/popular.pixy
Normal file
6
pages/popularanime/popular.pixy
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
component PopularAnime(animeList []*arn.Anime)
|
||||||
|
h2 Top 3
|
||||||
|
AnimeGrid(animeList[:3])
|
||||||
|
|
||||||
|
h2 Popular
|
||||||
|
AnimeGrid(animeList[3:])
|
@ -79,7 +79,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList,
|
|||||||
.post-author
|
.post-author
|
||||||
Avatar(post.Author())
|
Avatar(post.Author())
|
||||||
.post-content
|
.post-content
|
||||||
div!= aero.Markdown(post.Text)
|
div!= post.HTML()
|
||||||
.post-toolbar.active
|
.post-toolbar.active
|
||||||
.spacer
|
.spacer
|
||||||
.post-likes= len(post.Likes)
|
.post-likes= len(post.Likes)
|
||||||
|
@ -80,6 +80,10 @@ profile-boot-duration = 2s
|
|||||||
padding-left calc(content-padding * 2)
|
padding-left calc(content-padding * 2)
|
||||||
max-width 900px
|
max-width 900px
|
||||||
|
|
||||||
|
.website
|
||||||
|
a
|
||||||
|
color white
|
||||||
|
|
||||||
#nick
|
#nick
|
||||||
margin-bottom 1rem
|
margin-bottom 1rem
|
||||||
|
|
||||||
|
@ -1,29 +1,39 @@
|
|||||||
package threads
|
package threads
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/aero"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
"github.com/animenotifier/notify.moe/components"
|
"github.com/animenotifier/notify.moe/components"
|
||||||
|
"github.com/animenotifier/notify.moe/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get thread.
|
// Get thread.
|
||||||
func Get(ctx *aero.Context) string {
|
func Get(ctx *aero.Context) string {
|
||||||
id := ctx.Get("id")
|
id := ctx.Get("id")
|
||||||
thread, err := arn.GetThread(id)
|
thread, err := arn.GetThread(id)
|
||||||
|
user := utils.GetUser(ctx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.Error(404, "Thread not found", err)
|
return ctx.Error(404, "Thread not found", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
replies, filterErr := arn.FilterPosts(func(post *arn.Post) bool {
|
replies, filterErr := arn.FilterPosts(func(post *arn.Post) bool {
|
||||||
|
post.Text = strings.Replace(post.Text, "http://", "https://", -1)
|
||||||
return post.ThreadID == thread.ID
|
return post.ThreadID == thread.ID
|
||||||
})
|
})
|
||||||
|
|
||||||
arn.SortPostsLatestLast(replies)
|
arn.SortPostsLatestLast(replies)
|
||||||
|
|
||||||
|
// Benchmark
|
||||||
|
// for i := 0; i < 7; i++ {
|
||||||
|
// replies = append(replies, replies...)
|
||||||
|
// }
|
||||||
|
|
||||||
if filterErr != nil {
|
if filterErr != nil {
|
||||||
return ctx.Error(500, "Error fetching thread replies", err)
|
return ctx.Error(500, "Error fetching thread replies", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.HTML(components.Thread(thread, replies))
|
return ctx.HTML(components.Thread(thread, replies, user))
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
component Thread(thread *arn.Thread, posts []*arn.Post)
|
component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User)
|
||||||
h2.thread-title= thread.Title
|
h2.thread-title= thread.Title
|
||||||
|
|
||||||
.thread
|
.thread
|
||||||
@ -7,3 +7,12 @@ component Thread(thread *arn.Thread, posts []*arn.Post)
|
|||||||
|
|
||||||
each post in posts
|
each post in posts
|
||||||
Postable(post.ToPostable(), thread.Author().ID)
|
Postable(post.ToPostable(), thread.Author().ID)
|
||||||
|
|
||||||
|
// Reply
|
||||||
|
if user != nil
|
||||||
|
.post.mountable
|
||||||
|
.post-author
|
||||||
|
Avatar(user)
|
||||||
|
|
||||||
|
.post-content
|
||||||
|
textarea(id="new-reply", placeholder="Reply...")
|
@ -1,46 +0,0 @@
|
|||||||
html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video, main
|
|
||||||
outline 0
|
|
||||||
border 0
|
|
||||||
font-size 100%
|
|
||||||
font inherit
|
|
||||||
vertical-align baseline
|
|
||||||
background transparent
|
|
||||||
|
|
||||||
html
|
|
||||||
box-sizing border-box
|
|
||||||
|
|
||||||
textarea
|
|
||||||
resize vertical
|
|
||||||
|
|
||||||
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section
|
|
||||||
display block
|
|
||||||
|
|
||||||
body
|
|
||||||
line-height 1
|
|
||||||
-webkit-font-smoothing antialiased
|
|
||||||
-moz-osx-font-smoothing grayscale
|
|
||||||
|
|
||||||
ol, ul
|
|
||||||
list-style none
|
|
||||||
|
|
||||||
blockquote, q
|
|
||||||
quotes none
|
|
||||||
|
|
||||||
blockquote:before, blockquote:after, q:before, q:after
|
|
||||||
content ''
|
|
||||||
content none
|
|
||||||
|
|
||||||
table
|
|
||||||
border-collapse collapse
|
|
||||||
border-spacing 0
|
|
||||||
|
|
||||||
audio, canvas, img, video, input, select
|
|
||||||
vertical-align middle
|
|
||||||
|
|
||||||
:focus
|
|
||||||
outline 0
|
|
||||||
|
|
||||||
*
|
|
||||||
margin 0
|
|
||||||
padding 0
|
|
||||||
box-sizing inherit
|
|
@ -53,8 +53,8 @@ export class AnimeNotifier {
|
|||||||
this.visibilityObserver.disconnect()
|
this.visibilityObserver.disconnect()
|
||||||
|
|
||||||
// Update each of these asynchronously
|
// Update each of these asynchronously
|
||||||
Promise.resolve().then(() => this.updateMountables())
|
Promise.resolve().then(() => this.mountMountables())
|
||||||
Promise.resolve().then(() => this.updateActions())
|
Promise.resolve().then(() => this.assignActions())
|
||||||
Promise.resolve().then(() => this.lazyLoadImages())
|
Promise.resolve().then(() => this.lazyLoadImages())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ export class AnimeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActions() {
|
assignActions() {
|
||||||
for(let element of findAll("action")) {
|
for(let element of findAll("action")) {
|
||||||
if(element["action assigned"]) {
|
if(element["action assigned"]) {
|
||||||
continue
|
continue
|
||||||
@ -85,6 +85,9 @@ export class AnimeNotifier {
|
|||||||
|
|
||||||
element.addEventListener(element.dataset.trigger, e => {
|
element.addEventListener(element.dataset.trigger, e => {
|
||||||
actions[actionName](this, element, e)
|
actions[actionName](this, element, e)
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use "action assigned" flag instead of removing the class.
|
// Use "action assigned" flag instead of removing the class.
|
||||||
@ -121,21 +124,31 @@ export class AnimeNotifier {
|
|||||||
this.visibilityObserver.observe(img)
|
this.visibilityObserver.observe(img)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMountables() {
|
mountMountables() {
|
||||||
|
this.modifyDelayed("mountable", element => element.classList.add("mounted"))
|
||||||
|
}
|
||||||
|
|
||||||
|
unmountMountables() {
|
||||||
|
for(let element of findAll("mountable")) {
|
||||||
|
element.classList.remove("mounted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyDelayed(className: string, func: (element: HTMLElement) => void) {
|
||||||
const delay = 20
|
const delay = 20
|
||||||
const maxDelay = 1000
|
const maxDelay = 500
|
||||||
|
|
||||||
let time = 0
|
let time = 0
|
||||||
|
|
||||||
for(let element of findAll("mountable")) {
|
for(let element of findAll(className)) {
|
||||||
setTimeout(() => {
|
|
||||||
window.requestAnimationFrame(() => element.classList.add("mounted"))
|
|
||||||
}, time)
|
|
||||||
|
|
||||||
time += delay
|
time += delay
|
||||||
|
|
||||||
if(time > maxDelay) {
|
if(time > maxDelay) {
|
||||||
time = maxDelay
|
func(element)
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.requestAnimationFrame(() => func(element))
|
||||||
|
}, time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Diff } from "./Diff"
|
||||||
|
|
||||||
class LoadOptions {
|
class LoadOptions {
|
||||||
addToHistory?: boolean
|
addToHistory?: boolean
|
||||||
forceReload?: boolean
|
forceReload?: boolean
|
||||||
@ -57,6 +59,10 @@ export class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load(url: string, options?: LoadOptions) {
|
load(url: string, options?: LoadOptions) {
|
||||||
|
// Start sending a network request
|
||||||
|
let request = this.get("/_" + url).catch(error => error)
|
||||||
|
|
||||||
|
// Parse options
|
||||||
if(!options) {
|
if(!options) {
|
||||||
options = new LoadOptions()
|
options = new LoadOptions()
|
||||||
}
|
}
|
||||||
@ -65,10 +71,12 @@ export class Application {
|
|||||||
options.addToHistory = true
|
options.addToHistory = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set current path
|
||||||
this.currentPath = url
|
this.currentPath = url
|
||||||
|
|
||||||
// Start sending a network request
|
// Add to browser history
|
||||||
let request = this.get("/_" + url).catch(error => error)
|
if(options.addToHistory)
|
||||||
|
history.pushState(url, null, url)
|
||||||
|
|
||||||
let onTransitionEnd = e => {
|
let onTransitionEnd = e => {
|
||||||
// Ignore transitions of child elements.
|
// Ignore transitions of child elements.
|
||||||
@ -82,13 +90,8 @@ export class Application {
|
|||||||
|
|
||||||
// Wait for the network request to end.
|
// Wait for the network request to end.
|
||||||
request.then(html => {
|
request.then(html => {
|
||||||
// Add to browser history
|
|
||||||
if(options.addToHistory)
|
|
||||||
history.pushState(url, null, url)
|
|
||||||
|
|
||||||
// Set content
|
// Set content
|
||||||
this.setContent(html)
|
this.setContent(html, false)
|
||||||
this.scrollToTop()
|
|
||||||
|
|
||||||
// Fade animations
|
// Fade animations
|
||||||
this.content.classList.remove(this.fadeOutClass)
|
this.content.classList.remove(this.fadeOutClass)
|
||||||
@ -108,11 +111,16 @@ export class Application {
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent(html: string) {
|
setContent(html: string, diff: boolean) {
|
||||||
// Diff.innerHTML(this.content, html)
|
if(diff) {
|
||||||
|
Diff.innerHTML(this.content, html)
|
||||||
|
} else {
|
||||||
this.content.innerHTML = html
|
this.content.innerHTML = html
|
||||||
|
}
|
||||||
|
|
||||||
this.ajaxify(this.content)
|
this.ajaxify(this.content)
|
||||||
this.markActiveLinks(this.content)
|
this.markActiveLinks(this.content)
|
||||||
|
this.scrollToTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
markActiveLinks(element?: HTMLElement) {
|
markActiveLinks(element?: HTMLElement) {
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
export class Diff {
|
export class Diff {
|
||||||
static childNodes(aRoot: HTMLElement, bRoot: HTMLElement) {
|
static childNodes(aRoot: Node, bRoot: Node) {
|
||||||
let aChild = [...aRoot.childNodes]
|
let aChild = [...aRoot.childNodes]
|
||||||
let bChild = [...bRoot.childNodes]
|
let bChild = [...bRoot.childNodes]
|
||||||
let numNodes = Math.max(aChild.length, bChild.length)
|
let numNodes = Math.max(aChild.length, bChild.length)
|
||||||
|
|
||||||
for(let i = 0; i < numNodes; i++) {
|
for(let i = 0; i < numNodes; i++) {
|
||||||
let a = aChild[i] as HTMLElement
|
let a = aChild[i]
|
||||||
|
|
||||||
if(i >= bChild.length) {
|
if(i >= bChild.length) {
|
||||||
aRoot.removeChild(a)
|
aRoot.removeChild(a)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let b = bChild[i] as HTMLElement
|
let b = bChild[i]
|
||||||
|
|
||||||
if(i >= aChild.length) {
|
if(i >= aChild.length) {
|
||||||
aRoot.appendChild(b)
|
aRoot.appendChild(b)
|
||||||
@ -24,38 +24,46 @@ export class Diff {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(a.nodeType === Node.TEXT_NODE) {
|
||||||
|
a.textContent = b.textContent
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if(a.nodeType === Node.ELEMENT_NODE) {
|
if(a.nodeType === Node.ELEMENT_NODE) {
|
||||||
if(a.tagName === "IFRAME") {
|
let elemA = a as HTMLElement
|
||||||
|
let elemB = b as HTMLElement
|
||||||
|
|
||||||
|
if(elemA.tagName === "IFRAME") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let removeAttributes: Attr[] = []
|
let removeAttributes: Attr[] = []
|
||||||
|
|
||||||
for(let x = 0; x < a.attributes.length; x++) {
|
for(let x = 0; x < elemA.attributes.length; x++) {
|
||||||
let attrib = a.attributes[x]
|
let attrib = elemA.attributes[x]
|
||||||
|
|
||||||
if(attrib.specified) {
|
if(attrib.specified) {
|
||||||
if(!b.hasAttribute(attrib.name)) {
|
if(!elemB.hasAttribute(attrib.name)) {
|
||||||
removeAttributes.push(attrib)
|
removeAttributes.push(attrib)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let attr of removeAttributes) {
|
for(let attr of removeAttributes) {
|
||||||
a.removeAttributeNode(attr)
|
elemA.removeAttributeNode(attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let x = 0; x < b.attributes.length; x++) {
|
for(let x = 0; x < elemB.attributes.length; x++) {
|
||||||
let attrib = b.attributes[x]
|
let attrib = elemB.attributes[x]
|
||||||
|
|
||||||
if(attrib.specified) {
|
if(attrib.specified) {
|
||||||
a.setAttribute(attrib.name, b.getAttribute(attrib.name))
|
elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: Apply state of input elements
|
// Special case: Apply state of input elements
|
||||||
if(a !== document.activeElement && a instanceof HTMLInputElement && b instanceof HTMLInputElement) {
|
if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) {
|
||||||
a.value = b.value
|
elemA.value = elemB.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Application } from "./Application"
|
import { Application } from "./Application"
|
||||||
import { AnimeNotifier } from "./AnimeNotifier"
|
import { AnimeNotifier } from "./AnimeNotifier"
|
||||||
import { Diff } from "./Diff"
|
import { Diff } from "./Diff"
|
||||||
|
import { delay, findAll } from "./utils"
|
||||||
|
|
||||||
// Save new data from an input field
|
// Save new data from an input field
|
||||||
export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) {
|
export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) {
|
||||||
@ -10,7 +11,11 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE
|
|||||||
let value = input.value
|
let value = input.value
|
||||||
|
|
||||||
if(input.type === "number") {
|
if(input.type === "number") {
|
||||||
|
if(input.getAttribute("step") === "1") {
|
||||||
obj[input.id] = parseInt(value)
|
obj[input.id] = parseInt(value)
|
||||||
|
} else {
|
||||||
|
obj[input.id] = parseFloat(value)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
obj[input.id] = value
|
obj[input.id] = value
|
||||||
}
|
}
|
||||||
@ -53,6 +58,36 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diff
|
||||||
|
export function diff(arn: AnimeNotifier, element: HTMLElement) {
|
||||||
|
let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href")
|
||||||
|
let request = fetch("/_" + url).then(response => response.text())
|
||||||
|
|
||||||
|
history.pushState(url, null, url)
|
||||||
|
arn.app.currentPath = url
|
||||||
|
arn.app.markActiveLinks()
|
||||||
|
arn.loading(true)
|
||||||
|
arn.unmountMountables()
|
||||||
|
|
||||||
|
// for(let element of findAll("mountable")) {
|
||||||
|
// element.classList.remove("mountable")
|
||||||
|
// }
|
||||||
|
|
||||||
|
delay(300).then(() => {
|
||||||
|
request
|
||||||
|
.then(html => arn.app.setContent(html, true))
|
||||||
|
.then(() => arn.app.markActiveLinks())
|
||||||
|
// .then(() => {
|
||||||
|
// for(let element of findAll("mountable")) {
|
||||||
|
// element.classList.remove("mountable")
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
.then(() => arn.app.emit("DOMContentLoaded"))
|
||||||
|
.then(() => arn.loading(false))
|
||||||
|
.catch(console.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) {
|
export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) {
|
||||||
if(e.ctrlKey || e.altKey) {
|
if(e.ctrlKey || e.altKey) {
|
||||||
@ -139,3 +174,9 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen
|
|||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
.then(() => arn.loading(false))
|
.then(() => arn.loading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chrome extension installation
|
||||||
|
export function installExtension(arn: AnimeNotifier, button: HTMLElement) {
|
||||||
|
let browser: any = window["chrome"]
|
||||||
|
browser.webstore.install()
|
||||||
|
}
|
@ -1,9 +1,14 @@
|
|||||||
export function* findAll(className: string) {
|
export function* findAll(className: string) {
|
||||||
// getElementsByClassName failed for some reason.
|
// getElementsByClassName failed for some reason.
|
||||||
// TODO: Test getElementsByClassName again.
|
// TODO: Test getElementsByClassName again.
|
||||||
let elements = document.querySelectorAll("." + className)
|
// let elements = document.querySelectorAll("." + className)
|
||||||
|
let elements = document.getElementsByClassName(className)
|
||||||
|
|
||||||
for(let i = 0; i < elements.length; ++i) {
|
for(let i = 0; i < elements.length; ++i) {
|
||||||
yield elements[i] as HTMLElement
|
yield elements[i] as HTMLElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function delay<T>(millis: number, value?: T): Promise<T> {
|
||||||
|
return new Promise(resolve => setTimeout(() => resolve(value), millis))
|
||||||
|
}
|
@ -8,6 +8,7 @@ body
|
|||||||
overflow hidden
|
overflow hidden
|
||||||
height 100%
|
height 100%
|
||||||
color text-color
|
color text-color
|
||||||
|
background-color bg-color
|
||||||
|
|
||||||
a
|
a
|
||||||
color link-color
|
color link-color
|
||||||
@ -21,8 +22,8 @@ a
|
|||||||
:active
|
:active
|
||||||
transform translateY(3px)
|
transform translateY(3px)
|
||||||
|
|
||||||
&.active
|
// &.active
|
||||||
color link-active-color
|
// color link-active-color
|
||||||
|
|
||||||
strong
|
strong
|
||||||
font-weight bold
|
font-weight bold
|
||||||
|
13
styles/embedded.scarlet
Normal file
13
styles/embedded.scarlet
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.embedded
|
||||||
|
// Put navigation to the bottom of the screen
|
||||||
|
flex-direction column-reverse !important
|
||||||
|
|
||||||
|
.extension-navigation
|
||||||
|
display inline-block
|
||||||
|
|
||||||
|
.anime-list
|
||||||
|
max-width 500px
|
||||||
|
margin -1.1rem
|
||||||
|
|
||||||
|
#navigation
|
||||||
|
font-size 0.9rem
|
@ -1,36 +0,0 @@
|
|||||||
.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
|
|
@ -1,19 +1,28 @@
|
|||||||
// Colors
|
// Colors
|
||||||
text-color = rgb(60, 60, 60)
|
text-color = rgb(60, 60, 60)
|
||||||
main-color = rgb(248, 165, 130)
|
main-color = rgb(215, 38, 15)
|
||||||
link-color = rgb(225, 38, 15)
|
link-color = main-color
|
||||||
link-hover-color = rgb(242, 60, 30)
|
link-hover-color = rgb(242, 60, 30)
|
||||||
link-active-color = rgb(100, 149, 237)
|
link-active-color = link-hover-color
|
||||||
post-highlight-color = rgba(248, 165, 130, 0.7)
|
post-highlight-color = rgba(248, 165, 130, 0.7)
|
||||||
bg-color = white
|
bg-color = rgb(246, 246, 246)
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
ui-border = 1px solid rgba(0, 0, 0, 0.1)
|
ui-border-color = rgba(0, 0, 0, 0.1)
|
||||||
ui-hover-border = 1px solid rgba(0, 0, 0, 0.15)
|
ui-hover-border-color = rgba(0, 0, 0, 0.15)
|
||||||
ui-background = linear-gradient(to bottom, rgba(0, 0, 0, 0.02) 0%, rgba(0, 0, 0, 0.037) 100%)
|
ui-background = rgb(254, 254, 254)
|
||||||
ui-hover-background = linear-gradient(to bottom, rgba(0, 0, 0, 0.01) 0%, rgba(0, 0, 0, 0.027) 100%)
|
// ui-hover-background = rgb(254, 254, 254)
|
||||||
|
// ui-background = linear-gradient(to bottom, rgba(0, 0, 0, 0.02) 0%, rgba(0, 0, 0, 0.037) 100%)
|
||||||
|
// ui-hover-background = linear-gradient(to bottom, rgba(0, 0, 0, 0.01) 0%, rgba(0, 0, 0, 0.027) 100%)
|
||||||
ui-disabled-color = rgb(224, 224, 224)
|
ui-disabled-color = rgb(224, 224, 224)
|
||||||
|
|
||||||
|
// Input
|
||||||
|
input-focus-border-color = rgb(248, 165, 130)
|
||||||
|
|
||||||
|
// Button
|
||||||
|
button-hover-color = link-hover-color
|
||||||
|
forum-tag-hover-color = rgb(46, 85, 160)
|
||||||
|
|
||||||
// Forum
|
// Forum
|
||||||
forum-width = 830px
|
forum-width = 830px
|
||||||
|
|
||||||
@ -21,9 +30,10 @@ forum-width = 830px
|
|||||||
avatar-size = 50px
|
avatar-size = 50px
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
nav-color = rgb(60, 60, 60)
|
nav-color = text-color
|
||||||
nav-link-color = rgb(160, 160, 160)
|
nav-link-color = rgba(255, 255, 255, 0.5)
|
||||||
nav-link-hover-color = rgb(255, 255, 255)
|
nav-link-hover-color = white
|
||||||
|
nav-link-hover-slide-color = rgb(248, 165, 130)
|
||||||
// nav-color = rgb(245, 245, 245)
|
// nav-color = rgb(245, 245, 245)
|
||||||
// nav-link-color = rgb(160, 160, 160)
|
// nav-link-color = rgb(160, 160, 160)
|
||||||
// nav-link-hover-color = rgb(80, 80, 80)
|
// nav-link-hover-color = rgb(80, 80, 80)
|
||||||
@ -40,7 +50,7 @@ outline-shadow-heavy = 0 0 6px rgba(0, 0, 0, 0.6)
|
|||||||
// Distances
|
// Distances
|
||||||
content-padding = 1.6rem
|
content-padding = 1.6rem
|
||||||
content-padding-top = 1.6rem
|
content-padding-top = 1.6rem
|
||||||
hover-line-size = 2px
|
hover-line-size = 3px
|
||||||
nav-height = 3.11rem
|
nav-height = 3.11rem
|
||||||
|
|
||||||
// Timings
|
// Timings
|
||||||
|
@ -18,13 +18,13 @@ mixin vertical-wrap
|
|||||||
flex-flow column wrap
|
flex-flow column wrap
|
||||||
|
|
||||||
mixin ui-element
|
mixin ui-element
|
||||||
border ui-border
|
border 1px solid ui-border-color
|
||||||
background ui-background
|
background ui-background
|
||||||
border-radius 3px
|
border-radius 3px
|
||||||
default-transition
|
default-transition
|
||||||
:hover
|
:hover
|
||||||
border ui-hover-border
|
border-color ui-hover-border-color
|
||||||
background ui-hover-background
|
// background ui-hover-background
|
||||||
// box-shadow outline-shadow-medium
|
// box-shadow outline-shadow-medium
|
||||||
|
|
||||||
mixin ui-disabled
|
mixin ui-disabled
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
mixin input-focus
|
mixin input-focus
|
||||||
:focus
|
:focus
|
||||||
color black
|
color black
|
||||||
border 1px solid main-color
|
border 1px solid input-focus-border-color
|
||||||
// TODO: Replace with alpha(main-color, 20%) function
|
// TODO: Replace with alpha(main-color, 20%) function
|
||||||
box-shadow 0 0 6px rgba(248, 165, 130, 0.2)
|
box-shadow 0 0 6px rgba(248, 165, 130, 0.2)
|
||||||
|
|
||||||
@ -16,15 +16,26 @@ input, textarea
|
|||||||
border ui-border
|
border ui-border
|
||||||
background white
|
background white
|
||||||
box-shadow none
|
box-shadow none
|
||||||
|
width 100%
|
||||||
input-focus
|
input-focus
|
||||||
|
|
||||||
:disabled
|
:disabled
|
||||||
ui-disabled
|
ui-disabled
|
||||||
|
|
||||||
|
input
|
||||||
|
default-transition
|
||||||
|
|
||||||
|
:active
|
||||||
|
transform translateY(3px)
|
||||||
|
|
||||||
// We need this to have a selector with a higher priority than .widget-element:focus
|
// We need this to have a selector with a higher priority than .widget-element:focus
|
||||||
input.widget-element
|
input.widget-element,
|
||||||
|
textarea.widget-element
|
||||||
input-focus
|
input-focus
|
||||||
|
|
||||||
|
textarea
|
||||||
|
height 10rem
|
||||||
|
|
||||||
button, .button
|
button, .button
|
||||||
ui-element
|
ui-element
|
||||||
horizontal
|
horizontal
|
||||||
@ -33,10 +44,11 @@ button, .button
|
|||||||
padding 0.75rem 1rem
|
padding 0.75rem 1rem
|
||||||
color link-color
|
color link-color
|
||||||
|
|
||||||
:hover
|
:hover,
|
||||||
|
&.active
|
||||||
cursor pointer
|
cursor pointer
|
||||||
color white
|
color white
|
||||||
background-color link-hover-color
|
background-color button-hover-color
|
||||||
|
|
||||||
:active
|
:active
|
||||||
transform translateY(3px)
|
transform translateY(3px)
|
||||||
|
@ -6,3 +6,4 @@
|
|||||||
flex 1
|
flex 1
|
||||||
overflow-x hidden
|
overflow-x hidden
|
||||||
overflow-y scroll
|
overflow-y scroll
|
||||||
|
// will-change transform
|
@ -12,8 +12,8 @@
|
|||||||
:after
|
:after
|
||||||
content ""
|
content ""
|
||||||
display block
|
display block
|
||||||
height 3px
|
height hover-line-size
|
||||||
background-color main-color
|
background-color nav-link-hover-slide-color
|
||||||
transform scaleX(0)
|
transform scaleX(0)
|
||||||
opacity 0
|
opacity 0
|
||||||
default-transition
|
default-transition
|
||||||
@ -45,13 +45,16 @@
|
|||||||
#search
|
#search
|
||||||
display none
|
display none
|
||||||
border-radius 0
|
border-radius 0
|
||||||
background text-color
|
background transparent
|
||||||
border none
|
border none
|
||||||
|
|
||||||
color white
|
color nav-link-hover-color
|
||||||
font-size 1em
|
font-size 1em
|
||||||
min-width 0
|
min-width 0
|
||||||
|
|
||||||
|
::placeholder
|
||||||
|
color nav-link-color
|
||||||
|
|
||||||
:focus
|
:focus
|
||||||
border none
|
border none
|
||||||
box-shadow none
|
box-shadow none
|
||||||
@ -59,6 +62,9 @@
|
|||||||
.extra-navigation
|
.extra-navigation
|
||||||
display none
|
display none
|
||||||
|
|
||||||
|
.extension-navigation
|
||||||
|
display none
|
||||||
|
|
||||||
> 330px
|
> 330px
|
||||||
.navigation-button, #search
|
.navigation-button, #search
|
||||||
font-size 1.3em
|
font-size 1.3em
|
||||||
|
@ -20,4 +20,4 @@ th
|
|||||||
tbody
|
tbody
|
||||||
tr
|
tr
|
||||||
:hover
|
:hover
|
||||||
background-color rgba(0, 0, 0, 0.03)
|
background-color rgba(0, 0, 0, 0.015)
|
@ -8,6 +8,7 @@ p, h1, h2, h3, h4, h5, h6
|
|||||||
margin-bottom 0
|
margin-bottom 0
|
||||||
|
|
||||||
h2
|
h2
|
||||||
|
margin-top content-padding
|
||||||
margin-bottom content-padding
|
margin-bottom content-padding
|
||||||
|
|
||||||
p > img
|
p > img
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
.widget-element
|
.widget-element
|
||||||
vertical-wrap
|
vertical-wrap
|
||||||
ui-element
|
ui-element
|
||||||
transition border transition-speed ease, background transition-speed ease, transform transition-speed ease
|
transition border transition-speed ease, background transition-speed ease, transform transition-speed ease, transform color ease
|
||||||
margin-bottom 1rem
|
margin-bottom 1rem
|
||||||
padding 0.5rem 1rem
|
padding 0.5rem 1rem
|
||||||
width 100%
|
width 100%
|
||||||
|
Loading…
Reference in New Issue
Block a user