Merge pull request #3 from animenotifier/go

.
This commit is contained in:
Allen Lydiard 2017-06-26 12:43:11 -03:00 committed by GitHub
commit b1e9740a83
51 changed files with 556 additions and 223 deletions

View File

@ -6,10 +6,15 @@
"files.exclude": {
"**/*.js": {
"when": "$(basename).ts"
}
},
"components/": true
},
"files.associations": {
"*.pixy": "jade",
"*.scarlet": "stylus"
},
"search.exclude": {
"components/": true,
"**/*.svg": true
}
}

View File

@ -1,5 +1,9 @@
# 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
### Prerequisites

View File

@ -7,21 +7,21 @@ import (
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/components/js"
)
func init() {
// Scripts
js := components.JS()
scripts := js.Bundle()
app.Get("/scripts", func(ctx *aero.Context) string {
ctx.SetResponseHeader("Content-Type", "application/javascript")
return js
return scripts
})
app.Get("/scripts.js", func(ctx *aero.Context) string {
ctx.SetResponseHeader("Content-Type", "application/javascript")
return js
return scripts
})
// Web manifest

View 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)
}
})
}

View File

@ -22,7 +22,7 @@
"loading",
"fade",
"mobile",
"extension"
"embedded"
],
"scripts": {
"main": "main"

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"path"
"reflect"
"runtime"
"time"
@ -29,7 +30,14 @@ func main() {
color.Yellow("Generating user avatars")
// 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
avatarLog.AddOutput(log.File("logs/avatar.log"))

123
jobs/main.go Normal file
View 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
}

View File

@ -4,6 +4,7 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, conte
title= app.Config.Title
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)
link(rel="chrome-webstore-item", href="https://chrome.google.com/webstore/detail/hajchfikckiofgilinkpifobdbiajfch")
link(rel="manifest", href="/manifest.json")
body
#container(class=utils.GetContainerClass(ctx))

View File

@ -5,7 +5,7 @@ import (
"github.com/aerogo/api"
"github.com/animenotifier/arn"
"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/middleware"
"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")
// CSS
app.SetStyle(components.CSS())
app.SetStyle(css.Bundle())
// Sessions
app.Sessions.Duration = 3600 * 24

View File

@ -9,19 +9,22 @@ import (
)
func TestRoutes(t *testing.T) {
expectedStatus := http.StatusOK
app := configure(aero.New())
request, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
for _, examples := range tests {
for _, example := range examples {
request, err := http.NewRequest("GET", example, nil)
responseRecorder := httptest.NewRecorder()
app.Handler().ServeHTTP(responseRecorder, request)
if err != nil {
t.Fatal(err)
}
if status := responseRecorder.Code; status != expectedStatus {
t.Errorf("Wrong status code: %v instead of %v", status, expectedStatus)
responseRecorder := httptest.NewRecorder()
app.Handler().ServeHTTP(responseRecorder, request)
if status := responseRecorder.Code; status != http.StatusOK {
t.Errorf("%s | Wrong status code | %v instead of %v", example, status, http.StatusOK)
}
}
}
}

View File

@ -9,6 +9,6 @@ component ForumTags
ForumTag("Bugs", "bug", "list")
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))
span.forum-tag-text= title

View File

@ -8,10 +8,10 @@ component InputTextArea(id string, value string, label string, placeholder strin
label(for=id)= label + ":"
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
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)
.widget-input

View File

@ -20,10 +20,13 @@ component LoggedOutMenu
component LoggedInMenu(user *arn.User)
nav#navigation.logged-in
NavigationButton("Dash", "/", "inbox")
NavigationButton("Anime", "/anime", "television")
.extension-navigation
NavigationButton("Watching list", "/extension/embed", "home")
NavigationButton("Dash", "/", "dashboard")
NavigationButton("Profile", "/+", "user")
NavigationButton("Forum", "/forum", "comment")
NavigationButton("Anime", "/anime", "television")
FuzzySearch

View File

@ -7,7 +7,7 @@ component Postable(post arn.Postable, highlightAuthorID string)
//- 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)
.post-content
div(id="render-" + post.ID())!= aero.Markdown(post.Text())
div(id="render-" + post.ID())!= post.HTML()
//- if user && user.ID === post.authorId
//- textarea.post-input.hidden(id="source-" + post.ID)= post.text

View File

@ -41,9 +41,9 @@ component Anime(anime *arn.Anime, user *arn.User)
.anime-rating-category(title=toString(anime.Rating.Visuals / 10))
.anime-rating-category-name Visuals
Rating(anime.Rating.Visuals)
.anime-rating-category(title=toString(anime.Rating.Music / 10))
.anime-rating-category-name Music
Rating(anime.Rating.Music)
.anime-rating-category(title=toString(anime.Rating.Soundtrack / 10))
.anime-rating-category-name Soundtrack
Rating(anime.Rating.Soundtrack)
if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].VideoID != ""
h3.anime-section-name Video

View File

@ -7,11 +7,13 @@ import (
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
// Get anime list.
func Get(ctx *aero.Context) string {
nick := ctx.Get("nick")
user := utils.GetUser(ctx)
viewUser, err := arn.GetUserByNick(nick)
if err != nil {
@ -25,8 +27,8 @@ func Get(ctx *aero.Context) string {
}
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))
}

View File

@ -1,17 +1,26 @@
component AnimeList(animeList *arn.AnimeList)
component AnimeList(animeList *arn.AnimeList, user *arn.User)
table.anime-list
thead
tr
th.anime-list-item-name Anime
th.anime-list-item-episodes Progress
th.anime-list-item-episodes Episodes
th.anime-list-item-rating Rating
if user != nil
th.anime-list-item-actions Actions
tbody
each item in animeList.Items
tr.anime-list-item.mountable(title=item.Notes)
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
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()
.anime-list-item-episodes-watched= item.Episodes
.anime-list-item-episodes-separator /
.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()
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")

View File

@ -4,17 +4,53 @@
tr
horizontal
thead
display none
.anime-list-item-name
flex 0.8
flex 1
white-space nowrap
text-overflow ellipsis
overflow hidden
.anime-list-item-episodes
flex 0.1
text-align center
horizontal
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
flex 0.1
text-align center
flex-basis 100px
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

View File

@ -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)
h2= anime.Title.Canonical
InputNumber("Episodes", item.Episodes, "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount))
InputNumber("RewatchCount", item.RewatchCount, "Rewatched", "How often you rewatched this anime", "0", "100")
InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1")
.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")

View File

@ -1,7 +1,10 @@
.anime-list-item-view
textarea
height 10rem
.anime-list-item-view-image
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

View File

@ -19,7 +19,7 @@ func Get(ctx *aero.Context) string {
return frontpage.Get(ctx)
}
posts, err := arn.GetPosts()
posts, err := arn.AllPostsSlice()
if err != nil {
return ctx.Error(500, "Error fetching posts", err)
@ -32,6 +32,7 @@ func Get(ctx *aero.Context) string {
}
followIDList := user.Following
var followingList []*arn.User
if len(followIDList) > maxFollowing {
followIDList = followIDList[:maxFollowing]
@ -43,7 +44,7 @@ func Get(ctx *aero.Context) string {
return ctx.Error(500, "Error fetching followed users", err)
}
followingList := userList.([]*arn.User)
followingList = userList.([]*arn.User)
return ctx.HTML(components.Dashboard(posts, followingList))
}

View File

@ -14,7 +14,7 @@ func Get(ctx *aero.Context) string {
user := utils.GetUser(ctx)
if user == nil {
return ctx.Error(http.StatusUnauthorized, "Not logged in", nil)
return utils.AllowEmbed(ctx, ctx.HTML(components.Login()))
}
animeList := user.AnimeList()
@ -24,8 +24,8 @@ func Get(ctx *aero.Context) string {
}
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)))
}

View File

@ -8,11 +8,18 @@
.forum-tag
color text-color !important
:hover
color white !important
:hover,
&.active
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
.forum-tag

View File

@ -1,7 +1,12 @@
component FrontPage
p Anime Notifier 4.0 is currently under construction.
p
a(href="https://paypal.me/blitzprog", target="_blank", rel="noopener") Support the development
.frontpage
h2 notify.moe
p
a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub
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

View File

@ -1,11 +1,22 @@
.login-buttons
horizontal-wrap
width 100%
justify-content center
.frontpage
vertical
align-items center
.login-button
//
h2
font-size 2.5rem
font-weight normal
letter-spacing 3px
text-transform uppercase
.footer
text-align center
margin-top content-padding
.login-button-image
max-width 236px
max-height 44px
.screenshot
max-width 100%
border-radius 3px
box-shadow shadow-medium
margin-bottom 2rem
:hover
cursor pointer

11
pages/login/login.scarlet Normal file
View 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

View File

@ -16,5 +16,5 @@ func Get(ctx *aero.Context) string {
return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err)
}
return ctx.HTML(components.AnimeGrid(animeList))
return ctx.HTML(components.PopularAnime(animeList))
}

View File

@ -0,0 +1,6 @@
component PopularAnime(animeList []*arn.Anime)
h2 Top 3
AnimeGrid(animeList[:3])
h2 Popular
AnimeGrid(animeList[3:])

View File

@ -79,7 +79,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList,
.post-author
Avatar(post.Author())
.post-content
div!= aero.Markdown(post.Text)
div!= post.HTML()
.post-toolbar.active
.spacer
.post-likes= len(post.Likes)

View File

@ -80,6 +80,10 @@ profile-boot-duration = 2s
padding-left calc(content-padding * 2)
max-width 900px
.website
a
color white
#nick
margin-bottom 1rem

View File

@ -1,29 +1,39 @@
package threads
import (
"strings"
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
// Get thread.
func Get(ctx *aero.Context) string {
id := ctx.Get("id")
thread, err := arn.GetThread(id)
user := utils.GetUser(ctx)
if err != nil {
return ctx.Error(404, "Thread not found", err)
}
replies, filterErr := arn.FilterPosts(func(post *arn.Post) bool {
post.Text = strings.Replace(post.Text, "http://", "https://", -1)
return post.ThreadID == thread.ID
})
arn.SortPostsLatestLast(replies)
// Benchmark
// for i := 0; i < 7; i++ {
// replies = append(replies, replies...)
// }
if filterErr != nil {
return ctx.Error(500, "Error fetching thread replies", err)
}
return ctx.HTML(components.Thread(thread, replies))
return ctx.HTML(components.Thread(thread, replies, user))
}

View File

@ -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
.thread
@ -7,3 +7,12 @@ component Thread(thread *arn.Thread, posts []*arn.Post)
each post in posts
Postable(post.ToPostable(), thread.Author().ID)
// Reply
if user != nil
.post.mountable
.post-author
Avatar(user)
.post-content
textarea(id="new-reply", placeholder="Reply...")

View File

@ -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

View File

@ -53,8 +53,8 @@ export class AnimeNotifier {
this.visibilityObserver.disconnect()
// Update each of these asynchronously
Promise.resolve().then(() => this.updateMountables())
Promise.resolve().then(() => this.updateActions())
Promise.resolve().then(() => this.mountMountables())
Promise.resolve().then(() => this.assignActions())
Promise.resolve().then(() => this.lazyLoadImages())
}
@ -75,7 +75,7 @@ export class AnimeNotifier {
}
}
updateActions() {
assignActions() {
for(let element of findAll("action")) {
if(element["action assigned"]) {
continue
@ -85,6 +85,9 @@ export class AnimeNotifier {
element.addEventListener(element.dataset.trigger, e => {
actions[actionName](this, element, e)
e.stopPropagation()
e.preventDefault()
})
// Use "action assigned" flag instead of removing the class.
@ -121,21 +124,31 @@ export class AnimeNotifier {
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 maxDelay = 1000
const maxDelay = 500
let time = 0
for(let element of findAll("mountable")) {
setTimeout(() => {
window.requestAnimationFrame(() => element.classList.add("mounted"))
}, time)
for(let element of findAll(className)) {
time += delay
if(time > maxDelay) {
time = maxDelay
func(element)
} else {
setTimeout(() => {
window.requestAnimationFrame(() => func(element))
}, time)
}
}
}

View File

@ -1,3 +1,5 @@
import { Diff } from "./Diff"
class LoadOptions {
addToHistory?: boolean
forceReload?: boolean
@ -57,6 +59,10 @@ export class Application {
}
load(url: string, options?: LoadOptions) {
// Start sending a network request
let request = this.get("/_" + url).catch(error => error)
// Parse options
if(!options) {
options = new LoadOptions()
}
@ -64,11 +70,13 @@ export class Application {
if(options.addToHistory === undefined) {
options.addToHistory = true
}
// Set current path
this.currentPath = url
// Start sending a network request
let request = this.get("/_" + url).catch(error => error)
// Add to browser history
if(options.addToHistory)
history.pushState(url, null, url)
let onTransitionEnd = e => {
// Ignore transitions of child elements.
@ -82,13 +90,8 @@ export class Application {
// Wait for the network request to end.
request.then(html => {
// Add to browser history
if(options.addToHistory)
history.pushState(url, null, url)
// Set content
this.setContent(html)
this.scrollToTop()
this.setContent(html, false)
// Fade animations
this.content.classList.remove(this.fadeOutClass)
@ -108,11 +111,16 @@ export class Application {
return request
}
setContent(html: string) {
// Diff.innerHTML(this.content, html)
this.content.innerHTML = html
setContent(html: string, diff: boolean) {
if(diff) {
Diff.innerHTML(this.content, html)
} else {
this.content.innerHTML = html
}
this.ajaxify(this.content)
this.markActiveLinks(this.content)
this.scrollToTop()
}
markActiveLinks(element?: HTMLElement) {

View File

@ -1,18 +1,18 @@
export class Diff {
static childNodes(aRoot: HTMLElement, bRoot: HTMLElement) {
static childNodes(aRoot: Node, bRoot: Node) {
let aChild = [...aRoot.childNodes]
let bChild = [...bRoot.childNodes]
let numNodes = Math.max(aChild.length, bChild.length)
for(let i = 0; i < numNodes; i++) {
let a = aChild[i] as HTMLElement
let a = aChild[i]
if(i >= bChild.length) {
aRoot.removeChild(a)
continue
}
let b = bChild[i] as HTMLElement
let b = bChild[i]
if(i >= aChild.length) {
aRoot.appendChild(b)
@ -24,38 +24,46 @@ export class Diff {
continue
}
if(a.nodeType === Node.TEXT_NODE) {
a.textContent = b.textContent
continue
}
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
}
let removeAttributes: Attr[] = []
for(let x = 0; x < a.attributes.length; x++) {
let attrib = a.attributes[x]
for(let x = 0; x < elemA.attributes.length; x++) {
let attrib = elemA.attributes[x]
if(attrib.specified) {
if(!b.hasAttribute(attrib.name)) {
if(!elemB.hasAttribute(attrib.name)) {
removeAttributes.push(attrib)
}
}
}
for(let attr of removeAttributes) {
a.removeAttributeNode(attr)
elemA.removeAttributeNode(attr)
}
for(let x = 0; x < b.attributes.length; x++) {
let attrib = b.attributes[x]
for(let x = 0; x < elemB.attributes.length; x++) {
let attrib = elemB.attributes[x]
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
if(a !== document.activeElement && a instanceof HTMLInputElement && b instanceof HTMLInputElement) {
a.value = b.value
if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) {
elemA.value = elemB.value
}
}

View File

@ -1,6 +1,7 @@
import { Application } from "./Application"
import { AnimeNotifier } from "./AnimeNotifier"
import { Diff } from "./Diff"
import { delay, findAll } from "./utils"
// Save new data from an input field
export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) {
@ -10,7 +11,11 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE
let value = input.value
if(input.type === "number") {
obj[input.id] = parseInt(value)
if(input.getAttribute("step") === "1") {
obj[input.id] = parseInt(value)
} else {
obj[input.id] = parseFloat(value)
}
} else {
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
export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) {
if(e.ctrlKey || e.altKey) {
@ -138,4 +173,10 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen
})
.catch(console.error)
.then(() => arn.loading(false))
}
// Chrome extension installation
export function installExtension(arn: AnimeNotifier, button: HTMLElement) {
let browser: any = window["chrome"]
browser.webstore.install()
}

View File

@ -1,9 +1,14 @@
export function* findAll(className: string) {
// getElementsByClassName failed for some reason.
// 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) {
yield elements[i] as HTMLElement
}
}
export function delay<T>(millis: number, value?: T): Promise<T> {
return new Promise(resolve => setTimeout(() => resolve(value), millis))
}

View File

@ -8,6 +8,7 @@ body
overflow hidden
height 100%
color text-color
background-color bg-color
a
color link-color
@ -21,8 +22,8 @@ a
:active
transform translateY(3px)
&.active
color link-active-color
// &.active
// color link-active-color
strong
font-weight bold

13
styles/embedded.scarlet Normal file
View 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

View File

@ -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

View File

@ -1,19 +1,28 @@
// Colors
text-color = rgb(60, 60, 60)
main-color = rgb(248, 165, 130)
link-color = rgb(225, 38, 15)
main-color = rgb(215, 38, 15)
link-color = main-color
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)
bg-color = white
bg-color = rgb(246, 246, 246)
// UI
ui-border = 1px solid rgba(0, 0, 0, 0.1)
ui-hover-border = 1px solid 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-hover-background = linear-gradient(to bottom, rgba(0, 0, 0, 0.01) 0%, rgba(0, 0, 0, 0.027) 100%)
ui-border-color = rgba(0, 0, 0, 0.1)
ui-hover-border-color = rgba(0, 0, 0, 0.15)
ui-background = rgb(254, 254, 254)
// 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)
// 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-width = 830px
@ -21,9 +30,10 @@ forum-width = 830px
avatar-size = 50px
// Navigation
nav-color = rgb(60, 60, 60)
nav-link-color = rgb(160, 160, 160)
nav-link-hover-color = rgb(255, 255, 255)
nav-color = text-color
nav-link-color = rgba(255, 255, 255, 0.5)
nav-link-hover-color = white
nav-link-hover-slide-color = rgb(248, 165, 130)
// nav-color = rgb(245, 245, 245)
// nav-link-color = rgb(160, 160, 160)
// 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
content-padding = 1.6rem
content-padding-top = 1.6rem
hover-line-size = 2px
hover-line-size = 3px
nav-height = 3.11rem
// Timings

View File

@ -18,13 +18,13 @@ mixin vertical-wrap
flex-flow column wrap
mixin ui-element
border ui-border
border 1px solid ui-border-color
background ui-background
border-radius 3px
default-transition
:hover
border ui-hover-border
background ui-hover-background
border-color ui-hover-border-color
// background ui-hover-background
// box-shadow outline-shadow-medium
mixin ui-disabled

View File

@ -1,7 +1,7 @@
mixin input-focus
:focus
color black
border 1px solid main-color
border 1px solid input-focus-border-color
// TODO: Replace with alpha(main-color, 20%) function
box-shadow 0 0 6px rgba(248, 165, 130, 0.2)
@ -16,15 +16,26 @@ input, textarea
border ui-border
background white
box-shadow none
width 100%
input-focus
: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
input.widget-element
input.widget-element,
textarea.widget-element
input-focus
textarea
height 10rem
button, .button
ui-element
horizontal
@ -33,10 +44,11 @@ button, .button
padding 0.75rem 1rem
color link-color
:hover
:hover,
&.active
cursor pointer
color white
background-color link-hover-color
background-color button-hover-color
:active
transform translateY(3px)

View File

@ -5,4 +5,5 @@
#content-container
flex 1
overflow-x hidden
overflow-y scroll
overflow-y scroll
// will-change transform

View File

@ -12,8 +12,8 @@
:after
content ""
display block
height 3px
background-color main-color
height hover-line-size
background-color nav-link-hover-slide-color
transform scaleX(0)
opacity 0
default-transition
@ -45,13 +45,16 @@
#search
display none
border-radius 0
background text-color
background transparent
border none
color white
color nav-link-hover-color
font-size 1em
min-width 0
::placeholder
color nav-link-color
:focus
border none
box-shadow none
@ -59,6 +62,9 @@
.extra-navigation
display none
.extension-navigation
display none
> 330px
.navigation-button, #search
font-size 1.3em

View File

@ -20,4 +20,4 @@ th
tbody
tr
:hover
background-color rgba(0, 0, 0, 0.03)
background-color rgba(0, 0, 0, 0.015)

View File

@ -8,6 +8,7 @@ p, h1, h2, h3, h4, h5, h6
margin-bottom 0
h2
margin-top content-padding
margin-bottom content-padding
p > img

View File

@ -12,7 +12,7 @@
.widget-element
vertical-wrap
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
padding 0.5rem 1rem
width 100%

View File

@ -122,6 +122,7 @@ var tests = map[string][]string{
"/auth/google/callback": nil,
"/user": nil,
"/settings": nil,
"/extension/embed": nil,
}
func init() {