diff --git a/.vscode/settings.json b/.vscode/settings.json index 0fcb2420..c92756b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,10 +6,15 @@ "files.exclude": { "**/*.js": { "when": "$(basename).ts" - } + }, + "components/": true }, "files.associations": { "*.pixy": "jade", "*.scarlet": "stylus" + }, + "search.exclude": { + "components/": true, + "**/*.svg": true } } \ No newline at end of file diff --git a/README.md b/README.md index e55dfb03..e9c91f65 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets.go b/assets.go index 69cc8f80..1c312697 100644 --- a/assets.go +++ b/assets.go @@ -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 diff --git a/benchmarks/Components_test.go b/benchmarks/Components_test.go new file mode 100644 index 00000000..ef68455e --- /dev/null +++ b/benchmarks/Components_test.go @@ -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) + } + }) +} diff --git a/config.json b/config.json index c4244c39..b4ecbbf9 100644 --- a/config.json +++ b/config.json @@ -22,7 +22,7 @@ "loading", "fade", "mobile", - "extension" + "embedded" ], "scripts": { "main": "main" diff --git a/images/elements/extension-screenshot.png b/images/elements/extension-screenshot.png new file mode 100644 index 00000000..8c29bbe1 Binary files /dev/null and b/images/elements/extension-screenshot.png differ diff --git a/jobs/avatars/main.go b/jobs/avatars/main.go index 064fcf0f..c6a76bbc 100644 --- a/jobs/avatars/main.go +++ b/jobs/avatars/main.go @@ -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")) diff --git a/jobs/main.go b/jobs/main.go new file mode 100644 index 00000000..54db996b --- /dev/null +++ b/jobs/main.go @@ -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 +} diff --git a/layout/layout.pixy b/layout/layout.pixy index 86007cdb..65523533 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -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)) diff --git a/main.go b/main.go index 16c31cf2..c2993c4e 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/main_test.go b/main_test.go index e35031c9..a766bc9f 100644 --- a/main_test.go +++ b/main_test.go @@ -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) + } + } } } diff --git a/mixins/ForumTags.pixy b/mixins/ForumTags.pixy index b1754cbe..ff367246 100644 --- a/mixins/ForumTags.pixy +++ b/mixins/ForumTags.pixy @@ -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 \ No newline at end of file diff --git a/mixins/Input.pixy b/mixins/Input.pixy index b5aa3884..352bd112 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -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 diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 0a6a55df..a8c01021 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -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 diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index 214e4cea..8c165a6d 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -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 diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 2b770f46..4ef007d7 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -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 diff --git a/pages/animelist/animelist.go b/pages/animelist/animelist.go index d06f7227..6851effb 100644 --- a/pages/animelist/animelist.go +++ b/pages/animelist/animelist.go @@ -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)) } diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 89092062..7ea276f8 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -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() \ No newline at end of file + .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") \ No newline at end of file diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index e943c080..76badd5d 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index a03510f2..4c1276c4 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -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") diff --git a/pages/animelistitem/animelistitem.scarlet b/pages/animelistitem/animelistitem.scarlet index 459e3881..8603071c 100644 --- a/pages/animelistitem/animelistitem.scarlet +++ b/pages/animelistitem/animelistitem.scarlet @@ -1,7 +1,10 @@ -.anime-list-item-view - textarea - height 10rem - .anime-list-item-view-image max-width 55px - margin-bottom 1rem \ No newline at end of file + margin-bottom 1rem + +.anime-list-item-rating-edit + horizontal-wrap + justify-content space-between + width 100% + .widget-input + max-width 120px \ No newline at end of file diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index f37bbf1c..c4033b6f 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -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)) } diff --git a/pages/embed/embed.go b/pages/embed/embed.go index 8c486ce2..cf33a2f9 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -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))) } diff --git a/pages/forum/forum.scarlet b/pages/forum/forum.scarlet index 48c77a22..4775f365 100644 --- a/pages/forum/forum.scarlet +++ b/pages/forum/forum.scarlet @@ -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 diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index b4ca3084..0a364af2 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/pages/frontpage/frontpage.scarlet b/pages/frontpage/frontpage.scarlet index 2a43abf7..98cb03ae 100644 --- a/pages/frontpage/frontpage.scarlet +++ b/pages/frontpage/frontpage.scarlet @@ -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 \ No newline at end of file +.screenshot + max-width 100% + border-radius 3px + box-shadow shadow-medium + margin-bottom 2rem + + :hover + cursor pointer \ No newline at end of file diff --git a/pages/login/login.scarlet b/pages/login/login.scarlet new file mode 100644 index 00000000..2a43abf7 --- /dev/null +++ b/pages/login/login.scarlet @@ -0,0 +1,11 @@ +.login-buttons + horizontal-wrap + width 100% + justify-content center + +.login-button + // + +.login-button-image + max-width 236px + max-height 44px \ No newline at end of file diff --git a/pages/popularanime/popular.go b/pages/popularanime/popular.go index c7cd7e22..1d80e7ef 100644 --- a/pages/popularanime/popular.go +++ b/pages/popularanime/popular.go @@ -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)) } diff --git a/pages/popularanime/popular.pixy b/pages/popularanime/popular.pixy new file mode 100644 index 00000000..3d5aeec7 --- /dev/null +++ b/pages/popularanime/popular.pixy @@ -0,0 +1,6 @@ +component PopularAnime(animeList []*arn.Anime) + h2 Top 3 + AnimeGrid(animeList[:3]) + + h2 Popular + AnimeGrid(animeList[3:]) \ No newline at end of file diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index a7abc4fe..3dcbd6b2 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -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) diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index b8784fd0..f67aac62 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -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 diff --git a/pages/threads/threads.go b/pages/threads/threads.go index c2c112dd..92d96248 100644 --- a/pages/threads/threads.go +++ b/pages/threads/threads.go @@ -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)) } diff --git a/pages/threads/threads.pixy b/pages/threads/threads.pixy index 9ad4433f..6459af81 100644 --- a/pages/threads/threads.pixy +++ b/pages/threads/threads.pixy @@ -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...") \ No newline at end of file diff --git a/reset.scarlet b/reset.scarlet deleted file mode 100644 index a93e2791..00000000 --- a/reset.scarlet +++ /dev/null @@ -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 \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index d705e594..f371b015 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -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) } } } diff --git a/scripts/Application.ts b/scripts/Application.ts index 54f60e7f..a11f3f06 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -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) { diff --git a/scripts/Diff.ts b/scripts/Diff.ts index aa4ab348..ea3c2620 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -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 } } diff --git a/scripts/actions.ts b/scripts/actions.ts index a6e7cb5e..52a9d68a 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -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() } \ No newline at end of file diff --git a/scripts/utils.ts b/scripts/utils.ts index 41ebc19e..b97017d3 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -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(millis: number, value?: T): Promise { + return new Promise(resolve => setTimeout(() => resolve(value), millis)) } \ No newline at end of file diff --git a/styles/base.scarlet b/styles/base.scarlet index 6dd9d375..bf3fea68 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -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 diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet new file mode 100644 index 00000000..07a9d427 --- /dev/null +++ b/styles/embedded.scarlet @@ -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 \ No newline at end of file diff --git a/styles/extension.scarlet b/styles/extension.scarlet deleted file mode 100644 index 50eb9d4e..00000000 --- a/styles/extension.scarlet +++ /dev/null @@ -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 \ No newline at end of file diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index e5cb98c5..08c869ec 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -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 diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 20546f94..35770a34 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -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 diff --git a/styles/input.scarlet b/styles/input.scarlet index 8c272583..54afac5e 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -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) diff --git a/styles/layout.scarlet b/styles/layout.scarlet index 695f70ba..d1765c53 100644 --- a/styles/layout.scarlet +++ b/styles/layout.scarlet @@ -5,4 +5,5 @@ #content-container flex 1 overflow-x hidden - overflow-y scroll \ No newline at end of file + overflow-y scroll + // will-change transform \ No newline at end of file diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index 89582ed3..d22110d4 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -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 diff --git a/styles/table.scarlet b/styles/table.scarlet index dd94a43b..3ee0073b 100644 --- a/styles/table.scarlet +++ b/styles/table.scarlet @@ -20,4 +20,4 @@ th tbody tr :hover - background-color rgba(0, 0, 0, 0.03) \ No newline at end of file + background-color rgba(0, 0, 0, 0.015) \ No newline at end of file diff --git a/styles/typography.scarlet b/styles/typography.scarlet index a9bcb6bc..16633b0f 100644 --- a/styles/typography.scarlet +++ b/styles/typography.scarlet @@ -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 diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index 5dfb8485..f14acd84 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -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% diff --git a/tests.go b/tests.go index 53b1352e..36624929 100644 --- a/tests.go +++ b/tests.go @@ -122,6 +122,7 @@ var tests = map[string][]string{ "/auth/google/callback": nil, "/user": nil, "/settings": nil, + "/extension/embed": nil, } func init() {