From 184aa8568fbedeb6aeebce6e21f6253d3e34b7c6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 26 Jun 2017 18:26:15 +0200 Subject: [PATCH 001/527] Content container is now the root for position absolute --- styles/content.scarlet | 3 +-- styles/layout.scarlet | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/content.scarlet b/styles/content.scarlet index b881cd03..2062384e 100644 --- a/styles/content.scarlet +++ b/styles/content.scarlet @@ -2,5 +2,4 @@ vertical padding content-padding padding-top content-padding-top - line-height 1.7em - position relative \ No newline at end of file + line-height 1.7em \ No newline at end of file diff --git a/styles/layout.scarlet b/styles/layout.scarlet index d1765c53..6e425f76 100644 --- a/styles/layout.scarlet +++ b/styles/layout.scarlet @@ -6,4 +6,5 @@ flex 1 overflow-x hidden overflow-y scroll + position relative // will-change transform \ No newline at end of file From 3d62ab830502c8cb296dfe1536553e83babb536c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 26 Jun 2017 19:03:48 +0200 Subject: [PATCH 002/527] Style changes --- pages/dashboard/dashboard.go | 10 +++++----- pages/threads/threads.pixy | 7 ++++++- scripts/AnimeNotifier.ts | 30 +++++++++++++++++++++++++++++- scripts/actions.ts | 31 +++++++------------------------ 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 8aa9b386..6246dee2 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -12,6 +12,7 @@ import ( const maxPosts = 5 const maxFollowing = 5 +// Get the dashboard or the frontpage when logged out. func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) @@ -19,16 +20,17 @@ func Get(ctx *aero.Context) string { return frontpage.Get(ctx) } - return Dashboard(ctx) + return dashboard(ctx) } -// Get dashboard. -func Dashboard(ctx *aero.Context) string { +// Render the dashboard. +func dashboard(ctx *aero.Context) string { var posts []*arn.Post var err error var followIDList []string var userList interface{} var followingList []*arn.User + user := utils.GetUser(ctx) flow.Parallel(func() { @@ -38,7 +40,6 @@ func Dashboard(ctx *aero.Context) string { if len(posts) > maxPosts { posts = posts[:maxPosts] } - }, func() { followIDList = user.Following userList, err = arn.DB.GetMany("User", followIDList) @@ -48,7 +49,6 @@ func Dashboard(ctx *aero.Context) string { if len(followingList) > maxFollowing { followingList = followingList[:maxFollowing] } - }) if err != nil { diff --git a/pages/threads/threads.pixy b/pages/threads/threads.pixy index 6459af81..d2dc6514 100644 --- a/pages/threads/threads.pixy +++ b/pages/threads/threads.pixy @@ -15,4 +15,9 @@ component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User) Avatar(user) .post-content - textarea(id="new-reply", placeholder="Reply...") \ No newline at end of file + textarea#new-reply(placeholder="Reply...") + + .buttons + button.action(data-action="forumReply", data-trigger="click") + Icon("mail-reply") + span Reply \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index f371b015..0ce65f84 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -1,6 +1,6 @@ import { Application } from "./Application" import { Diff } from "./Diff" -import { findAll } from "./utils" +import { findAll, delay } from "./utils" import * as actions from "./actions" export class AnimeNotifier { @@ -153,6 +153,34 @@ export class AnimeNotifier { } } + diffURL(url: string) { + let request = fetch("/_" + url).then(response => response.text()) + + history.pushState(url, null, url) + this.app.currentPath = url + this.app.markActiveLinks() + this.loading(true) + this.unmountMountables() + + // for(let element of findAll("mountable")) { + // element.classList.remove("mountable") + // } + + delay(300).then(() => { + request + .then(html => this.app.setContent(html, true)) + .then(() => this.app.markActiveLinks()) + // .then(() => { + // for(let element of findAll("mountable")) { + // element.classList.remove("mountable") + // } + // }) + .then(() => this.app.emit("DOMContentLoaded")) + .then(() => this.loading(false)) + .catch(console.error) + }) + } + onPopState(e: PopStateEvent) { if(e.state) { this.app.load(e.state, { diff --git a/scripts/actions.ts b/scripts/actions.ts index 52a9d68a..5fc88c11 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -1,7 +1,6 @@ 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) { @@ -61,31 +60,15 @@ 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()) + arn.diffURL(url) +} - history.pushState(url, null, url) - arn.app.currentPath = url - arn.app.markActiveLinks() - arn.loading(true) - arn.unmountMountables() +// Forum reply +export function forumReply(arn: AnimeNotifier) { + let textarea = arn.app.find("new-reply") as HTMLTextAreaElement - // 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) - }) + console.log(textarea.value) + arn.diffURL(arn.app.currentPath) } // Search From 3215714e6a8fd9f808f34b37cd7e73d96a9849b7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 26 Jun 2017 20:37:04 +0200 Subject: [PATCH 003/527] Fixed UI border color and missing credentials --- scripts/AnimeNotifier.ts | 18 ++++++------------ scripts/Application.ts | 2 +- styles/include/config.scarlet | 2 ++ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 0ce65f84..d0fc6907 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -154,27 +154,21 @@ export class AnimeNotifier { } diffURL(url: string) { - let request = fetch("/_" + url).then(response => response.text()) - + let request = fetch("/_" + url, { + credentials: "same-origin" + }) + .then(response => response.text()) + history.pushState(url, null, url) this.app.currentPath = url this.app.markActiveLinks() - this.loading(true) this.unmountMountables() - - // for(let element of findAll("mountable")) { - // element.classList.remove("mountable") - // } + this.loading(true) delay(300).then(() => { request .then(html => this.app.setContent(html, true)) .then(() => this.app.markActiveLinks()) - // .then(() => { - // for(let element of findAll("mountable")) { - // element.classList.remove("mountable") - // } - // }) .then(() => this.app.emit("DOMContentLoaded")) .then(() => this.loading(false)) .catch(console.error) diff --git a/scripts/Application.ts b/scripts/Application.ts index a11f3f06..04c0d13d 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -92,6 +92,7 @@ export class Application { request.then(html => { // Set content this.setContent(html, false) + this.scrollToTop() // Fade animations this.content.classList.remove(this.fadeOutClass) @@ -120,7 +121,6 @@ export class Application { this.ajaxify(this.content) this.markActiveLinks(this.content) - this.scrollToTop() } markActiveLinks(element?: HTMLElement) { diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 08c869ec..7359ff69 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -9,7 +9,9 @@ bg-color = rgb(246, 246, 246) // UI ui-border-color = rgba(0, 0, 0, 0.1) +ui-border = 1px solid ui-border-color ui-hover-border-color = rgba(0, 0, 0, 0.15) +ui-hover-border-color = 1px solid ui-hover-border-color 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%) From fad6f7657a07ab5c75e5c09e50559dd743f0fc41 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 26 Jun 2017 20:58:51 +0200 Subject: [PATCH 004/527] Slightly reduced cover image brightness --- pages/profile/profile.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index f67aac62..80742404 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -52,7 +52,7 @@ profile-boot-duration = 2s // default-transition // animation cover-animation profile-boot-duration // animation-fill-mode forwards - filter brightness(35%) blur(0) + filter brightness(30%) blur(0) // animation cover-animation // 0% From db28721126347c330bef1c3f402fc3ee776357dc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 26 Jun 2017 23:04:15 +0200 Subject: [PATCH 005/527] Can now reply to threads --- pages/threads/threads.pixy | 2 +- scripts/AnimeNotifier.ts | 4 ++-- scripts/actions.ts | 26 +++++++++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pages/threads/threads.pixy b/pages/threads/threads.pixy index d2dc6514..0ebee69b 100644 --- a/pages/threads/threads.pixy +++ b/pages/threads/threads.pixy @@ -1,7 +1,7 @@ component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User) h2.thread-title= thread.Title - .thread + #thread.thread(data-id=thread.ID) .posts Postable(thread.ToPostable(), thread.Author().ID) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index d0fc6907..fdc2f926 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -153,7 +153,7 @@ export class AnimeNotifier { } } - diffURL(url: string) { + load(url: string) { let request = fetch("/_" + url, { credentials: "same-origin" }) @@ -165,7 +165,7 @@ export class AnimeNotifier { this.unmountMountables() this.loading(true) - delay(300).then(() => { + return delay(300).then(() => { request .then(html => this.app.setContent(html, true)) .then(() => this.app.markActiveLinks()) diff --git a/scripts/actions.ts b/scripts/actions.ts index 5fc88c11..3d9bdd1b 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -60,15 +60,35 @@ 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") - arn.diffURL(url) + arn.load(url) } // Forum reply export function forumReply(arn: AnimeNotifier) { let textarea = arn.app.find("new-reply") as HTMLTextAreaElement + let thread = arn.app.find("thread") - console.log(textarea.value) - arn.diffURL(arn.app.currentPath) + let post = { + text: textarea.value, + threadId: thread.dataset.id, + tags: [] + } + + fetch("/api/post/new", { + method: "POST", + body: JSON.stringify(post), + credentials: "same-origin" + }) + .then(response => response.text()) + .then(body => { + if(body !== "ok") { + throw body + } + + textarea.value = "" + }) + .then(() => arn.reloadContent()) + .catch(console.error) } // Search From e7d914f22386f2956f29b549b6812481707728be Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 26 Jun 2017 23:41:16 +0200 Subject: [PATCH 006/527] Improved forum database --- layout/layout.pixy | 3 +-- mixins/ThreadLink.pixy | 2 +- pages/threads/threads.go | 32 +++++++++++++++----------------- patches/post-texts/main.go | 29 +++++++++++++++++++++++++++++ patches/thread-posts/main.go | 34 ++++++++++++++++++++++++++++++++++ styles/include/config.scarlet | 3 +++ styles/loading.scarlet | 2 +- 7 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 patches/post-texts/main.go create mode 100644 patches/thread-posts/main.go diff --git a/layout/layout.pixy b/layout/layout.pixy index 65523533..cdcee99a 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -12,8 +12,7 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, conte Navigation(user) #content-container main#content.fade!= content - - LoadingAnimation + LoadingAnimation script(src="/scripts") component LoadingAnimation diff --git a/mixins/ThreadLink.pixy b/mixins/ThreadLink.pixy index 2c0a7952..ce038fc6 100644 --- a/mixins/ThreadLink.pixy +++ b/mixins/ThreadLink.pixy @@ -8,6 +8,6 @@ component ThreadLink(thread *arn.Thread) Icon("thumb-tack") a.thread-link-title.ajax(href="/threads/" + thread.ID)= thread.Title .spacer - .thread-reply-count= thread.Replies + .thread-reply-count= len(thread.Posts) .thread-icons Icon(arn.GetForumIcon(thread.Tags[0])) \ No newline at end of file diff --git a/pages/threads/threads.go b/pages/threads/threads.go index 92d96248..b9b7977e 100644 --- a/pages/threads/threads.go +++ b/pages/threads/threads.go @@ -1,7 +1,7 @@ package threads import ( - "strings" + "net/http" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -12,28 +12,26 @@ import ( // Get thread. func Get(ctx *aero.Context) string { id := ctx.Get("id") - thread, err := arn.GetThread(id) user := utils.GetUser(ctx) + // Fetch thread + thread, err := arn.GetThread(id) + if err != nil { - return ctx.Error(404, "Thread not found", err) + return ctx.Error(http.StatusNotFound, "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 - }) + // Fetch posts + postObjects, getErr := arn.DB.GetMany("Post", thread.Posts) - 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) + if getErr != nil { + return ctx.Error(http.StatusInternalServerError, "Could not retrieve posts", getErr) } - return ctx.HTML(components.Thread(thread, replies, user)) + posts := postObjects.([]*arn.Post) + + // Sort posts + arn.SortPostsLatestLast(posts) + + return ctx.HTML(components.Thread(thread, posts, user)) } diff --git a/patches/post-texts/main.go b/patches/post-texts/main.go new file mode 100644 index 00000000..540eaca9 --- /dev/null +++ b/patches/post-texts/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + // Get a stream of all posts + allPosts, err := arn.AllPosts() + arn.PanicOnError(err) + + // Iterate over the stream + for post := range allPosts { + // Fix text + color.Yellow(post.Text) + post.Text = arn.FixPostText(post.Text) + color.Green(post.Text) + + // Tags + if post.Tags == nil { + post.Tags = []string{} + } + + // Save + err = post.Save() + arn.PanicOnError(err) + } +} diff --git a/patches/thread-posts/main.go b/patches/thread-posts/main.go new file mode 100644 index 00000000..dbaf8dd4 --- /dev/null +++ b/patches/thread-posts/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + // Get a stream of all posts + allPosts, err := arn.AllPosts() + arn.PanicOnError(err) + + threadToPosts := make(map[string][]string) + + // Iterate over the stream + for post := range allPosts { + _, found := threadToPosts[post.ThreadID] + + if !found { + threadToPosts[post.ThreadID] = []string{post.ID} + } else { + threadToPosts[post.ThreadID] = append(threadToPosts[post.ThreadID], post.ID) + } + } + + // Save new post ID lists + for threadID, posts := range threadToPosts { + thread, err := arn.GetThread(threadID) + arn.PanicOnError(err) + + thread.Posts = posts + err = thread.Save() + arn.PanicOnError(err) + } +} diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 7359ff69..2e5732d8 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -40,6 +40,9 @@ nav-link-hover-slide-color = rgb(248, 165, 130) // nav-link-color = rgb(160, 160, 160) // nav-link-hover-color = rgb(80, 80, 80) +// Loading animation +loading-anim-color = nav-link-hover-slide-color + // Shadow shadow-light = 4px 4px 8px rgba(0, 0, 0, 0.05) shadow-medium = 6px 6px 12px rgba(0, 0, 0, 0.13) diff --git a/styles/loading.scarlet b/styles/loading.scarlet index 22b532b5..8169c95b 100644 --- a/styles/loading.scarlet +++ b/styles/loading.scarlet @@ -17,7 +17,7 @@ loading-anim-size = 24px .sk-cube width 33.3% height 33.3% - background-color main-color + background-color loading-anim-color opacity 0.7 border-radius 100% animation sk-pulse loading-anim-duration infinite linear From 8c631f4e26ee63f2445d5f27e26f5b7d367c0c7f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 00:06:56 +0200 Subject: [PATCH 007/527] Improved dashboard --- pages/dashboard/dashboard.go | 5 +---- pages/dashboard/dashboard.pixy | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 6246dee2..365d1b88 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -36,10 +36,7 @@ func dashboard(ctx *aero.Context) string { flow.Parallel(func() { posts, err = arn.AllPostsSlice() arn.SortPostsLatestFirst(posts) - - if len(posts) > maxPosts { - posts = posts[:maxPosts] - } + posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) }, func() { followIDList = user.Following userList, err = arn.DB.GetMany("User", followIDList) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 7fee6c6f..c47614f5 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -15,7 +15,7 @@ component Dashboard(posts []*arn.Post, following []*arn.User) h3.widget-title Forums each post in posts - a.widget-element.ajax(href=post.Link()) + a.widget-element.ajax(href=post.Thread().Link()) .widget-element-text Icon(arn.GetForumIcon(post.Thread().Tags[0])) span= post.Thread().Title From 7c5d0bbcff8a342d91fb14c49683c05afea418cb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 00:14:47 +0200 Subject: [PATCH 008/527] Improved dashboard --- pages/dashboard/dashboard.go | 4 +--- pages/dashboard/dashboard.pixy | 17 +++++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 365d1b88..f8774e06 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -27,7 +27,6 @@ func Get(ctx *aero.Context) string { func dashboard(ctx *aero.Context) string { var posts []*arn.Post var err error - var followIDList []string var userList interface{} var followingList []*arn.User @@ -38,8 +37,7 @@ func dashboard(ctx *aero.Context) string { arn.SortPostsLatestFirst(posts) posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) }, func() { - followIDList = user.Following - userList, err = arn.DB.GetMany("User", followIDList) + userList, err = arn.DB.GetMany("User", user.Following) followingList = userList.([]*arn.User) followingList = arn.SortUsersLastSeen(followingList) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index c47614f5..1ad95cac 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -38,15 +38,20 @@ component Dashboard(posts []*arn.Post, following []*arn.User) Icon("comment") span ... - if len(following) > 0 - .widget.mountable - h3.widget-title Contacts + .widget.mountable + h3.widget-title Contacts - each user in following - a.widget-element.ajax(href="/+" + user.Nick) + for i := 0; i <= 4; i++ + if i < len(following) + a.widget-element.ajax(href="/+" + following[i].Nick) .widget-element-text Icon("address-card") - span= user.Nick + span= following[i].Nick + else + .widget-element + .widget-element-text + Icon("address-card") + span ... .widget.mountable h3.widget-title Follow From 81720d71980a2bdda55cb41fa0165b7deea15d25 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 00:35:46 +0200 Subject: [PATCH 009/527] Added proper post highlight --- mixins/Postable.pixy | 2 +- pages/dashboard/dashboard.pixy | 2 +- pages/threads/threads.scarlet | 4 ++++ styles/include/config.scarlet | 9 +++++---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index 8c165a6d..baf09b73 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -1,5 +1,5 @@ component Postable(post arn.Postable, highlightAuthorID string) - .post.mountable(data-highlight=post.Author().ID == highlightAuthorID) + .post.mountable(id=post.ID(), data-highlight=post.Author().ID == highlightAuthorID) .post-author Avatar(post.Author()) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 1ad95cac..22a71b50 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -15,7 +15,7 @@ component Dashboard(posts []*arn.Post, following []*arn.User) h3.widget-title Forums each post in posts - a.widget-element.ajax(href=post.Thread().Link()) + a.widget-element.ajax(href=post.Thread().Link() + "#" + post.ID) .widget-element-text Icon(arn.GetForumIcon(post.Thread().Tags[0])) span= post.Thread().Title diff --git a/pages/threads/threads.scarlet b/pages/threads/threads.scarlet index f6bf9857..da44d2d9 100644 --- a/pages/threads/threads.scarlet +++ b/pages/threads/threads.scarlet @@ -13,6 +13,10 @@ .post-author margin-bottom 0.25rem + + [data-highlight="true"] + .post-content + border 2px solid post-highlight-color > 600px .post diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 2e5732d8..1ca8a328 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -1,10 +1,10 @@ // Colors text-color = rgb(60, 60, 60) -main-color = rgb(215, 38, 15) -link-color = main-color +main-color = rgb(248, 165, 130) +link-color = rgb(215, 38, 15) link-hover-color = rgb(242, 60, 30) link-active-color = link-hover-color -post-highlight-color = rgba(248, 165, 130, 0.7) + bg-color = rgb(246, 246, 246) // UI @@ -27,6 +27,7 @@ forum-tag-hover-color = rgb(46, 85, 160) // Forum forum-width = 830px +post-highlight-color = rgba(248, 165, 130, 0.7) // Avatar avatar-size = 50px @@ -35,7 +36,7 @@ avatar-size = 50px 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-link-hover-slide-color = main-color // nav-color = rgb(245, 245, 245) // nav-link-color = rgb(160, 160, 160) // nav-link-hover-color = rgb(80, 80, 80) From 4d24b817ff630d40ded6b54acb1843888c99ea8c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 03:05:52 +0200 Subject: [PATCH 010/527] Updated to latest ARN version --- mixins/ThreadLink.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/ThreadLink.pixy b/mixins/ThreadLink.pixy index ce038fc6..6d6bbb87 100644 --- a/mixins/ThreadLink.pixy +++ b/mixins/ThreadLink.pixy @@ -4,7 +4,7 @@ component ThreadLink(thread *arn.Thread) Avatar(thread.Author()) .thread-content-container .thread-content - if thread.Sticky + if thread.Sticky != 0 Icon("thumb-tack") a.thread-link-title.ajax(href="/threads/" + thread.ID)= thread.Title .spacer From 8758baa0face9eab01c5efc1f9d555ae5244c867 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 04:15:52 +0200 Subject: [PATCH 011/527] Create new thread --- main.go | 2 ++ pages/forum/forum.pixy | 8 +++++-- pages/forum/forum.scarlet | 7 ++++-- pages/newthread/newthread.go | 20 +++++++++++++++++ pages/newthread/newthread.pixy | 20 +++++++++++++++++ scripts/AnimeNotifier.ts | 16 +++++++++++++- scripts/actions.ts | 40 ++++++++++++++++++++++------------ 7 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 pages/newthread/newthread.go create mode 100644 pages/newthread/newthread.pixy diff --git a/main.go b/main.go index c2993c4e..e97f2b24 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/login" + "github.com/animenotifier/notify.moe/pages/newthread" "github.com/animenotifier/notify.moe/pages/popularanime" "github.com/animenotifier/notify.moe/pages/posts" "github.com/animenotifier/notify.moe/pages/profile" @@ -64,6 +65,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/posts", profile.GetPostsByUser) app.Ajax("/user/:nick/animelist", animelist.Get) app.Ajax("/user/:nick/animelist/:id", animelistitem.Get) + app.Ajax("/new/thread", newthread.Get) app.Ajax("/settings", settings.Get) app.Ajax("/admin", admin.Get) app.Ajax("/search/:term", search.Get) diff --git a/pages/forum/forum.pixy b/pages/forum/forum.pixy index dd4f9ee1..a248d5ed 100644 --- a/pages/forum/forum.pixy +++ b/pages/forum/forum.pixy @@ -4,8 +4,12 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) .forum ThreadList(threads) - if len(threads) == threadsPerPage - .buttons + + .buttons + button#new-thread.action(data-action="load", data-trigger="click", data-url="/new/thread") + Icon("plus") + span New thread + if len(threads) == threadsPerPage button Icon("refresh") span Load more diff --git a/pages/forum/forum.scarlet b/pages/forum/forum.scarlet index 4775f365..79f1ee73 100644 --- a/pages/forum/forum.scarlet +++ b/pages/forum/forum.scarlet @@ -29,5 +29,8 @@ .forum-tag-text display none -#load-more-threads - margin-top 1rem \ No newline at end of file +> 1250px + #new-thread + position fixed + right content-padding + bottom content-padding diff --git a/pages/newthread/newthread.go b/pages/newthread/newthread.go new file mode 100644 index 00000000..614af029 --- /dev/null +++ b/pages/newthread/newthread.go @@ -0,0 +1,20 @@ +package newthread + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get forums page. +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + return ctx.HTML(components.NewThread(user)) +} diff --git a/pages/newthread/newthread.pixy b/pages/newthread/newthread.pixy new file mode 100644 index 00000000..bd83d888 --- /dev/null +++ b/pages/newthread/newthread.pixy @@ -0,0 +1,20 @@ +component NewThread(user *arn.User) + .widgets + .widget + input#title.widget-element(type="text", placeholder="Title") + + textarea#text.widget-element(placeholder="Content") + + select#tag.widget-element + option(value="general") General + option(value="news") News + option(value="anime") Anime + option(value="bug") Bug + option(value="suggestion") Suggestion + + if user.Role == "admin" + option(value="update") Update + + button.action(data-action="createThread", data-trigger="click") + Icon("check") + span Create thread \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index fdc2f926..56052c01 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -153,7 +153,7 @@ export class AnimeNotifier { } } - load(url: string) { + diff(url: string) { let request = fetch("/_" + url, { credentials: "same-origin" }) @@ -175,6 +175,20 @@ export class AnimeNotifier { }) } + post(url, obj) { + return fetch(url, { + method: "POST", + body: JSON.stringify(obj), + credentials: "same-origin" + }) + .then(response => response.text()) + .then(body => { + if(body !== "ok") { + throw body + } + }) + } + onPopState(e: PopStateEvent) { if(e.state) { this.app.load(e.state, { diff --git a/scripts/actions.ts b/scripts/actions.ts index 3d9bdd1b..c575d55a 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -57,10 +57,16 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE }) } +// Load +export function load(arn: AnimeNotifier, element: HTMLElement) { + let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") + arn.app.load(url) +} + // Diff export function diff(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") - arn.load(url) + arn.diff(url) } // Forum reply @@ -74,20 +80,26 @@ export function forumReply(arn: AnimeNotifier) { tags: [] } - fetch("/api/post/new", { - method: "POST", - body: JSON.stringify(post), - credentials: "same-origin" - }) - .then(response => response.text()) - .then(body => { - if(body !== "ok") { - throw body - } - - textarea.value = "" - }) + arn.post("/api/post/new", post) .then(() => arn.reloadContent()) + .then(() => textarea.value = "") + .catch(console.error) +} + +// Create thread +export function createThread(arn: AnimeNotifier) { + let title = arn.app.find("title") as HTMLInputElement + let text = arn.app.find("text") as HTMLTextAreaElement + let category = arn.app.find("tag") as HTMLInputElement + + let thread = { + title: title.value, + text: text.value, + tags: [category.value] + } + + arn.post("/api/thread/new", thread) + .then(() => arn.app.load("/forum/" + thread.tags[0])) .catch(console.error) } From 731266cbea07d4d0585d41e04e52a97009adbb79 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 04:28:17 +0200 Subject: [PATCH 012/527] Improved HTML lists --- styles/list.scarlet | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 styles/list.scarlet diff --git a/styles/list.scarlet b/styles/list.scarlet new file mode 100644 index 00000000..646b4bc2 --- /dev/null +++ b/styles/list.scarlet @@ -0,0 +1,4 @@ +li + list-style-type disc + list-style-position outside + margin-left 2rem \ No newline at end of file From 93e7ad01d59684ffbb401b8f02adb24975a68ac0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 04:33:24 +0200 Subject: [PATCH 013/527] Hide reply button when not logged in --- pages/threads/threads.pixy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/threads/threads.pixy b/pages/threads/threads.pixy index 0ebee69b..9a1014bb 100644 --- a/pages/threads/threads.pixy +++ b/pages/threads/threads.pixy @@ -17,7 +17,7 @@ component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User) .post-content textarea#new-reply(placeholder="Reply...") - .buttons - button.action(data-action="forumReply", data-trigger="click") - Icon("mail-reply") - span Reply \ No newline at end of file + .buttons + button.action(data-action="forumReply", data-trigger="click") + Icon("mail-reply") + span Reply \ No newline at end of file From 04373033ccef6e3c534a089161543c31f79dee7b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 12:39:41 +0200 Subject: [PATCH 014/527] Updated to latest ARN API --- jobs/popular-anime/main.go | 8 +------- jobs/search-index/main.go | 2 +- jobs/sync-anime/main.go | 2 +- main.go | 3 +++ mixins/Navigation.pixy | 2 +- pages/dashboard/dashboard.go | 6 +++++- pages/dashboard/dashboard.pixy | 2 +- pages/music/music.go | 8 ++++++++ patches/post-texts/main.go | 2 +- patches/thread-posts/main.go | 2 +- 10 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 pages/music/music.go diff --git a/jobs/popular-anime/main.go b/jobs/popular-anime/main.go index c90015f2..9a1a0143 100644 --- a/jobs/popular-anime/main.go +++ b/jobs/popular-anime/main.go @@ -14,7 +14,7 @@ const maxPopularAnime = 10 func main() { color.Yellow("Caching popular anime") - animeChan, err := arn.AllAnime() + animeList, err := arn.AllAnime() if err != nil { color.Red("Failed fetching anime channel") @@ -22,12 +22,6 @@ func main() { return } - var animeList []*arn.Anime - - for anime := range animeChan { - animeList = append(animeList, anime) - } - sort.Slice(animeList, func(i, j int) bool { return animeList[i].Rating.Overall > animeList[j].Rating.Overall }) diff --git a/jobs/search-index/main.go b/jobs/search-index/main.go index aa98d388..d4b90078 100644 --- a/jobs/search-index/main.go +++ b/jobs/search-index/main.go @@ -21,7 +21,7 @@ func updateAnimeIndex() { animeSearchIndex := arn.NewSearchIndex() // Anime - animeStream, err := arn.AllAnime() + animeStream, err := arn.StreamAnime() if err != nil { panic(err) diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index ea44093f..abc45851 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -15,7 +15,7 @@ func main() { color.Yellow("Syncing Anime") // Get a stream of all anime - allAnime := kitsu.AllAnime() + allAnime := kitsu.StreamAnime() // Iterate over the stream for anime := range allAnime { diff --git a/main.go b/main.go index e97f2b24..301f8874 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/login" + "github.com/animenotifier/notify.moe/pages/music" "github.com/animenotifier/notify.moe/pages/newthread" "github.com/animenotifier/notify.moe/pages/popularanime" "github.com/animenotifier/notify.moe/pages/posts" @@ -67,7 +68,9 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/animelist/:id", animelistitem.Get) app.Ajax("/new/thread", newthread.Get) app.Ajax("/settings", settings.Get) + app.Ajax("/music", music.Get) app.Ajax("/admin", admin.Get) + app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) app.Ajax("/users", users.Get) app.Ajax("/login", login.Get) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index a8c01021..156546c2 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -26,7 +26,7 @@ component LoggedInMenu(user *arn.User) NavigationButton("Dash", "/", "dashboard") NavigationButton("Profile", "/+", "user") NavigationButton("Forum", "/forum", "comment") - NavigationButton("Anime", "/anime", "television") + NavigationButton("Music", "/music", "music") FuzzySearch diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index f8774e06..03366114 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -33,9 +33,13 @@ func dashboard(ctx *aero.Context) string { user := utils.GetUser(ctx) flow.Parallel(func() { - posts, err = arn.AllPostsSlice() + posts, err = arn.AllPosts() arn.SortPostsLatestFirst(posts) posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) + }, func() { + // threads, err = arn.AllThreadsSlice() + // arn.SortPostsLatestFirst(posts) + // posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) }, func() { userList, err = arn.DB.GetMany("User", user.Following) followingList = userList.([]*arn.User) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 22a71b50..1ad95cac 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -15,7 +15,7 @@ component Dashboard(posts []*arn.Post, following []*arn.User) h3.widget-title Forums each post in posts - a.widget-element.ajax(href=post.Thread().Link() + "#" + post.ID) + a.widget-element.ajax(href=post.Thread().Link()) .widget-element-text Icon(arn.GetForumIcon(post.Thread().Tags[0])) span= post.Thread().Title diff --git a/pages/music/music.go b/pages/music/music.go new file mode 100644 index 00000000..490a9930 --- /dev/null +++ b/pages/music/music.go @@ -0,0 +1,8 @@ +package music + +import "github.com/aerogo/aero" + +// Get renders the music page. +func Get(ctx *aero.Context) string { + return ctx.HTML("Coming soon.") +} diff --git a/patches/post-texts/main.go b/patches/post-texts/main.go index 540eaca9..c277a66e 100644 --- a/patches/post-texts/main.go +++ b/patches/post-texts/main.go @@ -7,7 +7,7 @@ import ( func main() { // Get a stream of all posts - allPosts, err := arn.AllPosts() + allPosts, err := arn.StreamPosts() arn.PanicOnError(err) // Iterate over the stream diff --git a/patches/thread-posts/main.go b/patches/thread-posts/main.go index dbaf8dd4..c668ab73 100644 --- a/patches/thread-posts/main.go +++ b/patches/thread-posts/main.go @@ -6,7 +6,7 @@ import ( func main() { // Get a stream of all posts - allPosts, err := arn.AllPosts() + allPosts, err := arn.StreamPosts() arn.PanicOnError(err) threadToPosts := make(map[string][]string) From 50fdc64bc54693c1e2daa9f1407dba7287bf0ae7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 12:46:56 +0200 Subject: [PATCH 015/527] Disable new thread page test --- tests.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests.go b/tests.go index 36624929..f1f1f0ac 100644 --- a/tests.go +++ b/tests.go @@ -120,6 +120,7 @@ var tests = map[string][]string{ // Disable "/auth/google": nil, "/auth/google/callback": nil, + "/new/thread": nil, "/user": nil, "/settings": nil, "/extension/embed": nil, From 57fda44d66278f21201b365403d54377cf5f2abc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 13:06:19 +0200 Subject: [PATCH 016/527] Added music page --- mixins/Navigation.pixy | 6 +++--- pages/music/music.go | 3 ++- pages/music/music.pixy | 5 +++++ styles/icons.scarlet | 5 ++++- utils/icons.go | 4 ++-- 5 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 pages/music/music.pixy diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 156546c2..dbf99a82 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -7,7 +7,7 @@ component Navigation(user *arn.User) component LoggedOutMenu nav#navigation.logged-out NavigationButton("About", "/", "question-circle") - NavigationButton("Anime", "/anime", "television") + NavigationButton("Music", "/music", "headphones") NavigationButton("Forum", "/forum", "comment") FuzzySearch @@ -26,7 +26,7 @@ component LoggedInMenu(user *arn.User) NavigationButton("Dash", "/", "dashboard") NavigationButton("Profile", "/+", "user") NavigationButton("Forum", "/forum", "comment") - NavigationButton("Music", "/music", "music") + NavigationButton("Music", "/music", "headphones") FuzzySearch @@ -37,7 +37,7 @@ component LoggedInMenu(user *arn.User) NavigationButton("Airing", "/airing", "th") NavigationButton("Settings", "/settings", "cog") - + .extra-navigation NavigationButtonNoAJAX("Logout", "/logout", "sign-out") diff --git a/pages/music/music.go b/pages/music/music.go index 490a9930..b2d0d24a 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -1,8 +1,9 @@ package music import "github.com/aerogo/aero" +import "github.com/animenotifier/notify.moe/components" // Get renders the music page. func Get(ctx *aero.Context) string { - return ctx.HTML("Coming soon.") + return ctx.HTML(components.Music()) } diff --git a/pages/music/music.pixy b/pages/music/music.pixy new file mode 100644 index 00000000..046d14fa --- /dev/null +++ b/pages/music/music.pixy @@ -0,0 +1,5 @@ +component Music + h2.page-title Music + + iframe(src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/127672476&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true") + //- \ No newline at end of file diff --git a/styles/icons.scarlet b/styles/icons.scarlet index dd1085d2..9cfeed76 100644 --- a/styles/icons.scarlet +++ b/styles/icons.scarlet @@ -10,4 +10,7 @@ width 1em height 1em min-width 1em - fill currentColor \ No newline at end of file + fill currentColor + +.icon-headphones + transform translateY(2px) \ No newline at end of file diff --git a/utils/icons.go b/utils/icons.go index f830531e..87886781 100644 --- a/utils/icons.go +++ b/utils/icons.go @@ -11,9 +11,9 @@ func init() { files, _ := ioutil.ReadDir("images/icons/") for _, file := range files { - name := strings.Replace(file.Name(), ".svg", "", 1) + name := strings.TrimSuffix(file.Name(), ".svg") data, _ := ioutil.ReadFile("images/icons/" + name + ".svg") - svgIcons[name] = strings.Replace(string(data), " Date: Tue, 27 Jun 2017 13:46:29 +0200 Subject: [PATCH 017/527] Improved music page --- jobs/sync-anime/main.go | 8 ++++---- pages/anime/anime.pixy | 4 ++-- pages/music/music.go | 31 ++++++++++++++++++++++++++++++- pages/music/music.pixy | 12 +++++++++--- pages/music/music.scarlet | 15 +++++++++++++++ 5 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 pages/music/music.scarlet diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index abc45851..7c87ddee 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -59,12 +59,12 @@ func sync(data *kitsu.Anime) { anime.Rating.Overall = overall // Trailers - anime.Trailers = []arn.AnimeTrailer{} + anime.Trailers = []arn.ExternalMedia{} if attr.YoutubeVideoID != "" { - anime.Trailers = append(anime.Trailers, arn.AnimeTrailer{ - Service: "Youtube", - VideoID: attr.YoutubeVideoID, + anime.Trailers = append(anime.Trailers, arn.ExternalMedia{ + Service: "Youtube", + ServiceID: attr.YoutubeVideoID, }) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 4ef007d7..0be1b18b 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -45,10 +45,10 @@ component Anime(anime *arn.Anime, user *arn.User) .anime-rating-category-name Soundtrack 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].ServiceID != "" h3.anime-section-name Video .anime-trailer.video-container - iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].VideoID + "?showinfo=0", allowfullscreen="allowfullscreen") + iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") //- if anime.Tracks != nil && anime.Tracks.Opening != nil //- h3.anime-section-name Tracks diff --git a/pages/music/music.go b/pages/music/music.go index b2d0d24a..64bdbb4f 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -2,8 +2,37 @@ package music import "github.com/aerogo/aero" import "github.com/animenotifier/notify.moe/components" +import "github.com/animenotifier/arn" // Get renders the music page. func Get(ctx *aero.Context) string { - return ctx.HTML(components.Music()) + tracks := []*arn.SoundTrack{} + + tracks = append(tracks, &arn.SoundTrack{ + ID: "1", + Media: []arn.ExternalMedia{ + arn.ExternalMedia{ + Service: "Soundcloud", + ServiceID: "127672476", + }, + }, + Tags: []string{ + "anime:7622", + }, + }) + + tracks = append(tracks, &arn.SoundTrack{ + ID: "2", + Media: []arn.ExternalMedia{ + arn.ExternalMedia{ + Service: "Soundcloud", + ServiceID: "270777538", + }, + }, + Tags: []string{ + "anime:11469", + }, + }) + + return ctx.HTML(components.Music(tracks)) } diff --git a/pages/music/music.pixy b/pages/music/music.pixy index 046d14fa..0276ddd6 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -1,5 +1,11 @@ -component Music +component Music(tracks []*arn.SoundTrack) h2.page-title Music - iframe(src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/127672476&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true") - //- \ No newline at end of file + .sound-tracks + each track in tracks + .sound-track + each anime in track.Anime() + a.sound-track-anime-link.ajax(href="/anime/" + anime.ID) + img.sound-track-anime-image(src=anime.Image.Small, alt=anime.Title.Canonical, title=anime.Title.Canonical) + + iframe(src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet new file mode 100644 index 00000000..e3b59811 --- /dev/null +++ b/pages/music/music.scarlet @@ -0,0 +1,15 @@ +.sound-tracks + vertical + +.sound-track + horizontal + margin-bottom 1rem + + iframe + width 100% + +.sound-track-anime-link + // + +.sound-track-anime-image + max-width 142px \ No newline at end of file From 976c964559f0dccb677478ff05e5768dacfbe4f8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 14:01:32 +0200 Subject: [PATCH 018/527] Improved music page --- pages/music/music.go | 18 +++++++++++++++--- pages/music/music.pixy | 8 +++++--- pages/music/music.scarlet | 9 ++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pages/music/music.go b/pages/music/music.go index 64bdbb4f..061a1d5a 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -1,8 +1,12 @@ package music -import "github.com/aerogo/aero" -import "github.com/animenotifier/notify.moe/components" -import "github.com/animenotifier/arn" +import ( + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) // Get renders the music page. func Get(ctx *aero.Context) string { @@ -19,6 +23,8 @@ func Get(ctx *aero.Context) string { Tags: []string{ "anime:7622", }, + Created: arn.DateTimeUTC(), + CreatedBy: "4J6qpK1ve", }) tracks = append(tracks, &arn.SoundTrack{ @@ -32,6 +38,12 @@ func Get(ctx *aero.Context) string { Tags: []string{ "anime:11469", }, + Created: arn.DateTimeUTC(), + CreatedBy: "4J6qpK1ve", + }) + + sort.Slice(tracks, func(i, j int) bool { + return tracks[i].Created > tracks[j].Created }) return ctx.HTML(components.Music(tracks)) diff --git a/pages/music/music.pixy b/pages/music/music.pixy index 0276ddd6..0d291360 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -4,8 +4,10 @@ component Music(tracks []*arn.SoundTrack) .sound-tracks each track in tracks .sound-track - each anime in track.Anime() - a.sound-track-anime-link.ajax(href="/anime/" + anime.ID) - img.sound-track-anime-image(src=anime.Image.Small, alt=anime.Title.Canonical, title=anime.Title.Canonical) + a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) + img.sound-track-anime-image(src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) iframe(src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") + .sound-track-footer + span posted by + a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet index e3b59811..f648ed36 100644 --- a/pages/music/music.scarlet +++ b/pages/music/music.scarlet @@ -3,11 +3,18 @@ .sound-track horizontal - margin-bottom 1rem iframe width 100% +.sound-track-footer + text-align right + margin-bottom content-padding + font-size 0.9em + + span + opacity 0.65 + .sound-track-anime-link // From 2b471b871d071f153de8af012c20053e6c15156f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 14:04:25 +0200 Subject: [PATCH 019/527] Limit tracks --- pages/music/music.go | 6 ++++++ pages/music/music.scarlet | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pages/music/music.go b/pages/music/music.go index 061a1d5a..66ced1d2 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -8,6 +8,8 @@ import ( "github.com/animenotifier/notify.moe/components" ) +const maxTracks = 10 + // Get renders the music page. func Get(ctx *aero.Context) string { tracks := []*arn.SoundTrack{} @@ -42,6 +44,10 @@ func Get(ctx *aero.Context) string { CreatedBy: "4J6qpK1ve", }) + if len(tracks) > maxTracks { + tracks = tracks[:maxTracks] + } + sort.Slice(tracks, func(i, j int) bool { return tracks[i].Created > tracks[j].Created }) diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet index f648ed36..66ab4050 100644 --- a/pages/music/music.scarlet +++ b/pages/music/music.scarlet @@ -9,7 +9,7 @@ .sound-track-footer text-align right - margin-bottom content-padding + margin-bottom 1rem font-size 0.9em span From ab253000ef1d5e8cbfa2b18e2612671fc0d813df Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 14:38:36 +0200 Subject: [PATCH 020/527] Updated API --- jobs/search-index/main.go | 2 +- pages/music/music.go | 60 ++++++++++++++++++++++++++ pages/music/music.pixy | 4 +- pages/music/music.scarlet | 2 + patches/add-anime-lists/main.go | 2 +- patches/add-last-seen/main.go | 2 +- patches/delete-private-data/main.go | 2 +- patches/user-references/main.go | 2 +- patches/video-id-to-service-id/main.go | 34 +++++++++++++++ 9 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 patches/video-id-to-service-id/main.go diff --git a/jobs/search-index/main.go b/jobs/search-index/main.go index d4b90078..0033b956 100644 --- a/jobs/search-index/main.go +++ b/jobs/search-index/main.go @@ -51,7 +51,7 @@ func updateUserIndex() { userSearchIndex := arn.NewSearchIndex() // Users - userStream, err := arn.AllUsers() + userStream, err := arn.StreamUsers() if err != nil { panic(err) diff --git a/pages/music/music.go b/pages/music/music.go index 66ced1d2..9ce4719c 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -14,6 +14,21 @@ const maxTracks = 10 func Get(ctx *aero.Context) string { tracks := []*arn.SoundTrack{} + tracks = append(tracks, &arn.SoundTrack{ + ID: "0", + Media: []arn.ExternalMedia{ + arn.ExternalMedia{ + Service: "Soundcloud", + ServiceID: "145918628", + }, + }, + Tags: []string{ + "anime:2357", + }, + Created: arn.DateTimeUTC(), + CreatedBy: "4J6qpK1ve", + }) + tracks = append(tracks, &arn.SoundTrack{ ID: "1", Media: []arn.ExternalMedia{ @@ -44,6 +59,51 @@ func Get(ctx *aero.Context) string { CreatedBy: "4J6qpK1ve", }) + tracks = append(tracks, &arn.SoundTrack{ + ID: "3", + Media: []arn.ExternalMedia{ + arn.ExternalMedia{ + Service: "Soundcloud", + ServiceID: "243839100", + }, + }, + Tags: []string{ + "anime:9962", + }, + Created: arn.DateTimeUTC(), + CreatedBy: "4J6qpK1ve", + }) + + tracks = append(tracks, &arn.SoundTrack{ + ID: "3", + Media: []arn.ExternalMedia{ + arn.ExternalMedia{ + Service: "Soundcloud", + ServiceID: "207355237", + }, + }, + Tags: []string{ + "anime:6589", + }, + Created: arn.DateTimeUTC(), + CreatedBy: "4J6qpK1ve", + }) + + tracks = append(tracks, &arn.SoundTrack{ + ID: "3", + Media: []arn.ExternalMedia{ + arn.ExternalMedia{ + Service: "Soundcloud", + ServiceID: "242172944", + }, + }, + Tags: []string{ + "anime:10740", + }, + Created: arn.DateTimeUTC(), + CreatedBy: "4J6qpK1ve", + }) + if len(tracks) > maxTracks { tracks = tracks[:maxTracks] } diff --git a/pages/music/music.pixy b/pages/music/music.pixy index 0d291360..f2b60d80 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -5,9 +5,9 @@ component Music(tracks []*arn.SoundTrack) each track in tracks .sound-track a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) - img.sound-track-anime-image(src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) + img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - iframe(src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") + iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") .sound-track-footer span posted by a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet index 66ab4050..c6170788 100644 --- a/pages/music/music.scarlet +++ b/pages/music/music.scarlet @@ -6,6 +6,8 @@ iframe width 100% + + box-shadow shadow-light .sound-track-footer text-align right diff --git a/patches/add-anime-lists/main.go b/patches/add-anime-lists/main.go index 51eb9275..88776a6f 100644 --- a/patches/add-anime-lists/main.go +++ b/patches/add-anime-lists/main.go @@ -11,7 +11,7 @@ func main() { color.Yellow("Adding empty anime lists to users who don't have one") // Get a stream of all users - allUsers, err := arn.AllUsers() + allUsers, err := arn.StreamUsers() if err != nil { panic(err) diff --git a/patches/add-last-seen/main.go b/patches/add-last-seen/main.go index ca8fcc3c..d2c3f228 100644 --- a/patches/add-last-seen/main.go +++ b/patches/add-last-seen/main.go @@ -6,7 +6,7 @@ import ( func main() { // Get a stream of all users - allUsers, err := arn.AllUsers() + allUsers, err := arn.StreamUsers() if err != nil { panic(err) diff --git a/patches/delete-private-data/main.go b/patches/delete-private-data/main.go index 51a134a0..94df6456 100644 --- a/patches/delete-private-data/main.go +++ b/patches/delete-private-data/main.go @@ -6,7 +6,7 @@ func main() { // color.Yellow("Deleting private user data") // // Get a stream of all users - // allUsers, err := arn.AllUsers() + // allUsers, err := arn.StreamUsers() // if err != nil { // panic(err) diff --git a/patches/user-references/main.go b/patches/user-references/main.go index 46ad2777..31a7d454 100644 --- a/patches/user-references/main.go +++ b/patches/user-references/main.go @@ -13,7 +13,7 @@ func main() { arn.DB.DeleteTable("GoogleToUser") // Get a stream of all users - allUsers, err := arn.AllUsers() + allUsers, err := arn.StreamUsers() if err != nil { panic(err) diff --git a/patches/video-id-to-service-id/main.go b/patches/video-id-to-service-id/main.go new file mode 100644 index 00000000..2436f116 --- /dev/null +++ b/patches/video-id-to-service-id/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + // Get a stream of all anime + allAnime, err := arn.AllAnime() + + if err != nil { + panic(err) + } + + // Iterate over the stream + for _, anime := range allAnime { + for _, trailer := range anime.Trailers { + // trailer.ServiceID = trailer.DeprecatedVideoID + println(trailer.DeprecatedVideoID) + trailer.ServiceID = trailer.DeprecatedVideoID + } + + if anime.Trailers == nil { + anime.Trailers = []*arn.ExternalMedia{} + } + + err := anime.Save() + + if err != nil { + color.Red("Error saving anime: %v", err) + } + } +} From c32166f38c42bf487ac9fa43ccc19b2f46004a15 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 14:56:22 +0200 Subject: [PATCH 021/527] Added new soundtrack interface --- main.go | 2 ++ pages/music/music.go | 12 ++++++------ pages/music/music.pixy | 7 ++++++- pages/music/music.scarlet | 7 ++++++- pages/newsoundtrack/newsoundtrack.go | 20 ++++++++++++++++++++ pages/newsoundtrack/newsoundtrack.pixy | 10 ++++++++++ 6 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 pages/newsoundtrack/newsoundtrack.go create mode 100644 pages/newsoundtrack/newsoundtrack.pixy diff --git a/main.go b/main.go index 301f8874..d97fa7dc 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/login" "github.com/animenotifier/notify.moe/pages/music" + "github.com/animenotifier/notify.moe/pages/newsoundtrack" "github.com/animenotifier/notify.moe/pages/newthread" "github.com/animenotifier/notify.moe/pages/popularanime" "github.com/animenotifier/notify.moe/pages/posts" @@ -67,6 +68,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/animelist", animelist.Get) app.Ajax("/user/:nick/animelist/:id", animelistitem.Get) app.Ajax("/new/thread", newthread.Get) + app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) app.Ajax("/music", music.Get) app.Ajax("/admin", admin.Get) diff --git a/pages/music/music.go b/pages/music/music.go index 9ce4719c..0400db47 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -15,7 +15,7 @@ func Get(ctx *aero.Context) string { tracks := []*arn.SoundTrack{} tracks = append(tracks, &arn.SoundTrack{ - ID: "0", + ID: arn.GenerateID("SoundTrack"), Media: []arn.ExternalMedia{ arn.ExternalMedia{ Service: "Soundcloud", @@ -30,7 +30,7 @@ func Get(ctx *aero.Context) string { }) tracks = append(tracks, &arn.SoundTrack{ - ID: "1", + ID: arn.GenerateID("SoundTrack"), Media: []arn.ExternalMedia{ arn.ExternalMedia{ Service: "Soundcloud", @@ -45,7 +45,7 @@ func Get(ctx *aero.Context) string { }) tracks = append(tracks, &arn.SoundTrack{ - ID: "2", + ID: arn.GenerateID("SoundTrack"), Media: []arn.ExternalMedia{ arn.ExternalMedia{ Service: "Soundcloud", @@ -60,7 +60,7 @@ func Get(ctx *aero.Context) string { }) tracks = append(tracks, &arn.SoundTrack{ - ID: "3", + ID: arn.GenerateID("SoundTrack"), Media: []arn.ExternalMedia{ arn.ExternalMedia{ Service: "Soundcloud", @@ -75,7 +75,7 @@ func Get(ctx *aero.Context) string { }) tracks = append(tracks, &arn.SoundTrack{ - ID: "3", + ID: arn.GenerateID("SoundTrack"), Media: []arn.ExternalMedia{ arn.ExternalMedia{ Service: "Soundcloud", @@ -90,7 +90,7 @@ func Get(ctx *aero.Context) string { }) tracks = append(tracks, &arn.SoundTrack{ - ID: "3", + ID: arn.GenerateID("SoundTrack"), Media: []arn.ExternalMedia{ arn.ExternalMedia{ Service: "Soundcloud", diff --git a/pages/music/music.pixy b/pages/music/music.pixy index f2b60d80..a935092e 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -1,5 +1,10 @@ component Music(tracks []*arn.SoundTrack) - h2.page-title Music + h2 Soundtracks + + .music-buttons + a.button.ajax(href="/new/soundtrack") + Icon("plus") + span Add soundtrack .sound-tracks each track in tracks diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet index c6170788..80ab7059 100644 --- a/pages/music/music.scarlet +++ b/pages/music/music.scarlet @@ -21,4 +21,9 @@ // .sound-track-anime-image - max-width 142px \ No newline at end of file + max-width 142px + +.music-buttons + position absolute + top content-padding + right content-padding \ No newline at end of file diff --git a/pages/newsoundtrack/newsoundtrack.go b/pages/newsoundtrack/newsoundtrack.go new file mode 100644 index 00000000..5a3a7fb2 --- /dev/null +++ b/pages/newsoundtrack/newsoundtrack.go @@ -0,0 +1,20 @@ +package newsoundtrack + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get forums page. +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + return ctx.HTML(components.NewSoundTrack(user)) +} diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy new file mode 100644 index 00000000..bcdf3b60 --- /dev/null +++ b/pages/newsoundtrack/newsoundtrack.pixy @@ -0,0 +1,10 @@ +component NewSoundTrack(user *arn.User) + .widgets + .widget + input#soundcloud-link.widget-element(type="text", placeholder="Soundcloud link or ID") + input#anime-link.widget-element(type="text", placeholder="Anime link or ID") + input#osu-link.widget-element(type="text", placeholder="Osu beatmap link (optional)") + + button.action(data-action="createSoundTrack", data-trigger="click") + Icon("check") + span Add soundtrack \ No newline at end of file From 50cd0621401ecc70c22b476a7603bd534d734433 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 16:23:57 +0200 Subject: [PATCH 022/527] Implemented music page --- auth/api-keys.go | 19 ----- auth/google.go | 4 +- jobs/discord/main.go | 28 +------ jobs/sync-anime/main.go | 12 ++- middleware/UserInfo.go | 14 +--- pages/music/music.go | 101 ++----------------------- pages/newsoundtrack/newsoundtrack.pixy | 4 +- scripts/actions.ts | 22 ++++++ 8 files changed, 45 insertions(+), 159 deletions(-) delete mode 100644 auth/api-keys.go diff --git a/auth/api-keys.go b/auth/api-keys.go deleted file mode 100644 index fdc7e7fa..00000000 --- a/auth/api-keys.go +++ /dev/null @@ -1,19 +0,0 @@ -package auth - -import ( - "encoding/json" - "io/ioutil" - - "github.com/animenotifier/arn" -) - -var apiKeys arn.APIKeys - -func init() { - data, _ := ioutil.ReadFile("security/api-keys.json") - err := json.Unmarshal(data, &apiKeys) - - if err != nil { - panic(err) - } -} diff --git a/auth/google.go b/auth/google.go index d23048b5..11e9717b 100644 --- a/auth/google.go +++ b/auth/google.go @@ -29,8 +29,8 @@ type GoogleUser struct { // InstallGoogleAuth enables Google login for the app. func InstallGoogleAuth(app *aero.Application) { config := &oauth2.Config{ - ClientID: apiKeys.Google.ID, - ClientSecret: apiKeys.Google.Secret, + ClientID: arn.APIKeys.Google.ID, + ClientSecret: arn.APIKeys.Google.Secret, RedirectURL: "https://" + app.Config.Domain + "/auth/google/callback", Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", diff --git a/jobs/discord/main.go b/jobs/discord/main.go index b0885f08..af3bbf0b 100644 --- a/jobs/discord/main.go +++ b/jobs/discord/main.go @@ -1,13 +1,9 @@ package main import ( - "encoding/json" - "io/ioutil" "log" "os" "os/signal" - "path" - "path/filepath" "strings" "syscall" @@ -21,30 +17,8 @@ var discord *discordgo.Session func main() { var err error - exe, err := os.Executable() - - if err != nil { - panic(err) - } - - dir := path.Dir(exe) - var apiKeysPath string - apiKeysPath, err = filepath.Abs(dir + "/../../security/api-keys.json") - - if err != nil { - panic(err) - } - - var apiKeys arn.APIKeys - data, _ := ioutil.ReadFile(apiKeysPath) - err = json.Unmarshal(data, &apiKeys) - - if err != nil { - panic(err) - } - discord, _ = discordgo.New() - discord.Token = "Bot " + apiKeys.Discord.Token + discord.Token = "Bot " + arn.APIKeys.Discord.Token // Verify a Token was provided if discord.Token == "" { diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index 7c87ddee..3ccdcacb 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -46,9 +46,15 @@ func sync(data *kitsu.Anime) { anime.EpisodeCount = attr.EpisodeCount anime.EpisodeLength = attr.EpisodeLength anime.Status = attr.Status - anime.NSFW = attr.Nsfw anime.Summary = arn.FixAnimeDescription(attr.Synopsis) + // NSFW + if attr.Nsfw { + anime.NSFW = 1 + } else { + anime.NSFW = 0 + } + // Rating overall, convertError := strconv.ParseFloat(attr.AverageRating, 64) @@ -59,10 +65,10 @@ func sync(data *kitsu.Anime) { anime.Rating.Overall = overall // Trailers - anime.Trailers = []arn.ExternalMedia{} + anime.Trailers = []*arn.ExternalMedia{} if attr.YoutubeVideoID != "" { - anime.Trailers = append(anime.Trailers, arn.ExternalMedia{ + anime.Trailers = append(anime.Trailers, &arn.ExternalMedia{ Service: "Youtube", ServiceID: attr.YoutubeVideoID, }) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index 96aecab1..f2888793 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -2,7 +2,6 @@ package middleware import ( "encoding/json" - "io/ioutil" "net/http" "strconv" "strings" @@ -15,17 +14,6 @@ import ( "github.com/parnurzeal/gorequest" ) -var apiKeys arn.APIKeys - -func init() { - data, _ := ioutil.ReadFile("security/api-keys.json") - err := json.Unmarshal(data, &apiKeys) - - if err != nil { - panic(err) - } -} - // UserInfo updates user related information after each request. func UserInfo() aero.Middleware { return func(ctx *aero.Context, next func()) { @@ -84,7 +72,7 @@ func updateUserInfo(ctx *aero.Context, user *arn.User) { // Updates the location of the user. func updateUserLocation(user *arn.User, newIP string) { user.IP = newIP - locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json" + locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + arn.APIKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json" response, data, err := gorequest.New().Get(locationAPI).EndBytes() diff --git a/pages/music/music.go b/pages/music/music.go index 0400db47..8918ab0c 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -1,6 +1,7 @@ package music import ( + "net/http" "sort" "github.com/aerogo/aero" @@ -12,105 +13,19 @@ const maxTracks = 10 // Get renders the music page. func Get(ctx *aero.Context) string { - tracks := []*arn.SoundTrack{} + tracks, err := arn.AllSoundTracks() - tracks = append(tracks, &arn.SoundTrack{ - ID: arn.GenerateID("SoundTrack"), - Media: []arn.ExternalMedia{ - arn.ExternalMedia{ - Service: "Soundcloud", - ServiceID: "145918628", - }, - }, - Tags: []string{ - "anime:2357", - }, - Created: arn.DateTimeUTC(), - CreatedBy: "4J6qpK1ve", - }) - - tracks = append(tracks, &arn.SoundTrack{ - ID: arn.GenerateID("SoundTrack"), - Media: []arn.ExternalMedia{ - arn.ExternalMedia{ - Service: "Soundcloud", - ServiceID: "127672476", - }, - }, - Tags: []string{ - "anime:7622", - }, - Created: arn.DateTimeUTC(), - CreatedBy: "4J6qpK1ve", - }) - - tracks = append(tracks, &arn.SoundTrack{ - ID: arn.GenerateID("SoundTrack"), - Media: []arn.ExternalMedia{ - arn.ExternalMedia{ - Service: "Soundcloud", - ServiceID: "270777538", - }, - }, - Tags: []string{ - "anime:11469", - }, - Created: arn.DateTimeUTC(), - CreatedBy: "4J6qpK1ve", - }) - - tracks = append(tracks, &arn.SoundTrack{ - ID: arn.GenerateID("SoundTrack"), - Media: []arn.ExternalMedia{ - arn.ExternalMedia{ - Service: "Soundcloud", - ServiceID: "243839100", - }, - }, - Tags: []string{ - "anime:9962", - }, - Created: arn.DateTimeUTC(), - CreatedBy: "4J6qpK1ve", - }) - - tracks = append(tracks, &arn.SoundTrack{ - ID: arn.GenerateID("SoundTrack"), - Media: []arn.ExternalMedia{ - arn.ExternalMedia{ - Service: "Soundcloud", - ServiceID: "207355237", - }, - }, - Tags: []string{ - "anime:6589", - }, - Created: arn.DateTimeUTC(), - CreatedBy: "4J6qpK1ve", - }) - - tracks = append(tracks, &arn.SoundTrack{ - ID: arn.GenerateID("SoundTrack"), - Media: []arn.ExternalMedia{ - arn.ExternalMedia{ - Service: "Soundcloud", - ServiceID: "242172944", - }, - }, - Tags: []string{ - "anime:10740", - }, - Created: arn.DateTimeUTC(), - CreatedBy: "4J6qpK1ve", - }) - - if len(tracks) > maxTracks { - tracks = tracks[:maxTracks] + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) } sort.Slice(tracks, func(i, j int) bool { return tracks[i].Created > tracks[j].Created }) + if len(tracks) > maxTracks { + tracks = tracks[:maxTracks] + } + return ctx.HTML(components.Music(tracks)) } diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy index bcdf3b60..4c57b373 100644 --- a/pages/newsoundtrack/newsoundtrack.pixy +++ b/pages/newsoundtrack/newsoundtrack.pixy @@ -1,8 +1,8 @@ component NewSoundTrack(user *arn.User) .widgets .widget - input#soundcloud-link.widget-element(type="text", placeholder="Soundcloud link or ID") - input#anime-link.widget-element(type="text", placeholder="Anime link or ID") + input#soundcloud-link.widget-element(type="text", placeholder="Soundcloud link") + input#anime-link.widget-element(type="text", placeholder="Anime link") input#osu-link.widget-element(type="text", placeholder="Osu beatmap link (optional)") button.action(data-action="createSoundTrack", data-trigger="click") diff --git a/scripts/actions.ts b/scripts/actions.ts index c575d55a..14b80412 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -103,6 +103,28 @@ export function createThread(arn: AnimeNotifier) { .catch(console.error) } +// Create soundtrack +export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { + let soundcloud = arn.app.find("soundcloud-link") as HTMLInputElement + let anime = arn.app.find("anime-link") as HTMLInputElement + let osu = arn.app.find("osu-link") as HTMLInputElement + + let soundtrack = { + soundcloud: soundcloud.value, + tags: [anime.value, osu.value], + } + + button.innerText = "Adding..." + button.disabled = true + + arn.post("/api/soundtrack/new", soundtrack) + .then(() => arn.app.load("/music")) + .catch(err => { + console.error(err) + arn.reloadContent() + }) +} + // Search export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { if(e.ctrlKey || e.altKey) { From 990a8032f344a7ffb1cc88e638291e410a629c72 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 16:31:30 +0200 Subject: [PATCH 023/527] Disabled old patch --- patches/video-id-to-service-id/main.go | 62 ++++++++++++++------------ 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/patches/video-id-to-service-id/main.go b/patches/video-id-to-service-id/main.go index 2436f116..d2d7136a 100644 --- a/patches/video-id-to-service-id/main.go +++ b/patches/video-id-to-service-id/main.go @@ -1,34 +1,38 @@ package main -import ( - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - func main() { - // Get a stream of all anime - allAnime, err := arn.AllAnime() - if err != nil { - panic(err) - } - - // Iterate over the stream - for _, anime := range allAnime { - for _, trailer := range anime.Trailers { - // trailer.ServiceID = trailer.DeprecatedVideoID - println(trailer.DeprecatedVideoID) - trailer.ServiceID = trailer.DeprecatedVideoID - } - - if anime.Trailers == nil { - anime.Trailers = []*arn.ExternalMedia{} - } - - err := anime.Save() - - if err != nil { - color.Red("Error saving anime: %v", err) - } - } } + +// import ( +// "github.com/animenotifier/arn" +// "github.com/fatih/color" +// ) + +// func main() { +// // Get a stream of all anime +// allAnime, err := arn.AllAnime() + +// if err != nil { +// panic(err) +// } + +// // Iterate over the stream +// for _, anime := range allAnime { +// for _, trailer := range anime.Trailers { +// // trailer.ServiceID = trailer.DeprecatedVideoID +// println(trailer.DeprecatedVideoID) +// trailer.ServiceID = trailer.DeprecatedVideoID +// } + +// if anime.Trailers == nil { +// anime.Trailers = []*arn.ExternalMedia{} +// } + +// err := anime.Save() + +// if err != nil { +// color.Red("Error saving anime: %v", err) +// } +// } +// } From 064a3e1fa54687a69250e2ee50bd254f6db616ba Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 16:51:27 +0200 Subject: [PATCH 024/527] Added soundtracks to dashboard --- pages/dashboard/dashboard.go | 36 +++++++++++++++++++++++++--------- pages/dashboard/dashboard.pixy | 20 ++++++++++++------- pages/music/music.go | 5 ----- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 03366114..eb8a3ced 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -11,6 +11,7 @@ import ( const maxPosts = 5 const maxFollowing = 5 +const maxSoundTracks = 5 // Get the dashboard or the frontpage when logged out. func Get(ctx *aero.Context) string { @@ -26,22 +27,43 @@ func Get(ctx *aero.Context) string { // Render the dashboard. func dashboard(ctx *aero.Context) string { var posts []*arn.Post - var err error var userList interface{} var followingList []*arn.User + var soundTracks []*arn.SoundTrack user := utils.GetUser(ctx) flow.Parallel(func() { + var err error posts, err = arn.AllPosts() + + if err != nil { + return + } + arn.SortPostsLatestFirst(posts) posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) }, func() { - // threads, err = arn.AllThreadsSlice() - // arn.SortPostsLatestFirst(posts) - // posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) + var err error + soundTracks, err = arn.AllSoundTracks() + + if err != nil { + return + } + + arn.SortSoundTracksLatestFirst(soundTracks) + + if len(soundTracks) > maxSoundTracks { + soundTracks = soundTracks[:maxSoundTracks] + } }, func() { + var err error userList, err = arn.DB.GetMany("User", user.Following) + + if err != nil { + return + } + followingList = userList.([]*arn.User) followingList = arn.SortUsersLastSeen(followingList) @@ -50,9 +72,5 @@ func dashboard(ctx *aero.Context) string { } }) - if err != nil { - return ctx.Error(500, "Error displaying dashboard", err) - } - - return ctx.HTML(components.Dashboard(posts, followingList)) + return ctx.HTML(components.Dashboard(posts, soundTracks, followingList)) } diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 1ad95cac..588b8a1c 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -1,4 +1,4 @@ -component Dashboard(posts []*arn.Post, following []*arn.User) +component Dashboard(posts []*arn.Post, soundTracks []*arn.SoundTrack, following []*arn.User) h2.page-title Dash .widgets @@ -30,13 +30,19 @@ component Dashboard(posts []*arn.Post, following []*arn.User) span ... .widget.mountable - h3.widget-title Messages + h3.widget-title Soundtracks - for i := 1; i <= 5; i++ - .widget-element - .widget-element-text - Icon("comment") - span ... + for i := 0; i <= 4; i++ + if i < len(soundTracks) + a.widget-element.ajax(href="/music") + .widget-element-text + Icon("music") + span= soundTracks[i].MainAnime().Title.Canonical + else + .widget-element + .widget-element-text + Icon("music") + span ... .widget.mountable h3.widget-title Contacts diff --git a/pages/music/music.go b/pages/music/music.go index 8918ab0c..562d50a2 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -2,7 +2,6 @@ package music import ( "net/http" - "sort" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -19,10 +18,6 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) } - sort.Slice(tracks, func(i, j int) bool { - return tracks[i].Created > tracks[j].Created - }) - if len(tracks) > maxTracks { tracks = tracks[:maxTracks] } From 622cc548454752b1cb25fbf9007e4865c5a8fb5c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 16:54:16 +0200 Subject: [PATCH 025/527] Fixed sorting on music page --- pages/music/music.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pages/music/music.go b/pages/music/music.go index 562d50a2..38f9ec79 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -18,6 +18,8 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) } + arn.SortSoundTracksLatestFirst(tracks) + if len(tracks) > maxTracks { tracks = tracks[:maxTracks] } From 9463f1c13b6367aaca8fd3a9490d3d72897f0835 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 17:05:16 +0200 Subject: [PATCH 026/527] Add ID to music list --- pages/music/music.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/music/music.pixy b/pages/music/music.pixy index a935092e..337c632f 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -8,7 +8,7 @@ component Music(tracks []*arn.SoundTrack) .sound-tracks each track in tracks - .sound-track + .sound-track(id=track.ID) a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) From 8727ab63eb073defa3f5f42818475baf42c18b1b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 17:51:15 +0200 Subject: [PATCH 027/527] Added soundtracks to user profiles --- pages/music/music.pixy | 19 +++++++++++-------- pages/profile/profile.go | 11 ++++++++++- pages/profile/profile.pixy | 15 +++++++++++++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/pages/music/music.pixy b/pages/music/music.pixy index 337c632f..ad400e24 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -8,11 +8,14 @@ component Music(tracks []*arn.SoundTrack) .sound-tracks each track in tracks - .sound-track(id=track.ID) - a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) - img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - - iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") - .sound-track-footer - span posted by - a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file + SoundTrack(track) + +component SoundTrack(track *arn.SoundTrack) + .sound-track(id=track.ID) + a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) + img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) + + iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") + .sound-track-footer + span posted by + a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file diff --git a/pages/profile/profile.go b/pages/profile/profile.go index c146f2ac..1c3fa464 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -9,6 +9,7 @@ import ( ) const maxPosts = 5 +const maxTracks = 5 // Get user profile page. func Get(ctx *aero.Context) string { @@ -27,6 +28,7 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { var user *arn.User var threads []*arn.Thread var animeList *arn.AnimeList + var tracks []*arn.SoundTrack var posts []*arn.Post flow.Parallel(func() { @@ -48,7 +50,14 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { if len(posts) > maxPosts { posts = posts[:maxPosts] } + }, func() { + tracks = viewUser.SoundTracks() + arn.SortSoundTracksLatestFirst(tracks) + + if len(tracks) > maxTracks { + tracks = tracks[:maxTracks] + } }) - return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts)) + return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts, tracks)) } diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 3dcbd6b2..fee38873 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -44,7 +44,7 @@ component ProfileHeader(viewUser *arn.User, user *arn.User) Icon("rocket") span= arn.Capitalize(viewUser.Role) -component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post) +component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack) ProfileHeader(viewUser, user) .profile-category.mountable @@ -58,7 +58,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, each item in animeList.Items a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") img.anime-cover-image.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) - + .profile-category.mountable h3 a.ajax(href="/+" + viewUser.Nick + "/threads", title="View all threads") Threads @@ -68,6 +68,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, else each thread in threads ThreadLink(thread) + .profile-category.mountable h3 a.ajax(href="/+" + viewUser.Nick + "/posts", title="View all posts") Posts @@ -84,6 +85,16 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .spacer .post-likes= len(post.Likes) + .profile-category.mountable + h3 Tracks + + if len(tracks) == 0 + p No soundtracks posted yet. + else + .sound-tracks + each track in tracks + SoundTrack(track) + //- if user != nil && user.Role == "admin" //- .footer //- a(href="/api/user/" + viewUser.ID) User API \ No newline at end of file From de9e743067ca35ec0765470f10c6b0d3944024e3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 18:43:13 +0200 Subject: [PATCH 028/527] Improved tracks on the user profile --- pages/music/music.pixy | 16 +++++++++------- pages/music/music.scarlet | 3 +++ pages/profile/profile.go | 2 +- pages/profile/profile.scarlet | 20 +++++++++++++++++++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pages/music/music.pixy b/pages/music/music.pixy index ad400e24..4e9c00ac 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -11,11 +11,13 @@ component Music(tracks []*arn.SoundTrack) SoundTrack(track) component SoundTrack(track *arn.SoundTrack) - .sound-track(id=track.ID) - a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) - img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) + .sound-track.mountable(id=track.ID) + .sound-track-content + a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) + img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) + + iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") - iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") - .sound-track-footer - span posted by - a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file + .sound-track-footer + span posted by + a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet index 80ab7059..fcaa5a19 100644 --- a/pages/music/music.scarlet +++ b/pages/music/music.scarlet @@ -2,6 +2,9 @@ vertical .sound-track + vertical + +.sound-track-content horizontal iframe diff --git a/pages/profile/profile.go b/pages/profile/profile.go index 1c3fa464..c719a4f2 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -9,7 +9,7 @@ import ( ) const maxPosts = 5 -const maxTracks = 5 +const maxTracks = 4 // Get user profile page. func Get(ctx *aero.Context) string { diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 80742404..15704c88 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -90,4 +90,22 @@ profile-boot-duration = 2s // Categories .profile-category - margin-bottom content-padding \ No newline at end of file + margin-bottom content-padding + + .sound-tracks + horizontal-wrap + justify-content space-around + + .sound-track-anime-link + display none + + .sound-track + flex 1 + flex-basis 500px + padding 1rem + +> 800px + .profile-category + .sound-tracks + .sound-track-anime-link + display block \ No newline at end of file From fb1723d15ff474825f1ddf83450c798f30526035 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 18:44:22 +0200 Subject: [PATCH 029/527] Reduced max tracks to 3 --- pages/profile/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.go b/pages/profile/profile.go index c719a4f2..c69c5e57 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -9,7 +9,7 @@ import ( ) const maxPosts = 5 -const maxTracks = 4 +const maxTracks = 3 // Get user profile page. func Get(ctx *aero.Context) string { From af726bd1ac64d457f4787b7204c02f1b0f1e71e6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 18:53:31 +0200 Subject: [PATCH 030/527] Improved soundtrack overview --- pages/music/music.go | 2 +- pages/music/music.scarlet | 13 +++++++++++-- pages/profile/profile.scarlet | 20 +------------------- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/pages/music/music.go b/pages/music/music.go index 38f9ec79..da00dd90 100644 --- a/pages/music/music.go +++ b/pages/music/music.go @@ -8,7 +8,7 @@ import ( "github.com/animenotifier/notify.moe/components" ) -const maxTracks = 10 +const maxTracks = 9 // Get renders the music page. func Get(ctx *aero.Context) string { diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet index fcaa5a19..247ec453 100644 --- a/pages/music/music.scarlet +++ b/pages/music/music.scarlet @@ -1,8 +1,13 @@ .sound-tracks - vertical + horizontal-wrap + justify-content space-around .sound-track vertical + flex 1 + flex-basis 500px + padding 1rem + .sound-track-content horizontal @@ -21,7 +26,11 @@ opacity 0.65 .sound-track-anime-link - // + display none + +> 800px + .sound-track-anime-link + display block .sound-track-anime-image max-width 142px diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 15704c88..80742404 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -90,22 +90,4 @@ profile-boot-duration = 2s // Categories .profile-category - margin-bottom content-padding - - .sound-tracks - horizontal-wrap - justify-content space-around - - .sound-track-anime-link - display none - - .sound-track - flex 1 - flex-basis 500px - padding 1rem - -> 800px - .profile-category - .sound-tracks - .sound-track-anime-link - display block \ No newline at end of file + margin-bottom content-padding \ No newline at end of file From 0072b5cac924df8eb15940ad7f2e6b6716768f53 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 18:57:15 +0200 Subject: [PATCH 031/527] Improved mobile version --- pages/music/music.scarlet | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pages/music/music.scarlet b/pages/music/music.scarlet index 247ec453..226833be 100644 --- a/pages/music/music.scarlet +++ b/pages/music/music.scarlet @@ -35,7 +35,8 @@ .sound-track-anime-image max-width 142px -.music-buttons - position absolute - top content-padding - right content-padding \ No newline at end of file +> 600px + .music-buttons + position absolute + top content-padding + right content-padding \ No newline at end of file From 9bd46d1eb0b70068b9e5af884edd12c6b70ceab0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 19:03:21 +0200 Subject: [PATCH 032/527] Improved overview --- pages/music/music.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/music/music.pixy b/pages/music/music.pixy index 4e9c00ac..074280c2 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -16,7 +16,7 @@ component SoundTrack(track *arn.SoundTrack) a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") + iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&visual=true") .sound-track-footer span posted by From 9ce39df2eb5737d3107636f3f1c9cb703985ec41 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 20:01:24 +0200 Subject: [PATCH 033/527] Increased session duration to 1 week --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d97fa7dc..59c7fc71 100644 --- a/main.go +++ b/main.go @@ -47,7 +47,7 @@ func configure(app *aero.Application) *aero.Application { app.SetStyle(css.Bundle()) // Sessions - app.Sessions.Duration = 3600 * 24 + app.Sessions.Duration = 3600 * 24 * 7 app.Sessions.Store = arn.NewAerospikeStore("Session", app.Sessions.Duration) // Layout From c1765a23ea6ac080abb3c1bb769f831c8921ca44 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 20:12:08 +0200 Subject: [PATCH 034/527] Added list of all tracks by one user --- main.go | 1 + pages/profile/profile.pixy | 3 ++- pages/profile/tracks.go | 32 ++++++++++++++++++++++++++++++++ pages/profile/tracks.pixy | 6 ++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 pages/profile/tracks.go create mode 100644 pages/profile/tracks.pixy diff --git a/main.go b/main.go index 59c7fc71..07e61e83 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick", profile.Get) app.Ajax("/user/:nick/threads", profile.GetThreadsByUser) app.Ajax("/user/:nick/posts", profile.GetPostsByUser) + app.Ajax("/user/:nick/tracks", profile.GetSoundTracksByUser) app.Ajax("/user/:nick/animelist", animelist.Get) app.Ajax("/user/:nick/animelist/:id", animelistitem.Get) app.Ajax("/new/thread", newthread.Get) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index fee38873..aebde1c9 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -86,7 +86,8 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .post-likes= len(post.Likes) .profile-category.mountable - h3 Tracks + h3 + a.ajax(href="/+" + viewUser.Nick + "/tracks", title="View all tracks") Tracks if len(tracks) == 0 p No soundtracks posted yet. diff --git a/pages/profile/tracks.go b/pages/profile/tracks.go new file mode 100644 index 00000000..abf825a1 --- /dev/null +++ b/pages/profile/tracks.go @@ -0,0 +1,32 @@ +package profile + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// GetSoundTracksByUser shows all soundtracks of a particular user. +func GetSoundTracksByUser(ctx *aero.Context) string { + nick := ctx.Get("nick") + user := utils.GetUser(ctx) + viewUser, err := arn.GetUserByNick(nick) + + if err != nil { + return ctx.Error(http.StatusNotFound, "User not found", err) + } + + tracks, err := arn.GetSoundTracksByUser(viewUser) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) + } + + arn.SortSoundTracksLatestFirst(tracks) + + return ctx.HTML(components.TrackList(tracks, viewUser, user)) + +} diff --git a/pages/profile/tracks.pixy b/pages/profile/tracks.pixy new file mode 100644 index 00000000..f554fca8 --- /dev/null +++ b/pages/profile/tracks.pixy @@ -0,0 +1,6 @@ +component TrackList(tracks []*arn.SoundTrack, viewUser *arn.User, user *arn.User) + h2= "Tracks added by " + viewUser.Nick + .sound-tracks + each track in tracks + SoundTrack(track) + \ No newline at end of file From 5099c0b0e711460aa763b37be1924a189ab0a8db Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 27 Jun 2017 23:33:21 +0200 Subject: [PATCH 035/527] Added Youtube support for sound tracks --- mixins/SoundTrack.pixy | 10 ++++++++++ pages/music/music.pixy | 14 +------------- pages/newsoundtrack/newsoundtrack.pixy | 22 ++++++++++++++++------ scripts/actions.ts | 2 ++ tests.go | 1 + 5 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 mixins/SoundTrack.pixy diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy new file mode 100644 index 00000000..301e853c --- /dev/null +++ b/mixins/SoundTrack.pixy @@ -0,0 +1,10 @@ +component SoundTrack(track *arn.SoundTrack) + .sound-track.mountable(id=track.ID) + .sound-track-content + a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) + img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) + + iframe.lazy(data-src=track.Media[0].EmbedLink()) + .sound-track-footer + span posted by + a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file diff --git a/pages/music/music.pixy b/pages/music/music.pixy index 074280c2..bbda7e6e 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -8,16 +8,4 @@ component Music(tracks []*arn.SoundTrack) .sound-tracks each track in tracks - SoundTrack(track) - -component SoundTrack(track *arn.SoundTrack) - .sound-track.mountable(id=track.ID) - .sound-track-content - a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) - img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - - iframe.lazy(data-src="https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/" + track.Media[0].ServiceID + "?auto_play=false&hide_related=true&show_comments=false&show_user=false&show_reposts=false&visual=true") - - .sound-track-footer - span posted by - a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file + SoundTrack(track) \ No newline at end of file diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy index 4c57b373..2237f08a 100644 --- a/pages/newsoundtrack/newsoundtrack.pixy +++ b/pages/newsoundtrack/newsoundtrack.pixy @@ -1,10 +1,20 @@ component NewSoundTrack(user *arn.User) .widgets .widget - input#soundcloud-link.widget-element(type="text", placeholder="Soundcloud link") - input#anime-link.widget-element(type="text", placeholder="Anime link") - input#osu-link.widget-element(type="text", placeholder="Osu beatmap link (optional)") + h3 New soundtrack + label(for="soundcloud-link") Soundcloud link: + input#soundcloud-link.widget-element(type="text", placeholder="https://soundcloud.com/abc/123") + + label(for="youtube-link") Youtube link: + input#youtube-link.widget-element(type="text", placeholder="https://www.youtube.com/watch?v=123") + + label(for="anime-link") Anime link: + input#anime-link.widget-element(type="text", placeholder="https://notify.moe/anime/123") + + label(for="osu-link") Osu beatmap (optional): + input#osu-link.widget-element(type="text", placeholder="https://osu.ppy.sh/s/123") - button.action(data-action="createSoundTrack", data-trigger="click") - Icon("check") - span Add soundtrack \ No newline at end of file + .buttons + button.action(data-action="createSoundTrack", data-trigger="click") + Icon("check") + span Add soundtrack \ No newline at end of file diff --git a/scripts/actions.ts b/scripts/actions.ts index 14b80412..28c7d0b6 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -106,11 +106,13 @@ export function createThread(arn: AnimeNotifier) { // Create soundtrack export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { let soundcloud = arn.app.find("soundcloud-link") as HTMLInputElement + let youtube = arn.app.find("youtube-link") as HTMLInputElement let anime = arn.app.find("anime-link") as HTMLInputElement let osu = arn.app.find("osu-link") as HTMLInputElement let soundtrack = { soundcloud: soundcloud.value, + youtube: youtube.value, tags: [anime.value, osu.value], } diff --git a/tests.go b/tests.go index f1f1f0ac..80a35633 100644 --- a/tests.go +++ b/tests.go @@ -121,6 +121,7 @@ var tests = map[string][]string{ "/auth/google": nil, "/auth/google/callback": nil, "/new/thread": nil, + "/new/soundtrack": nil, "/user": nil, "/settings": nil, "/extension/embed": nil, From d5dcd9c909019dceadaa43dff6a2c7e611b3d79c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 00:16:45 +0200 Subject: [PATCH 036/527] Tracks have permalinks now --- jobs/main.go | 13 ++++++----- jobs/refresh-track-titles/main.go | 37 +++++++++++++++++++++++++++++++ jobs/sync-anime/main.go | 2 +- main.go | 2 ++ mixins/SoundTrack.pixy | 13 +++++++++-- pages/dashboard/dashboard.pixy | 4 ++-- pages/posts/posts.go | 4 +++- pages/tracks/tracks.go | 21 ++++++++++++++++++ pages/tracks/tracks.pixy | 5 +++++ 9 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 jobs/refresh-track-titles/main.go create mode 100644 pages/tracks/tracks.go create mode 100644 pages/tracks/tracks.pixy diff --git a/jobs/main.go b/jobs/main.go index 54db996b..9368da98 100644 --- a/jobs/main.go +++ b/jobs/main.go @@ -23,12 +23,13 @@ var colorPool = []*color.Color{ } 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, + "active-users": 1 * time.Minute, + "avatars": 1 * time.Hour, + "refresh-track-titles": 10 * time.Hour, + "sync-anime": 12 * time.Hour, + "popular-anime": 12 * time.Hour, + "airing-anime": 12 * time.Hour, + "search-index": 12 * time.Hour, } func main() { diff --git a/jobs/refresh-track-titles/main.go b/jobs/refresh-track-titles/main.go new file mode 100644 index 00000000..7bd9dd15 --- /dev/null +++ b/jobs/refresh-track-titles/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Refreshing track titles") + + // Get a stream of all soundtracks + soundtracks, err := arn.StreamSoundTracks() + + if err != nil { + panic(err) + } + + // Iterate over the stream + for track := range soundtracks { + sync(track) + } + + color.Green("Finished.") +} + +func sync(track *arn.SoundTrack) { + for _, media := range track.Media { + media.RefreshMetaData() + println(media.Service, media.Title) + } + + err := track.Save() + + if err != nil { + panic(err) + } +} diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index 3ccdcacb..2f6fa7ec 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -22,7 +22,7 @@ func main() { sync(anime) } - println("Finished.") + color.Green("Finished.") } func sync(data *kitsu.Anime) { diff --git a/main.go b/main.go index 07e61e83..7dcf0e0c 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "github.com/animenotifier/notify.moe/pages/search" "github.com/animenotifier/notify.moe/pages/settings" "github.com/animenotifier/notify.moe/pages/threads" + "github.com/animenotifier/notify.moe/pages/tracks" "github.com/animenotifier/notify.moe/pages/user" "github.com/animenotifier/notify.moe/pages/users" "github.com/animenotifier/notify.moe/pages/webdev" @@ -61,6 +62,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/forum/:tag", forum.Get) app.Ajax("/threads/:id", threads.Get) app.Ajax("/posts/:id", posts.Get) + app.Ajax("/tracks/:id", tracks.Get) app.Ajax("/user", user.Get) app.Ajax("/user/:nick", profile.Get) app.Ajax("/user/:nick/threads", profile.GetThreadsByUser) diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index 301e853c..d7e9f85e 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -1,10 +1,19 @@ component SoundTrack(track *arn.SoundTrack) + SoundTrackMedia(track, track.Media[0]) + +component SoundTrackAllMedia(track *arn.SoundTrack) + each media in track.Media + SoundTrackMedia(track, media) + +component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia) .sound-track.mountable(id=track.ID) .sound-track-content a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - iframe.lazy(data-src=track.Media[0].EmbedLink()) + iframe.lazy(data-src=media.EmbedLink()) .sound-track-footer + a.ajax(href=track.Link()) + Icon("music") span posted by - a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick \ No newline at end of file + a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick + " " \ No newline at end of file diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 588b8a1c..c6037c90 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -34,10 +34,10 @@ component Dashboard(posts []*arn.Post, soundTracks []*arn.SoundTrack, following for i := 0; i <= 4; i++ if i < len(soundTracks) - a.widget-element.ajax(href="/music") + a.widget-element.ajax(href=soundTracks[i].Link()) .widget-element-text Icon("music") - span= soundTracks[i].MainAnime().Title.Canonical + span= soundTracks[i].Media[0].Title else .widget-element .widget-element-text diff --git a/pages/posts/posts.go b/pages/posts/posts.go index de0e45bd..5e705172 100644 --- a/pages/posts/posts.go +++ b/pages/posts/posts.go @@ -1,6 +1,8 @@ package posts import ( + "net/http" + "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" @@ -12,7 +14,7 @@ func Get(ctx *aero.Context) string { post, err := arn.GetPost(id) if err != nil { - return ctx.Error(404, "Post not found", err) + return ctx.Error(http.StatusNotFound, "Post not found", err) } return ctx.HTML(components.Post(post)) diff --git a/pages/tracks/tracks.go b/pages/tracks/tracks.go new file mode 100644 index 00000000..deb9efdb --- /dev/null +++ b/pages/tracks/tracks.go @@ -0,0 +1,21 @@ +package tracks + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +// Get post. +func Get(ctx *aero.Context) string { + id := ctx.Get("id") + track, err := arn.GetSoundTrack(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Track not found", err) + } + + return ctx.HTML(components.Track(track)) +} diff --git a/pages/tracks/tracks.pixy b/pages/tracks/tracks.pixy new file mode 100644 index 00000000..cb510210 --- /dev/null +++ b/pages/tracks/tracks.pixy @@ -0,0 +1,5 @@ +component Track(track *arn.SoundTrack) + h2= track.Media[0].Title + + .sound-tracks + SoundTrackAllMedia(track) \ No newline at end of file From bfbce4c9a20a23321447729aa8e7c87006d91170 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 01:05:39 +0200 Subject: [PATCH 037/527] Added tracks to anime pages --- pages/anime/anime.go | 8 +++++++- pages/anime/anime.pixy | 13 ++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index e10bb0f1..41e1893d 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -19,5 +19,11 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime not found", err) } - return ctx.HTML(components.Anime(anime, user)) + tracks, err := arn.GetSoundTracksByTag("anime:" + anime.ID) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Error fetching soundtracks", err) + } + + return ctx.HTML(components.Anime(anime, tracks, user)) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 0be1b18b..3df5117d 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,4 +1,4 @@ -component Anime(anime *arn.Anime, user *arn.User) +component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) .anime-header(data-id=anime.ID) if anime.Image.Small != "" .anime-image-container @@ -133,11 +133,14 @@ component Anime(anime *arn.Anime, user *arn.User) //- if providers.Nyaa && providers.Nyaa.episodes !== undefined //- span(class=providers.Nyaa.episodes === 0 ? "entry-error" : "entry-ok")= providers.Nyaa.episodes + " eps" - h3.anime-section-name Tracks - p Coming soon. + if len(tracks) > 0 + h3.anime-section-name Tracks + .sound-tracks + each track in tracks + SoundTrack(track) - h3.anime-section-name Reviews - p Coming soon. + //- h3.anime-section-name Reviews + //- p Coming soon. h3.anime-section-name Links .light-button-group From de46187216bffe06961df155d0d25d75fa255a2b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 02:51:11 +0200 Subject: [PATCH 038/527] Improved search index --- jobs/search-index/main.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jobs/search-index/main.go b/jobs/search-index/main.go index 0033b956..e1ef2c6f 100644 --- a/jobs/search-index/main.go +++ b/jobs/search-index/main.go @@ -28,8 +28,12 @@ func updateAnimeIndex() { } for anime := range animeStream { - if anime.Title.Canonical != "" { - animeSearchIndex.TextToID[strings.ToLower(anime.Title.Canonical)] = anime.ID + if anime.Title.Romaji != "" { + animeSearchIndex.TextToID[strings.ToLower(anime.Title.Romaji)] = anime.ID + } + + if anime.Title.English != "" { + animeSearchIndex.TextToID[strings.ToLower(anime.Title.English)] = anime.ID } if anime.Title.Japanese != "" { From ba24b6e2d0f572a6a3efe39c5cbb1adfa69bf095 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 17:55:08 +0200 Subject: [PATCH 039/527] Added anime edit page --- jobs/sync-anime/main.go | 17 +++++++++++++++-- main.go | 2 ++ pages/anime/anime.pixy | 10 ++++++++++ pages/editanime/editanime.go | 28 ++++++++++++++++++++++++++++ pages/editanime/editanime.pixy | 7 +++++++ patches/add-mappings/main.go | 28 ++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 pages/editanime/editanime.go create mode 100644 pages/editanime/editanime.pixy create mode 100644 patches/add-mappings/main.go diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index 2f6fa7ec..8e40361e 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -26,7 +26,16 @@ func main() { } func sync(data *kitsu.Anime) { - anime := arn.Anime{} + anime, err := arn.GetAnime(data.ID) + + if err != nil { + if strings.Contains(err.Error(), "not found") { + anime = &arn.Anime{} + } else { + panic(err) + } + } + attr := data.Attributes // General data @@ -48,6 +57,10 @@ func sync(data *kitsu.Anime) { anime.Status = attr.Status anime.Summary = arn.FixAnimeDescription(attr.Synopsis) + if anime.Mappings == nil { + anime.Mappings = []*arn.Mapping{} + } + // NSFW if attr.Nsfw { anime.NSFW = 1 @@ -75,7 +88,7 @@ func sync(data *kitsu.Anime) { } // Save in database - err := anime.Save() + err = anime.Save() status := "" if err == nil { diff --git a/main.go b/main.go index 7dcf0e0c..c5c0127e 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/animenotifier/notify.moe/pages/animelist" "github.com/animenotifier/notify.moe/pages/animelistitem" "github.com/animenotifier/notify.moe/pages/dashboard" + "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/embed" "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" @@ -58,6 +59,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/", dashboard.Get) app.Ajax("/anime", popularanime.Get) app.Ajax("/anime/:id", anime.Get) + app.Ajax("/anime/:id/edit", editanime.Get) app.Ajax("/forum", forums.Get) app.Ajax("/forum/:tag", forum.Get) app.Ajax("/threads/:id", threads.Get) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 3df5117d..0b311b1e 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -21,6 +21,11 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) if user != nil .buttons.anime-actions + if user.Role == "editor" || user.Role == "admin" + a.button.ajax(href=anime.Link() + "/edit") + Icon("database") + span Edit anime + if user.AnimeList().Contains(anime.ID) a.button.ajax(href="/+" + user.Nick + "/animelist/" + anime.ID) Icon("pencil") @@ -152,6 +157,11 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") Icon("external-link") span Kitsu + + each mapping in anime.Mappings + a.light-button(href=mapping.Link(), target="_blank", rel="noopener") + Icon("external-link") + span= mapping.Name() //- if providers.HummingBird //- a.light-button(href="https://hummingbird.me/anime/" + providers.HummingBird.providerId, target="_blank") HummingBird diff --git a/pages/editanime/editanime.go b/pages/editanime/editanime.go new file mode 100644 index 00000000..592d979f --- /dev/null +++ b/pages/editanime/editanime.go @@ -0,0 +1,28 @@ +package editanime + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get anime edit page. +func Get(ctx *aero.Context) string { + id := ctx.Get("id") + user := utils.GetUser(ctx) + + if user == nil || (user.Role != "editor" && user.Role != "admin") { + return ctx.Error(http.StatusBadRequest, "Not logged in or not auhorized to edit this anime", nil) + } + + anime, err := arn.GetAnime(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Anime not found", err) + } + + return ctx.HTML(components.EditAnime(anime)) +} diff --git a/pages/editanime/editanime.pixy b/pages/editanime/editanime.pixy new file mode 100644 index 00000000..1c1cd3b9 --- /dev/null +++ b/pages/editanime/editanime.pixy @@ -0,0 +1,7 @@ +component EditAnime(anime *arn.Anime) + h2= anime.Title.Canonical + + .widgets + .widget(data-api="/api/anime/" + anime.ID) + h3.anime-section-name Mappings + InputText("ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on http://cal.syoboi.jp") \ No newline at end of file diff --git a/patches/add-mappings/main.go b/patches/add-mappings/main.go new file mode 100644 index 00000000..70cb7cd4 --- /dev/null +++ b/patches/add-mappings/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" +) + +var mappings = map[string]arn.Mapping{ + "13055": arn.Mapping{ + Service: "shoboi/anime", + ServiceID: "4528", + }, +} + +func main() { + for animeID, mapping := range mappings { + anime, err := arn.GetAnime(animeID) + + if err != nil { + panic(err) + } + + fmt.Println(anime.ID, "=", mapping.Service, mapping.ServiceID) + anime.AddMapping(mapping.Service, mapping.ServiceID) + anime.Save() + } +} From 9bae97f9dbccdeddd2a50ab59e88833911dca416 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 18:39:52 +0200 Subject: [PATCH 040/527] Make Shoboi ID editable --- pages/anime/anime.pixy | 2 +- pages/editanime/editanime.pixy | 11 ++++++++++- patches/add-mappings/main.go | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 0b311b1e..b5a53f32 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -23,7 +23,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) .buttons.anime-actions if user.Role == "editor" || user.Role == "admin" a.button.ajax(href=anime.Link() + "/edit") - Icon("database") + Icon("pencil-square-o") span Edit anime if user.AnimeList().Contains(anime.ID) diff --git a/pages/editanime/editanime.pixy b/pages/editanime/editanime.pixy index 1c1cd3b9..adbf6c61 100644 --- a/pages/editanime/editanime.pixy +++ b/pages/editanime/editanime.pixy @@ -4,4 +4,13 @@ component EditAnime(anime *arn.Anime) .widgets .widget(data-api="/api/anime/" + anime.ID) h3.anime-section-name Mappings - InputText("ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on http://cal.syoboi.jp") \ No newline at end of file + InputText("Custom:ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on http://cal.syoboi.jp") + + .buttons + a.button.ajax(href="/anime/" + anime.ID) + Icon("arrow-left") + span View anime + + a.button(href="/api/anime/" + anime.ID, target="_blank") + Icon("search-plus") + span JSON API \ No newline at end of file diff --git a/patches/add-mappings/main.go b/patches/add-mappings/main.go index 70cb7cd4..12c2f541 100644 --- a/patches/add-mappings/main.go +++ b/patches/add-mappings/main.go @@ -22,7 +22,7 @@ func main() { } fmt.Println(anime.ID, "=", mapping.Service, mapping.ServiceID) - anime.AddMapping(mapping.Service, mapping.ServiceID) + anime.AddMapping(mapping.Service, mapping.ServiceID, "4J6qpK1ve") anime.Save() } } From f4e9b31fb83f46dade84b31811247b4e4300d3c6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 19:30:09 +0200 Subject: [PATCH 041/527] Added episodes and airing dates --- jobs/sync-anime/main.go | 4 ++++ pages/anime/anime.pixy | 13 ++++++++++++ pages/anime/episode.scarlet | 21 ++++++++++++++++++++ patches/add-empty-episodes/main.go | 32 ++++++++++++++++++++++++++++++ styles/include/mixins.scarlet | 5 +++++ styles/table.scarlet | 3 +-- 6 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 pages/anime/episode.scarlet create mode 100644 patches/add-empty-episodes/main.go diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index 8e40361e..f3dfd977 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -61,6 +61,10 @@ func sync(data *kitsu.Anime) { anime.Mappings = []*arn.Mapping{} } + if anime.Episodes == nil { + anime.Episodes = []*arn.AnimeEpisode{} + } + // NSFW if attr.Nsfw { anime.NSFW = 1 diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index b5a53f32..e6c42779 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -143,6 +143,19 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) .sound-tracks each track in tracks SoundTrack(track) + + if len(anime.Episodes) > 0 + h3.anime-section-name Episodes + table + tbody + each episode in anime.Episodes + tr.episode + td.episode-number= episode.Number + td.episode-title= episode.Title.Japanese + td.episode-actions + a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") + RawIcon("google") + td.episode-airing-date-start= episode.AiringDate.StartDateHuman() //- h3.anime-section-name Reviews //- p Coming soon. diff --git a/pages/anime/episode.scarlet b/pages/anime/episode.scarlet new file mode 100644 index 00000000..0d963fc2 --- /dev/null +++ b/pages/anime/episode.scarlet @@ -0,0 +1,21 @@ +.episode + horizontal + +.episode-number + flex-basis 3.2rem + // text-align right + +.episode-title + flex 1 + +.episode-airing-date-start + flex-basis 270px + text-align right + +< 800px + .episode-airing-date-start + display none + +< 500px + .episode-actions + display none \ No newline at end of file diff --git a/patches/add-empty-episodes/main.go b/patches/add-empty-episodes/main.go new file mode 100644 index 00000000..47b32a6c --- /dev/null +++ b/patches/add-empty-episodes/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + // Get a stream of all anime + allAnime, err := arn.AllAnime() + + if err != nil { + panic(err) + } + + // Iterate over the stream + for _, anime := range allAnime { + if anime.Mappings == nil { + anime.Mappings = []*arn.Mapping{} + } + + if anime.Episodes == nil { + anime.Episodes = []*arn.AnimeEpisode{} + } + + err := anime.Save() + + if err != nil { + color.Red("Error saving anime: %v", err) + } + } +} diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 35770a34..6bafeb75 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -36,6 +36,11 @@ mixin clip-long-text white-space nowrap text-overflow ellipsis +mixin bg-dark-up + background-color transparent + :hover + background-color rgba(0, 0, 0, 0.015) + mixin light-up filter brightness(0.4) saturate(1) :hover diff --git a/styles/table.scarlet b/styles/table.scarlet index 3ee0073b..95095801 100644 --- a/styles/table.scarlet +++ b/styles/table.scarlet @@ -19,5 +19,4 @@ th tbody tr - :hover - background-color rgba(0, 0, 0, 0.015) \ No newline at end of file + bg-dark-up \ No newline at end of file From ec2eabebf0b1aa411099108968ea814bdd6ae421 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 20:10:17 +0200 Subject: [PATCH 042/527] Updated episode style --- pages/anime/episode.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/anime/episode.scarlet b/pages/anime/episode.scarlet index 0d963fc2..a760fc2a 100644 --- a/pages/anime/episode.scarlet +++ b/pages/anime/episode.scarlet @@ -9,7 +9,7 @@ flex 1 .episode-airing-date-start - flex-basis 270px + flex-basis 280px text-align right < 800px From 1911d9954bbce88170ba9c8cb792a18d2c8b2830 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 20:29:04 +0200 Subject: [PATCH 043/527] Minor changes --- pages/anime/anime.pixy | 2 +- pages/anime/episode.scarlet | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index e6c42779..104d918e 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -147,7 +147,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) if len(anime.Episodes) > 0 h3.anime-section-name Episodes table - tbody + tbody.episodes each episode in anime.Episodes tr.episode td.episode-number= episode.Number diff --git a/pages/anime/episode.scarlet b/pages/anime/episode.scarlet index a760fc2a..8fe8f2dc 100644 --- a/pages/anime/episode.scarlet +++ b/pages/anime/episode.scarlet @@ -1,3 +1,6 @@ +.episodes + // + .episode horizontal From a63da27429e32f554a740264abd30bbf245e1841 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 21:17:49 +0200 Subject: [PATCH 044/527] Implemented schedule --- pages/dashboard/dashboard.go | 43 ++++++++++++++++++++++++++++++- pages/dashboard/dashboard.pixy | 19 +++++++++----- pages/dashboard/dashboard.scarlet | 5 ++++ 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 pages/dashboard/dashboard.scarlet diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index eb8a3ced..d16d774c 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -1,6 +1,8 @@ package dashboard import ( + "time" + "github.com/aerogo/aero" "github.com/aerogo/flow" "github.com/animenotifier/arn" @@ -12,6 +14,7 @@ import ( const maxPosts = 5 const maxFollowing = 5 const maxSoundTracks = 5 +const maxScheduleItems = 5 // Get the dashboard or the frontpage when logged out. func Get(ctx *aero.Context) string { @@ -30,6 +33,7 @@ func dashboard(ctx *aero.Context) string { var userList interface{} var followingList []*arn.User var soundTracks []*arn.SoundTrack + var upcomingEpisodes []*arn.UpcomingEpisode user := utils.GetUser(ctx) @@ -43,6 +47,43 @@ func dashboard(ctx *aero.Context) string { arn.SortPostsLatestFirst(posts) posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) + }, func() { + animeList, err := arn.GetAnimeList(user) + + if err != nil { + return + } + + var keys []string + + for _, item := range animeList.Items { + keys = append(keys, item.AnimeID) + } + + objects, getErr := arn.DB.GetMany("Anime", keys) + + if getErr != nil { + return + } + + allAnimeInList := objects.([]*arn.Anime) + now := time.Now().UTC().Format(time.RFC3339) + + for _, anime := range allAnimeInList { + if len(upcomingEpisodes) >= maxScheduleItems { + break + } + + for _, episode := range anime.Episodes { + if episode.AiringDate.Start > now { + upcomingEpisodes = append(upcomingEpisodes, &arn.UpcomingEpisode{ + Anime: anime, + Episode: episode, + }) + continue + } + } + } }, func() { var err error soundTracks, err = arn.AllSoundTracks() @@ -72,5 +113,5 @@ func dashboard(ctx *aero.Context) string { } }) - return ctx.HTML(components.Dashboard(posts, soundTracks, followingList)) + return ctx.HTML(components.Dashboard(upcomingEpisodes, posts, soundTracks, followingList)) } diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index c6037c90..4189ff1d 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -1,15 +1,22 @@ -component Dashboard(posts []*arn.Post, soundTracks []*arn.SoundTrack, following []*arn.User) +component Dashboard(schedule []*arn.UpcomingEpisode, posts []*arn.Post, soundTracks []*arn.SoundTrack, following []*arn.User) h2.page-title Dash .widgets .widget.mountable h3.widget-title Schedule - for i := 1; i <= 5; i++ - .widget-element - .widget-element-text - Icon("calendar-o") - span ... + for i := 0; i <= 4; i++ + if i < len(schedule) + a.widget-element.ajax(href=schedule[i].Anime.Link()) + .widget-element-text + Icon("calendar-o") + .schedule-item-title= schedule[i].Anime.Title.Canonical + .schedule-item-episode= "# " + toString(schedule[i].Episode.Number) + else + .widget-element + .widget-element-text + Icon("calendar-o") + span ... .widget.mountable h3.widget-title Forums diff --git a/pages/dashboard/dashboard.scarlet b/pages/dashboard/dashboard.scarlet new file mode 100644 index 00000000..54fa04e9 --- /dev/null +++ b/pages/dashboard/dashboard.scarlet @@ -0,0 +1,5 @@ +.schedule-item-title + flex 1 + +.schedule-item-episode + text-align right \ No newline at end of file From 2f5fa949f4712abddfdf564741138549a68e383c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 21:29:36 +0200 Subject: [PATCH 045/527] Improved schedule --- pages/dashboard/dashboard.scarlet | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/dashboard/dashboard.scarlet b/pages/dashboard/dashboard.scarlet index 54fa04e9..a380f5b8 100644 --- a/pages/dashboard/dashboard.scarlet +++ b/pages/dashboard/dashboard.scarlet @@ -1,5 +1,9 @@ .schedule-item-title flex 1 + white-space nowrap + text-overflow ellipsis + overflow hidden .schedule-item-episode - text-align right \ No newline at end of file + text-align right + flex-basis 50px \ No newline at end of file From b90603be89d37172c7197881db799e42b871fcfd Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 21:32:13 +0200 Subject: [PATCH 046/527] Added sorting to schedule --- pages/dashboard/dashboard.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index d16d774c..4b68f9d8 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -1,6 +1,7 @@ package dashboard import ( + "sort" "time" "github.com/aerogo/aero" @@ -84,6 +85,10 @@ func dashboard(ctx *aero.Context) string { } } } + + sort.Slice(upcomingEpisodes, func(i, j int) bool { + return upcomingEpisodes[i].Episode.AiringDate.Start < upcomingEpisodes[j].Episode.AiringDate.Start + }) }, func() { var err error soundTracks, err = arn.AllSoundTracks() From b03d5335123d7cbc837f593aea06b775fb289caf Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 21:53:15 +0200 Subject: [PATCH 047/527] Center images in forum posts --- styles/typography.scarlet | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/styles/typography.scarlet b/styles/typography.scarlet index 16633b0f..81c3b08e 100644 --- a/styles/typography.scarlet +++ b/styles/typography.scarlet @@ -13,4 +13,6 @@ h2 p > img max-width 100% - border-radius 3px \ No newline at end of file + border-radius 3px + display inherit + margin 0 auto \ No newline at end of file From edf98fd6018129a590bef209ea3ca43b9f0937f7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 23:08:58 +0200 Subject: [PATCH 048/527] Improved episode overview for long series --- pages/anime/anime.go | 16 +++++++++++++++- pages/anime/anime.pixy | 9 ++++++--- pages/anime/episode.scarlet | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 41e1893d..d3ac6bf6 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -9,6 +9,9 @@ import ( "github.com/animenotifier/notify.moe/utils" ) +const maxEpisodes = 26 +const maxEpisodesLongSeries = 5 + // Get anime page. func Get(ctx *aero.Context) string { id := ctx.Get("id") @@ -25,5 +28,16 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Error fetching soundtracks", err) } - return ctx.HTML(components.Anime(anime, tracks, user)) + episodesReversed := false + + if len(anime.Episodes) > maxEpisodes { + episodesReversed = true + anime.Episodes = anime.Episodes[len(anime.Episodes)-maxEpisodesLongSeries-1:] + + for i, j := 0, len(anime.Episodes)-1; i < j; i, j = i+1, j-1 { + anime.Episodes[i], anime.Episodes[j] = anime.Episodes[j], anime.Episodes[i] + } + } + + return ctx.HTML(components.Anime(anime, tracks, user, episodesReversed)) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 104d918e..75972cd1 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,4 +1,4 @@ -component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) +component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, episodesReversed bool) .anime-header(data-id=anime.ID) if anime.Image.Small != "" .anime-image-container @@ -145,7 +145,10 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) SoundTrack(track) if len(anime.Episodes) > 0 - h3.anime-section-name Episodes + if episodesReversed + h3.anime-section-name Latest episodes + else + h3.anime-section-name Episodes table tbody.episodes each episode in anime.Episodes @@ -155,7 +158,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) td.episode-actions a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") RawIcon("google") - td.episode-airing-date-start= episode.AiringDate.StartDateHuman() + td.episode-airing-date-start(title=episode.AiringDate.StartTimeHuman())= episode.AiringDate.StartDateHuman() //- h3.anime-section-name Reviews //- p Coming soon. diff --git a/pages/anime/episode.scarlet b/pages/anime/episode.scarlet index 8fe8f2dc..16310866 100644 --- a/pages/anime/episode.scarlet +++ b/pages/anime/episode.scarlet @@ -12,7 +12,7 @@ flex 1 .episode-airing-date-start - flex-basis 280px + flex-basis 180px text-align right < 800px From c5104db5df94daf3d813c2044ee50624d799010f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 23:26:48 +0200 Subject: [PATCH 049/527] Fixed eps count --- pages/anime/anime.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index d3ac6bf6..c61a6de9 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -32,7 +32,7 @@ func Get(ctx *aero.Context) string { if len(anime.Episodes) > maxEpisodes { episodesReversed = true - anime.Episodes = anime.Episodes[len(anime.Episodes)-maxEpisodesLongSeries-1:] + anime.Episodes = anime.Episodes[len(anime.Episodes)-maxEpisodesLongSeries:] for i, j := 0, len(anime.Episodes)-1; i < j; i, j = i+1, j-1 { anime.Episodes[i], anime.Episodes[j] = anime.Episodes[j], anime.Episodes[i] From ce658fcc38a936d409197550b9be39ca6cb5e6c9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Jun 2017 23:28:55 +0200 Subject: [PATCH 050/527] Added fullscreen for Youtube --- mixins/SoundTrack.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index d7e9f85e..41931775 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -11,7 +11,7 @@ component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia) a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - iframe.lazy(data-src=media.EmbedLink()) + iframe.lazy(data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") .sound-track-footer a.ajax(href=track.Link()) Icon("music") From d8828cf8a34fd9e34f0fbe8c1f2bc32bd6796506 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 01:40:30 +0200 Subject: [PATCH 051/527] Uploaded optimized image --- images/elements/extension-screenshot.png | Bin 50821 -> 38311 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/images/elements/extension-screenshot.png b/images/elements/extension-screenshot.png index 8c29bbe18140b2eb3fb7c8d4a03c2df5935bd7e6..44a2451b6de2a2c0d14d34d118a64b78b225073e 100644 GIT binary patch literal 38311 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

@m4+S)rvYwDUrkUFJRrqfTio;jm)?L6b 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 Date: Thu, 29 Jun 2017 07:02:09 +0200 Subject: [PATCH 052/527] Added shoboi sync --- jobs/sync-shoboi/main.go | 80 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 jobs/sync-shoboi/main.go diff --git a/jobs/sync-shoboi/main.go b/jobs/sync-shoboi/main.go new file mode 100644 index 00000000..e97f0558 --- /dev/null +++ b/jobs/sync-shoboi/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "os" + "time" + + "github.com/animenotifier/arn" + "github.com/animenotifier/shoboi" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Syncing Shoboi Anime") + + // Get a slice of all anime + allAnime, _ := arn.AllAnime() + + // Iterate over the slice + count := 0 + for _, anime := range allAnime { + if sync(anime) { + count++ + } + } + + // Log + color.Green("Successfully added Shoboi IDs for %d anime", count) + + // This is a lazy hack: Wait 5 minutes for goroutines to finish their remaining work. + time.Sleep(5 * time.Minute) + + color.Green("Finished.") +} + +func sync(anime *arn.Anime) bool { + // If we already have the ID, nothing to do here + if anime.GetMapping("shoboi/anime") != "" { + return false + } + + // Log ID and title + os.Stdout.Write([]byte(anime.ID + " | [JP] " + anime.Title.Japanese + " | [EN] " + anime.Title.English)) + + // Search Japanese title + if anime.GetMapping("shoboi/anime") == "" && anime.Title.Japanese != "" { + search(anime, anime.Title.Japanese) + } + + // Search English title + if anime.GetMapping("shoboi/anime") == "" && anime.Title.English != "" { + search(anime, anime.Title.English) + } + + // Did we get the ID? + if anime.GetMapping("shoboi/anime") != "" { + println(color.GreenString("✔")) + time.Sleep(2 * time.Second) + return true + } + + println(color.RedString("✘")) + return false +} + +// Search for a specific title +func search(anime *arn.Anime, title string) { + tid, err := shoboi.SearchAnime(title) + + if err != nil { + color.Red(err.Error()) + return + } + + if tid == "" { + return + } + + // This will start a goroutine that saves the anime + anime.AddMapping("shoboi/anime", tid, "") +} From 524cc4311d6355c3bdaf3f86ee8d626494867982 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 07:09:29 +0200 Subject: [PATCH 053/527] Improved shoboi sync --- jobs/sync-shoboi/main.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/jobs/sync-shoboi/main.go b/jobs/sync-shoboi/main.go index e97f0558..5f871852 100644 --- a/jobs/sync-shoboi/main.go +++ b/jobs/sync-shoboi/main.go @@ -64,17 +64,30 @@ func sync(anime *arn.Anime) bool { // Search for a specific title func search(anime *arn.Anime, title string) { - tid, err := shoboi.SearchAnime(title) + shoboi, err := shoboi.SearchAnime(title) if err != nil { color.Red(err.Error()) return } - if tid == "" { + if shoboi == nil { return } + // Copy titles + if shoboi.TitleJapanese != "" { + anime.Title.Japanese = shoboi.TitleJapanese + } + + if shoboi.TitleHiragana != "" { + anime.Title.Hiragana = shoboi.TitleHiragana + } + + if shoboi.FirstChannel != "" { + anime.FirstChannel = shoboi.FirstChannel + } + // This will start a goroutine that saves the anime - anime.AddMapping("shoboi/anime", tid, "") + anime.AddMapping("shoboi/anime", shoboi.TID, "") } From b5266a3a1723704137a5d44f2ac2fcfdf77e496c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 07:20:55 +0200 Subject: [PATCH 054/527] Improved sync --- jobs/sync-anime/main.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index f3dfd977..f1d5bb08 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -43,7 +43,6 @@ func sync(data *kitsu.Anime) { anime.Type = strings.ToLower(attr.ShowType) anime.Title.Canonical = attr.CanonicalTitle anime.Title.English = attr.Titles.En - anime.Title.Japanese = attr.Titles.JaJp anime.Title.Romaji = attr.Titles.EnJp anime.Title.Synonyms = attr.AbbreviatedTitles anime.Image.Tiny = kitsu.FixImageURL(attr.PosterImage.Tiny) @@ -65,6 +64,17 @@ func sync(data *kitsu.Anime) { anime.Episodes = []*arn.AnimeEpisode{} } + // Prefer Shoboi Japanese titles over Kitsu JP titles + if anime.GetMapping("shoboi/anime") != "" { + // Only take Kitsu title when our JP title is empty + if anime.Title.Japanese == "" { + anime.Title.Japanese = attr.Titles.JaJp + } + } else { + // Update JP title with Kitsu JP title + anime.Title.Japanese = attr.Titles.JaJp + } + // NSFW if attr.Nsfw { anime.NSFW = 1 From f4c18002447dec8cbc12ed069498a4ee554e8bbc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 08:32:46 +0200 Subject: [PATCH 055/527] Added airing dates --- pages/animelist/animelist.go | 5 +--- pages/animelist/animelist.pixy | 4 +++ pages/animelist/animelist.scarlet | 7 +++++ pages/dashboard/dashboard.go | 16 ++++------ pages/embed/embed.go | 5 +--- scripts/AnimeNotifier.ts | 49 +++++++++++++++++++++++++++++++ styles/embedded.scarlet | 11 +++++-- 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/pages/animelist/animelist.go b/pages/animelist/animelist.go index 6851effb..b10a09c9 100644 --- a/pages/animelist/animelist.go +++ b/pages/animelist/animelist.go @@ -2,7 +2,6 @@ package animelist import ( "net/http" - "sort" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -26,9 +25,7 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime list not found", nil) } - sort.Slice(animeList.Items, func(i, j int) bool { - return animeList.Items[i].FinalRating() > animeList.Items[j].FinalRating() - }) + animeList.Sort() return ctx.HTML(components.AnimeList(animeList, user)) } diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 7ea276f8..e042362a 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -3,6 +3,7 @@ component AnimeList(animeList *arn.AnimeList, user *arn.User) thead tr th.anime-list-item-name Anime + th.anime-list-item-airing-date Airing th.anime-list-item-episodes Episodes th.anime-list-item-rating Rating if user != nil @@ -12,6 +13,9 @@ component AnimeList(animeList *arn.AnimeList, user *arn.User) tr.anime-list-item.mountable(title=item.Notes) td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical + td.anime-list-item-airing-date + if item.Anime().UpcomingEpisode() != nil + span.utc-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) td.anime-list-item-episodes .anime-list-item-episodes-watched= item.Episodes .anime-list-item-episodes-separator / diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 76badd5d..93799342 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -51,6 +51,13 @@ .raw-icon margin-bottom -4px +.anime-list-item-airing-date + display none + +> 700px + .anime-list-item-airing-date + display block + < 1100px .anime-list-item-rating display none \ No newline at end of file diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 4b68f9d8..54498f71 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -2,7 +2,6 @@ package dashboard import ( "sort" - "time" "github.com/aerogo/aero" "github.com/aerogo/flow" @@ -68,22 +67,19 @@ func dashboard(ctx *aero.Context) string { } allAnimeInList := objects.([]*arn.Anime) - now := time.Now().UTC().Format(time.RFC3339) for _, anime := range allAnimeInList { if len(upcomingEpisodes) >= maxScheduleItems { break } - for _, episode := range anime.Episodes { - if episode.AiringDate.Start > now { - upcomingEpisodes = append(upcomingEpisodes, &arn.UpcomingEpisode{ - Anime: anime, - Episode: episode, - }) - continue - } + futureEpisodes := anime.UpcomingEpisodes() + + if len(futureEpisodes) == 0 { + continue } + + upcomingEpisodes = append(upcomingEpisodes, futureEpisodes...) } sort.Slice(upcomingEpisodes, func(i, j int) bool { diff --git a/pages/embed/embed.go b/pages/embed/embed.go index cf33a2f9..13009cef 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -2,7 +2,6 @@ package embed import ( "net/http" - "sort" "github.com/aerogo/aero" "github.com/animenotifier/notify.moe/components" @@ -23,9 +22,7 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime list not found", nil) } - sort.Slice(animeList.Items, func(i, j int) bool { - return animeList.Items[i].FinalRating() > animeList.Items[j].FinalRating() - }) + animeList.Sort() return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(animeList, user))) } diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 56052c01..474a1513 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -3,6 +3,23 @@ import { Diff } from "./Diff" import { findAll, delay } from "./utils" import * as actions from "./actions" +var monthNames = [ + "January", "February", "March", + "April", "May", "June", "July", + "August", "September", "October", + "November", "December" +] + +var dayNames = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" +] + export class AnimeNotifier { app: Application visibilityObserver: IntersectionObserver @@ -56,6 +73,38 @@ export class AnimeNotifier { Promise.resolve().then(() => this.mountMountables()) Promise.resolve().then(() => this.assignActions()) Promise.resolve().then(() => this.lazyLoadImages()) + Promise.resolve().then(() => this.displayLocalDates()) + } + + displayLocalDates() { + const oneDay = 24 * 60 * 60 * 1000 + const now = new Date() + + for(let element of findAll("utc-date")) { + let startDate = new Date(element.dataset.startDate) + let endDate = new Date(element.dataset.endDate) + + let h = startDate.getHours() + let m = startDate.getMinutes() + let startTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) + + h = endDate.getHours() + m = endDate.getMinutes() + let endTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) + + let dayDifference = Math.round(Math.abs((startDate.getTime() - now.getTime()) / oneDay)) + let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() + + if(dayDifference > 1) { + element.innerText = dayDifference + " day" + (dayDifference == 1 ? "" : "s") + } else if(dayDifference == 1) { + element.innerText = "Tomorrow" + } else { + element.innerText = "Today" + } + + element.title = "Episode " + element.dataset.episodeNumber + " will be airing " + startTime + " - " + endTime + " your time" + } } reloadContent() { diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet index 07a9d427..92e2416c 100644 --- a/styles/embedded.scarlet +++ b/styles/embedded.scarlet @@ -1,3 +1,5 @@ +remove-margin = 1.1rem + .embedded // Put navigation to the bottom of the screen flex-direction column-reverse !important @@ -6,8 +8,13 @@ display inline-block .anime-list - max-width 500px - margin -1.1rem + // max-width 500px + margin-left calc(remove-margin * -1) + margin-top calc(remove-margin * -1) + width calc(100% + remove-margin * 2) + + td + padding 0.25rem 0.5rem #navigation font-size 0.9rem \ No newline at end of file From 58e7c8a34d938d145436fae0b3f6734c5e837680 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 09:02:06 +0200 Subject: [PATCH 056/527] Improved airing dates in anime view --- pages/anime/anime.pixy | 2 +- scripts/AnimeNotifier.ts | 28 ++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 75972cd1..b412156e 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -158,7 +158,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis td.episode-actions a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") RawIcon("google") - td.episode-airing-date-start(title=episode.AiringDate.StartTimeHuman())= episode.AiringDate.StartDateHuman() + td.episode-airing-date-start.utc-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() //- h3.anime-section-name Reviews //- p Coming soon. diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 474a1513..2b38c29a 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -92,18 +92,30 @@ export class AnimeNotifier { m = endDate.getMinutes() let endTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) - let dayDifference = Math.round(Math.abs((startDate.getTime() - now.getTime()) / oneDay)) + let dayDifference = Math.round((startDate.getTime() - now.getTime()) / oneDay) let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() - if(dayDifference > 1) { - element.innerText = dayDifference + " day" + (dayDifference == 1 ? "" : "s") - } else if(dayDifference == 1) { - element.innerText = "Tomorrow" - } else { - element.innerText = "Today" + let airingVerb = "will be airing" + + switch(dayDifference) { + case 0: + element.innerText = "Today" + case 1: + element.innerText = "Tomorrow" + case -1: + element.innerText = "Yesterday" + default: + let text = Math.abs(dayDifference) + " days" + + if(dayDifference < 0) { + text += " ago" + airingVerb = "aired" + } else { + element.innerText = text + } } - element.title = "Episode " + element.dataset.episodeNumber + " will be airing " + startTime + " - " + endTime + " your time" + element.title = "Episode " + element.dataset.episodeNumber + " " + airingVerb + " " + startTime + " - " + endTime + " your time" } } From b6d00684b70b6fb28e17c6999cfa7e81b7de075f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 09:04:59 +0200 Subject: [PATCH 057/527] Fixed airing dates --- scripts/AnimeNotifier.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 2b38c29a..e4147d86 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -100,10 +100,13 @@ export class AnimeNotifier { switch(dayDifference) { case 0: element.innerText = "Today" + break case 1: element.innerText = "Tomorrow" + break case -1: element.innerText = "Yesterday" + break default: let text = Math.abs(dayDifference) + " days" From a62ebb00e3f397f6dea61e6a05e8cdc6d510980c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 14:12:27 +0200 Subject: [PATCH 058/527] Changed anime list design --- pages/animelist/animelist.pixy | 1 + pages/animelist/animelist.scarlet | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index e042362a..3f8b771a 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -1,4 +1,5 @@ component AnimeList(animeList *arn.AnimeList, user *arn.User) + h2= animeList.User().Nick + "'s collection" table.anime-list thead tr diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 93799342..91d35f99 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -1,6 +1,11 @@ .anime-list vertical width 100% + max-width 1100px + margin 0 auto + // border ui-border + // background rgb(249, 249, 249) + // border-radius 3px tr horizontal From 7f9d599b500ece0f91515bd7e58805bd82273e10 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 14:18:13 +0200 Subject: [PATCH 059/527] Removed owner info from extension --- pages/animelist/animelist.pixy | 2 +- pages/animelist/animelist.scarlet | 3 --- styles/embedded.scarlet | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 3f8b771a..7432aaa0 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -1,5 +1,5 @@ component AnimeList(animeList *arn.AnimeList, user *arn.User) - h2= animeList.User().Nick + "'s collection" + h2.anime-list-owner= animeList.User().Nick + "'s collection" table.anime-list thead tr diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 91d35f99..85c2a68f 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -3,9 +3,6 @@ width 100% max-width 1100px margin 0 auto - // border ui-border - // background rgb(249, 249, 249) - // border-radius 3px tr horizontal diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet index 92e2416c..6c37a093 100644 --- a/styles/embedded.scarlet +++ b/styles/embedded.scarlet @@ -16,5 +16,8 @@ remove-margin = 1.1rem td padding 0.25rem 0.5rem + .anime-list-owner + display none + #navigation font-size 0.9rem \ No newline at end of file From dc4d4ae34c273efd5f6709100997efee61d7d96e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 15:55:04 +0200 Subject: [PATCH 060/527] Added list item status --- mixins/Input.pixy | 2 +- pages/animelist/animelist.scarlet | 2 +- pages/animelistitem/animelistitem.pixy | 8 ++++++++ pages/newthread/newthread.pixy | 2 +- scripts/AnimeNotifier.ts | 11 +++++++++-- scripts/Diff.ts | 5 +++++ styles/include/config.scarlet | 2 +- styles/input.scarlet | 11 +++++++---- 8 files changed, 33 insertions(+), 10 deletions(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index 352bd112..01553a40 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -13,7 +13,7 @@ component InputNumber(id string, value float64, label string, placeholder string label(for=id)= label + ":" 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, values []string) .widget-input label(for=id)= label + ":" select.widget-element.action(id=id, value=value, title=placeholder, data-action="save", data-trigger="change") diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 85c2a68f..4a32d6b2 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -1,7 +1,7 @@ .anime-list vertical width 100% - max-width 1100px + max-width 1200px margin 0 auto tr diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 4c1276c4..7194f5f6 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -5,6 +5,14 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") + label(for="Status") Status: + select.widget-element.action(id="Status", value=item.Status, data-action="save", data-trigger="change") + option(value=arn.AnimeListStatusWatching) Watching + option(value=arn.AnimeListStatusCompleted) Completed + option(value=arn.AnimeListStatusPlanned) Plan to watch + option(value=arn.AnimeListStatusHold) On hold + option(value=arn.AnimeListStatusDropped) Dropped + .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") diff --git a/pages/newthread/newthread.pixy b/pages/newthread/newthread.pixy index bd83d888..08bfb5ef 100644 --- a/pages/newthread/newthread.pixy +++ b/pages/newthread/newthread.pixy @@ -5,7 +5,7 @@ component NewThread(user *arn.User) textarea#text.widget-element(placeholder="Content") - select#tag.widget-element + select#tag.widget-element(value="general") option(value="general") General option(value="news") News option(value="anime") Anime diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e4147d86..836c1f07 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -71,9 +71,16 @@ export class AnimeNotifier { // Update each of these asynchronously Promise.resolve().then(() => this.mountMountables()) - Promise.resolve().then(() => this.assignActions()) Promise.resolve().then(() => this.lazyLoadImages()) Promise.resolve().then(() => this.displayLocalDates()) + Promise.resolve().then(() => this.setSelectBoxValue()) + Promise.resolve().then(() => this.assignActions()) + } + + setSelectBoxValue() { + for(let element of document.getElementsByTagName("select")) { + element.value = element.getAttribute("value") + } } displayLocalDates() { @@ -229,7 +236,7 @@ export class AnimeNotifier { this.unmountMountables() this.loading(true) - return delay(300).then(() => { + return delay(330).then(() => { request .then(html => this.app.setContent(html, true)) .then(() => this.app.markActiveLinks()) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index ea3c2620..342e8186 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -57,6 +57,11 @@ export class Diff { let attrib = elemB.attributes[x] if(attrib.specified) { + // Skip mountables + if(attrib.name == "class" && elemA.classList.contains("mounted")) { + continue + } + elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name)) } } diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 1ca8a328..f6ecc921 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -61,4 +61,4 @@ nav-height = 3.11rem // Timings fade-speed = 200ms -transition-speed = 290ms \ No newline at end of file +transition-speed = 250ms \ No newline at end of file diff --git a/styles/input.scarlet b/styles/input.scarlet index 54afac5e..4270571b 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -62,10 +62,13 @@ button, .button // // box-shadow 0 0 6px alpha(mainColor, 20%) // border 1px solid main-color -// select -// ui-element -// font-size 1rem -// padding 0.5em 1em +select + ui-element + appearance none + -webkit-appearance none + -moz-appearance none + font-size 1rem + // padding 0.5em 1em label width 100% From ecc9b90a2112340ae807a9cb3abdf5b627533688 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 16:04:09 +0200 Subject: [PATCH 061/527] Added patch to automatically set status to correct value --- patches/anime-list-item-status/main.go | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 patches/anime-list-item-status/main.go diff --git a/patches/anime-list-item-status/main.go b/patches/anime-list-item-status/main.go new file mode 100644 index 00000000..97233c47 --- /dev/null +++ b/patches/anime-list-item-status/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Setting list item status to correct value") + + // Get a stream of all anime lists + allAnimeLists, err := arn.StreamAnimeLists() + + if err != nil { + panic(err) + } + + // Iterate over the stream + for animeList := range allAnimeLists { + fmt.Println(animeList.User().Nick) + + for _, item := range animeList.Items { + if item.Status == arn.AnimeListStatusPlanned && item.Episodes > 0 { + item.Status = arn.AnimeListStatusWatching + } + + if item.Anime().Status == "finished" && item.Anime().EpisodeCount != 0 && item.Episodes >= item.Anime().EpisodeCount { + item.Status = arn.AnimeListStatusCompleted + item.Episodes = item.Anime().EpisodeCount + } + } + + err := animeList.Save() + arn.PanicOnError(err) + } + + color.Green("Finished.") +} From 3a1a8ad19b0d4a8593ea29e7cdf9ab377fb43b45 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 16:49:26 +0200 Subject: [PATCH 062/527] Minor changes --- scripts/AnimeNotifier.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 836c1f07..c861a741 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -236,7 +236,8 @@ export class AnimeNotifier { this.unmountMountables() this.loading(true) - return delay(330).then(() => { + // Delay by transition-speed + return delay(300).then(() => { request .then(html => this.app.setContent(html, true)) .then(() => this.app.markActiveLinks()) From f4703fdd5fc16962a625596fd67d0bea7ed7de31 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 18:39:42 +0200 Subject: [PATCH 063/527] Improved forum activity in dashboard --- pages/dashboard/dashboard.go | 27 +++++++++++++++++++++------ pages/dashboard/dashboard.pixy | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 54498f71..f71591c4 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -29,7 +29,8 @@ func Get(ctx *aero.Context) string { // Render the dashboard. func dashboard(ctx *aero.Context) string { - var posts []*arn.Post + var forumPosts []arn.Postable + var forumThreads []arn.Postable var userList interface{} var followingList []*arn.User var soundTracks []*arn.SoundTrack @@ -38,15 +39,21 @@ func dashboard(ctx *aero.Context) string { user := utils.GetUser(ctx) flow.Parallel(func() { - var err error - posts, err = arn.AllPosts() + posts, err := arn.AllPosts() if err != nil { return } - arn.SortPostsLatestFirst(posts) - posts = arn.FilterPostsWithUniqueThreads(posts, maxPosts) + forumPosts = arn.ToPostables(posts) + }, func() { + threads, err := arn.AllThreads() + + if err != nil { + return + } + + forumThreads = arn.ToPostables(threads) }, func() { animeList, err := arn.GetAnimeList(user) @@ -114,5 +121,13 @@ func dashboard(ctx *aero.Context) string { } }) - return ctx.HTML(components.Dashboard(upcomingEpisodes, posts, soundTracks, followingList)) + forumActivity := append(forumPosts, forumThreads...) + + sort.Slice(forumActivity, func(i, j int) bool { + return forumActivity[i].Created() > forumActivity[j].Created() + }) + + forumActivity = arn.FilterPostablesWithUniqueThreads(forumActivity, maxPosts) + + return ctx.HTML(components.Dashboard(upcomingEpisodes, forumActivity, soundTracks, followingList)) } diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 4189ff1d..767c74cf 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -1,4 +1,4 @@ -component Dashboard(schedule []*arn.UpcomingEpisode, posts []*arn.Post, soundTracks []*arn.SoundTrack, following []*arn.User) +component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, soundTracks []*arn.SoundTrack, following []*arn.User) h2.page-title Dash .widgets From dca96b317ef095b3f0d0f300414843fda6cac324 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 20:52:30 +0200 Subject: [PATCH 064/527] Improved dashboard performance --- jobs/forum-activity/main.go | 49 ++++++++++++++++++++++++++++++++++++ jobs/main.go | 1 + pages/dashboard/dashboard.go | 27 ++------------------ pages/profile/profile.go | 2 +- pages/profile/threads.go | 2 +- scripts/AnimeNotifier.ts | 6 +++++ 6 files changed, 60 insertions(+), 27 deletions(-) create mode 100644 jobs/forum-activity/main.go diff --git a/jobs/forum-activity/main.go b/jobs/forum-activity/main.go new file mode 100644 index 00000000..3c2bb808 --- /dev/null +++ b/jobs/forum-activity/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +const maxEntries = 5 + +func main() { + color.Yellow("Caching list of forum activities") + + posts, err := arn.AllPosts() + arn.PanicOnError(err) + + threads, err := arn.AllThreads() + arn.PanicOnError(err) + + arn.SortPostsLatestFirst(posts) + arn.SortThreadsLatestFirst(threads) + + posts = arn.FilterPostsWithUniqueThreads(posts, maxEntries) + + postPostables := arn.ToPostables(posts) + threadPostables := arn.ToPostables(threads) + + allPostables := append(postPostables, threadPostables...) + + arn.SortPostablesLatestFirst(allPostables) + cachedPostables := arn.FilterPostablesWithUniqueThreads(allPostables, maxEntries) + + cache := &arn.ListOfMappedIDs{} + + for _, postable := range cachedPostables { + cache.Append(postable.Type(), postable.ID()) + } + + // // Debug log + // arn.PrettyPrint(cache) + + // // Try to resolve + // for _, r := range arn.ToPostables(cache.Resolve()) { + // color.Green(r.Title()) + // } + + arn.PanicOnError(arn.DB.Set("Cache", "forum activity", cache)) + + color.Green("Finished.") +} diff --git a/jobs/main.go b/jobs/main.go index 9368da98..ebb8afa1 100644 --- a/jobs/main.go +++ b/jobs/main.go @@ -24,6 +24,7 @@ var colorPool = []*color.Color{ var jobs = map[string]time.Duration{ "active-users": 1 * time.Minute, + "forum-activity": 1 * time.Minute, "avatars": 1 * time.Hour, "refresh-track-titles": 10 * time.Hour, "sync-anime": 12 * time.Hour, diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index f71591c4..1b68a404 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -29,8 +29,7 @@ func Get(ctx *aero.Context) string { // Render the dashboard. func dashboard(ctx *aero.Context) string { - var forumPosts []arn.Postable - var forumThreads []arn.Postable + var forumActivity []arn.Postable var userList interface{} var followingList []*arn.User var soundTracks []*arn.SoundTrack @@ -39,21 +38,7 @@ func dashboard(ctx *aero.Context) string { user := utils.GetUser(ctx) flow.Parallel(func() { - posts, err := arn.AllPosts() - - if err != nil { - return - } - - forumPosts = arn.ToPostables(posts) - }, func() { - threads, err := arn.AllThreads() - - if err != nil { - return - } - - forumThreads = arn.ToPostables(threads) + forumActivity, _ = arn.GetForumActivityCached() }, func() { animeList, err := arn.GetAnimeList(user) @@ -121,13 +106,5 @@ func dashboard(ctx *aero.Context) string { } }) - forumActivity := append(forumPosts, forumThreads...) - - sort.Slice(forumActivity, func(i, j int) bool { - return forumActivity[i].Created() > forumActivity[j].Created() - }) - - forumActivity = arn.FilterPostablesWithUniqueThreads(forumActivity, maxPosts) - return ctx.HTML(components.Dashboard(upcomingEpisodes, forumActivity, soundTracks, followingList)) } diff --git a/pages/profile/profile.go b/pages/profile/profile.go index c69c5e57..aaa39dfe 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -38,7 +38,7 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { }, func() { threads = viewUser.Threads() - arn.SortThreadsByDate(threads) + arn.SortThreadsLatestFirst(threads) if len(threads) > maxPosts { threads = threads[:maxPosts] diff --git a/pages/profile/threads.go b/pages/profile/threads.go index 29cdd457..5a0a8e9e 100644 --- a/pages/profile/threads.go +++ b/pages/profile/threads.go @@ -18,7 +18,7 @@ func GetThreadsByUser(ctx *aero.Context) string { } threads := user.Threads() - arn.SortThreadsByDate(threads) + arn.SortThreadsLatestFirst(threads) return ctx.HTML(components.ThreadList(threads)) } diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index c861a741..039b0fc7 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -100,6 +100,12 @@ export class AnimeNotifier { let endTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) let dayDifference = Math.round((startDate.getTime() - now.getTime()) / oneDay) + + if(isNaN(dayDifference)) { + element.style.opacity = "0" + continue + } + let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() let airingVerb = "will be airing" From a05b76a222b8c1f72db2b23cd691408c366ced2b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 21:04:57 +0200 Subject: [PATCH 065/527] Made soundtrack dashboard cleaner --- pages/dashboard/dashboard.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 767c74cf..39760413 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -44,7 +44,7 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound a.widget-element.ajax(href=soundTracks[i].Link()) .widget-element-text Icon("music") - span= soundTracks[i].Media[0].Title + span(title=soundTracks[i].Media[0].Title)= soundTracks[i].Anime()[0].Title.Canonical else .widget-element .widget-element-text From 72087df11c619f06fbe7fcea9d3f9629b0abece8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 21:13:49 +0200 Subject: [PATCH 066/527] Improved schedule --- pages/dashboard/dashboard.pixy | 10 ++++++---- pages/dashboard/dashboard.scarlet | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 39760413..a6f5258c 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -7,11 +7,13 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound for i := 0; i <= 4; i++ if i < len(schedule) - a.widget-element.ajax(href=schedule[i].Anime.Link()) + .widget-element .widget-element-text - Icon("calendar-o") - .schedule-item-title= schedule[i].Anime.Title.Canonical - .schedule-item-episode= "# " + toString(schedule[i].Episode.Number) + a.schedule-item-link.ajax(href=schedule[i].Anime.Link()) + Icon("calendar-o") + .schedule-item-title= schedule[i].Anime.Title.Canonical + .spacer + .schedule-item-date.utc-date(data-start-date=schedule[i].Episode.AiringDate.Start, data-end-date=schedule[i].Episode.AiringDate.End, data-episode-number=schedule[i].Episode.Number) else .widget-element .widget-element-text diff --git a/pages/dashboard/dashboard.scarlet b/pages/dashboard/dashboard.scarlet index a380f5b8..3fd2c3c8 100644 --- a/pages/dashboard/dashboard.scarlet +++ b/pages/dashboard/dashboard.scarlet @@ -1,9 +1,12 @@ +.schedule-item-link, .schedule-item-title - flex 1 white-space nowrap text-overflow ellipsis overflow hidden -.schedule-item-episode - text-align right - flex-basis 50px \ No newline at end of file +.schedule-item-link + horizontal + align-items center + +.schedule-item-date + text-align right \ No newline at end of file From f648f196869eaf053cb50d6e51a7835e1e0e775e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 29 Jun 2017 23:27:21 +0200 Subject: [PATCH 067/527] Improved anime lists --- pages/animelist/animelist.go | 2 +- pages/animelist/animelist.pixy | 33 ++++++++++++++++++++++++++++++- pages/animelist/animelist.scarlet | 6 +++++- pages/embed/embed.go | 4 +++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/pages/animelist/animelist.go b/pages/animelist/animelist.go index b10a09c9..e404d300 100644 --- a/pages/animelist/animelist.go +++ b/pages/animelist/animelist.go @@ -27,5 +27,5 @@ func Get(ctx *aero.Context) string { animeList.Sort() - return ctx.HTML(components.AnimeList(animeList, user)) + return ctx.HTML(components.AnimeLists(animeList.SplitByStatus(), animeList.User(), user)) } diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 7432aaa0..281cc8a0 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -1,5 +1,36 @@ +component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User) + h2.anime-list-owner= viewUser.Nick + "'s collection" + + if len(animeLists[arn.AnimeListStatusWatching].Items) > 0 + .anime-list-container + h3.status-name Watching + AnimeList(animeLists[arn.AnimeListStatusWatching], user) + + if len(animeLists[arn.AnimeListStatusCompleted].Items) > 0 + .anime-list-container + h3.status-name Completed + AnimeList(animeLists[arn.AnimeListStatusCompleted], user) + + if len(animeLists[arn.AnimeListStatusPlanned].Items) > 0 + .anime-list-container + h3.status-name Planned + AnimeList(animeLists[arn.AnimeListStatusPlanned], user) + + if len(animeLists[arn.AnimeListStatusHold].Items) > 0 + .anime-list-container + h3.status-name On hold + AnimeList(animeLists[arn.AnimeListStatusHold], user) + + if len(animeLists[arn.AnimeListStatusDropped].Items) > 0 + .anime-list-container + h3.status-name Dropped + AnimeList(animeLists[arn.AnimeListStatusDropped], user) + + //- for status, animeList := range animeLists + //- h3= status + //- AnimeList(animeList, user) + component AnimeList(animeList *arn.AnimeList, user *arn.User) - h2.anime-list-owner= animeList.User().Nick + "'s collection" table.anime-list thead tr diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 4a32d6b2..fa271dd9 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -1,8 +1,12 @@ -.anime-list +.anime-list-container vertical width 100% max-width 1200px margin 0 auto + margin-bottom 1rem + +.anime-list + vertical tr horizontal diff --git a/pages/embed/embed.go b/pages/embed/embed.go index 13009cef..53ba9158 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/aerogo/aero" + "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" ) @@ -23,6 +24,7 @@ func Get(ctx *aero.Context) string { } animeList.Sort() + watchingList := animeList.SplitByStatus()[arn.AnimeListStatusWatching] - return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(animeList, user))) + return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(watchingList, user))) } From fde816b897f369cdddaabf966a6bc0ae994bd047 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 01:14:24 +0200 Subject: [PATCH 068/527] Added short synonyms to search index --- jobs/search-index/main.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/jobs/search-index/main.go b/jobs/search-index/main.go index e1ef2c6f..b3f6d244 100644 --- a/jobs/search-index/main.go +++ b/jobs/search-index/main.go @@ -37,7 +37,15 @@ func updateAnimeIndex() { } if anime.Title.Japanese != "" { - animeSearchIndex.TextToID[anime.Title.Japanese] = anime.ID + animeSearchIndex.TextToID[strings.ToLower(anime.Title.Japanese)] = anime.ID + } + + for _, synonym := range anime.Title.Synonyms { + synonym = strings.ToLower(synonym) + + if synonym != "" && len(synonym) <= 10 { + animeSearchIndex.TextToID[synonym] = anime.ID + } } } From 7d189b3914ad67bd1295fd67bc201d484b274036 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 17:51:17 +0200 Subject: [PATCH 069/527] Cleanup and analytics --- layout/layout.pixy | 2 + scripts/{actions.ts => Actions.ts} | 0 scripts/AnimeNotifier.ts | 133 ++++++++++++----------------- scripts/DateView.ts | 65 ++++++++++++++ scripts/{utils.ts => Utils.ts} | 0 scripts/main.ts | 5 +- 6 files changed, 122 insertions(+), 83 deletions(-) rename scripts/{actions.ts => Actions.ts} (100%) create mode 100644 scripts/DateView.ts rename scripts/{utils.ts => Utils.ts} (100%) diff --git a/layout/layout.pixy b/layout/layout.pixy index cdcee99a..7cc95122 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -13,6 +13,8 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, conte #content-container main#content.fade!= content LoadingAnimation + if user != nil + #user(data-id=user.ID) script(src="/scripts") component LoadingAnimation diff --git a/scripts/actions.ts b/scripts/Actions.ts similarity index 100% rename from scripts/actions.ts rename to scripts/Actions.ts diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 039b0fc7..70e0df4d 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -1,31 +1,17 @@ import { Application } from "./Application" import { Diff } from "./Diff" -import { findAll, delay } from "./utils" -import * as actions from "./actions" - -var monthNames = [ - "January", "February", "March", - "April", "May", "June", "July", - "August", "September", "October", - "November", "December" -] - -var dayNames = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" -] +import { displayLocalDate } from "./DateView" +import { findAll, delay } from "./Utils" +import * as actions from "./Actions" export class AnimeNotifier { app: Application visibilityObserver: IntersectionObserver + user: HTMLElement constructor(app: Application) { this.app = app + this.user = null if("IntersectionObserver" in window) { // Enable lazy load @@ -52,6 +38,19 @@ export class AnimeNotifier { } } + init() { + document.addEventListener("readystatechange", this.onReadyStateChange.bind(this)) + document.addEventListener("DOMContentLoaded", this.onContentLoaded.bind(this)) + document.addEventListener("keydown", this.onKeyDown.bind(this), false) + window.addEventListener("popstate", this.onPopState.bind(this)) + + if("requestIdleCallback" in window) { + window["requestIdleCallback"](this.onIdle.bind(this)) + } else { + this.onIdle() + } + } + onReadyStateChange() { if(document.readyState !== "interactive") { return @@ -61,22 +60,52 @@ export class AnimeNotifier { } run() { + this.user = this.app.find("user") this.app.content = this.app.find("content") this.app.loading = this.app.find("loading") this.app.run() } onContentLoaded() { + // Stop watching all the objects from the previous page. this.visibilityObserver.disconnect() - // Update each of these asynchronously - Promise.resolve().then(() => this.mountMountables()) - Promise.resolve().then(() => this.lazyLoadImages()) - Promise.resolve().then(() => this.displayLocalDates()) - Promise.resolve().then(() => this.setSelectBoxValue()) + Promise.resolve().then(() => this.mountMountables()), + Promise.resolve().then(() => this.lazyLoadImages()), + Promise.resolve().then(() => this.displayLocalDates()), + Promise.resolve().then(() => this.setSelectBoxValue()), Promise.resolve().then(() => this.assignActions()) } + onIdle() { + if(!this.user) { + return + } + + let analytics = { + general: { + timezoneOffset: new Date().getTimezoneOffset() + }, + screen: { + width: screen.width, + height: screen.height, + availableWidth: screen.availWidth, + availableHeight: screen.availHeight, + pixelRatio: window.devicePixelRatio + }, + system: { + cpuCount: navigator.hardwareConcurrency, + platform: navigator.platform + } + } + + fetch("/api/analytics/new", { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(analytics) + }) + } + setSelectBoxValue() { for(let element of document.getElementsByTagName("select")) { element.value = element.getAttribute("value") @@ -84,54 +113,10 @@ export class AnimeNotifier { } displayLocalDates() { - const oneDay = 24 * 60 * 60 * 1000 const now = new Date() for(let element of findAll("utc-date")) { - let startDate = new Date(element.dataset.startDate) - let endDate = new Date(element.dataset.endDate) - - let h = startDate.getHours() - let m = startDate.getMinutes() - let startTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) - - h = endDate.getHours() - m = endDate.getMinutes() - let endTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) - - let dayDifference = Math.round((startDate.getTime() - now.getTime()) / oneDay) - - if(isNaN(dayDifference)) { - element.style.opacity = "0" - continue - } - - let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() - - let airingVerb = "will be airing" - - switch(dayDifference) { - case 0: - element.innerText = "Today" - break - case 1: - element.innerText = "Tomorrow" - break - case -1: - element.innerText = "Yesterday" - break - default: - let text = Math.abs(dayDifference) + " days" - - if(dayDifference < 0) { - text += " ago" - airingVerb = "aired" - } else { - element.innerText = text - } - } - - element.title = "Episode " + element.dataset.episodeNumber + " " + airingVerb + " " + startTime + " - " + endTime + " your time" + displayLocalDate(element, now) } } @@ -298,14 +283,4 @@ export class AnimeNotifier { e.stopPropagation() } } - - // onResize(e: UIEvent) { - // let hasScrollbar = this.app.content.clientHeight === this.app.content.scrollHeight - - // if(hasScrollbar) { - // this.app.content.classList.add("has-scrollbar") - // } else { - // this.app.content.classList.remove("has-scrollbar") - // } - // } } \ No newline at end of file diff --git a/scripts/DateView.ts b/scripts/DateView.ts new file mode 100644 index 00000000..ad34e15b --- /dev/null +++ b/scripts/DateView.ts @@ -0,0 +1,65 @@ +const oneDay = 24 * 60 * 60 * 1000 + +export var monthNames = [ + "January", "February", "March", + "April", "May", "June", "July", + "August", "September", "October", + "November", "December" +] + +export var dayNames = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" +] + +export function displayLocalDate(element: HTMLElement, now: Date) { + let startDate = new Date(element.dataset.startDate) + let endDate = new Date(element.dataset.endDate) + + let h = startDate.getHours() + let m = startDate.getMinutes() + let startTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) + + h = endDate.getHours() + m = endDate.getMinutes() + let endTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) + + let dayDifference = Math.round((startDate.getTime() - now.getTime()) / oneDay) + + if(isNaN(dayDifference)) { + element.style.opacity = "0" + return + } + + let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() + + let airingVerb = "will be airing" + + switch(dayDifference) { + case 0: + element.innerText = "Today" + break + case 1: + element.innerText = "Tomorrow" + break + case -1: + element.innerText = "Yesterday" + break + default: + let text = Math.abs(dayDifference) + " days" + + if(dayDifference < 0) { + text += " ago" + airingVerb = "aired" + } else { + element.innerText = text + } + } + + element.title = "Episode " + element.dataset.episodeNumber + " " + airingVerb + " " + startTime + " - " + endTime + " your time" +} \ No newline at end of file diff --git a/scripts/utils.ts b/scripts/Utils.ts similarity index 100% rename from scripts/utils.ts rename to scripts/Utils.ts diff --git a/scripts/main.ts b/scripts/main.ts index 05af48fb..8bea2009 100644 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -4,7 +4,4 @@ import { AnimeNotifier } from "./AnimeNotifier" let app = new Application() let arn = new AnimeNotifier(app) -document.addEventListener("readystatechange", arn.onReadyStateChange.bind(arn)) -document.addEventListener("DOMContentLoaded", arn.onContentLoaded.bind(arn)) -document.addEventListener("keydown", arn.onKeyDown.bind(arn), false) -window.addEventListener("popstate", arn.onPopState.bind(arn)) \ No newline at end of file +arn.init() \ No newline at end of file From 02ac46bf8e3dedc7b4de4313f96112ddb93ce390 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 18:12:14 +0200 Subject: [PATCH 070/527] Improved test suite --- tests.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/tests.go b/tests.go index 80a35633..47160cd1 100644 --- a/tests.go +++ b/tests.go @@ -1,6 +1,14 @@ package main -var tests = map[string][]string{ +import ( + "errors" + "reflect" + + "github.com/aerogo/api" + "github.com/animenotifier/arn" +) + +var routeTests = map[string][]string{ // User "/user/:nick": []string{ "/+Akyoto", @@ -14,6 +22,10 @@ var tests = map[string][]string{ "/+Akyoto/posts", }, + "/user/:nick/tracks": []string{ + "/+Akyoto/tracks", + }, + "/user/:nick/animelist": []string{ "/+Akyoto/animelist", }, @@ -43,6 +55,10 @@ var tests = map[string][]string{ "/search/Dragon Ball", }, + "/tracks/:id": []string{ + "/tracks/h0ac8sKkg", + }, + // API "/api/anime/:id": []string{ "/api/anime/1", @@ -92,6 +108,22 @@ var tests = map[string][]string{ "/api/searchindex/Anime", }, + "/api/analytics/:id": []string{ + "/api/analytics/4J6qpK1ve", + }, + + "/api/soundtrack/:id": []string{ + "/api/soundtrack/h0ac8sKkg", + }, + + "/api/soundcloudtosoundtrack/:id": []string{ + "/api/soundcloudtosoundtrack/145918628", + }, + + "/api/youtubetosoundtrack/:id": []string{ + "/api/youtubetosoundtrack/hU2wqJuOIp4", + }, + // Images "/images/avatars/large/:file": []string{ "/images/avatars/large/4J6qpK1ve.webp", @@ -117,9 +149,10 @@ var tests = map[string][]string{ "/images/elements/no-avatar.svg", }, - // Disable + // Disable these tests because they require authorization "/auth/google": nil, "/auth/google/callback": nil, + "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, "/user": nil, @@ -127,9 +160,45 @@ var tests = map[string][]string{ "/extension/embed": nil, } +// API interfaces +var creatable = reflect.TypeOf((*api.Creatable)(nil)).Elem() +var updatable = reflect.TypeOf((*api.Updatable)(nil)).Elem() +var collection = reflect.TypeOf((*api.Collection)(nil)).Elem() + +// Required interface implementations +var interfaceImplementations = map[string][]reflect.Type{ + "User": []reflect.Type{ + updatable, + }, + "Thread": []reflect.Type{ + creatable, + }, + "Post": []reflect.Type{ + creatable, + }, + "SoundTrack": []reflect.Type{ + creatable, + }, + "Analytics": []reflect.Type{ + creatable, + }, + "AnimeList": []reflect.Type{ + collection, + }, +} + func init() { // Specify test routes - for route, examples := range tests { + for route, examples := range routeTests { app.Test(route, examples) } + + // Check interface implementations + for typeName, interfaces := range interfaceImplementations { + for _, requiredInterface := range interfaces { + if !reflect.PtrTo(arn.DB.Type(typeName)).Implements(requiredInterface) { + panic(errors.New(typeName + " does not implement interface " + requiredInterface.Name())) + } + } + } } From 7215dffb3b59e9c36dcb1855e957262331788f4a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 18:36:40 +0200 Subject: [PATCH 071/527] Improved bot --- jobs/discord/main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/jobs/discord/main.go b/jobs/discord/main.go index af3bbf0b..6e3749cd 100644 --- a/jobs/discord/main.go +++ b/jobs/discord/main.go @@ -67,6 +67,14 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { **!tag** [forum tag]`) } + // Has the bot been mentioned? + for _, user := range m.Mentions { + if user.ID == discord.State.User.ID { + s.ChannelMessageSend(m.ChannelID, m.Author.Mention()+" :heart:") + return + } + } + if strings.HasPrefix(m.Content, "!user ") { s.ChannelMessageSend(m.ChannelID, "https://notify.moe/+"+strings.Split(m.Content, " ")[1]) return From db347c7470682bdfddc0e25f59d07f516821adf4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 20:00:56 +0200 Subject: [PATCH 072/527] Anime ratings --- jobs/anime-ratings/main.go | 94 +++++++++++++++++++++++++++++ jobs/main.go | 3 +- jobs/sync-anime/main.go | 11 ++-- mixins/Rating.pixy | 2 +- pages/anime/anime.pixy | 8 +-- patches/clear-anime-ratings/main.go | 10 +++ 6 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 jobs/anime-ratings/main.go create mode 100644 patches/clear-anime-ratings/main.go diff --git a/jobs/anime-ratings/main.go b/jobs/anime-ratings/main.go new file mode 100644 index 00000000..35f053e7 --- /dev/null +++ b/jobs/anime-ratings/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +var ratings = map[string][]*arn.AnimeRating{} +var finalRating = map[string]*arn.AnimeRating{} + +// Note this is using the airing-anime as a template with modfications +// made to it. +func main() { + color.Yellow("Updating anime ratings") + + allAnimeLists, err := arn.AllAnimeLists() + arn.PanicOnError(err) + + for _, animeList := range allAnimeLists { + extractRatings(animeList) + } + + // Calculate + for animeID := range finalRating { + overall := []float64{} + story := []float64{} + visuals := []float64{} + soundtrack := []float64{} + + for _, rating := range ratings[animeID] { + if rating.Overall != 0 { + overall = append(overall, rating.Overall) + } + + if rating.Story != 0 { + story = append(story, rating.Story) + } + + if rating.Visuals != 0 { + visuals = append(visuals, rating.Visuals) + } + + if rating.Soundtrack != 0 { + soundtrack = append(soundtrack, rating.Soundtrack) + } + } + + finalRating[animeID].Overall = average(overall) + finalRating[animeID].Story = average(story) + finalRating[animeID].Visuals = average(visuals) + finalRating[animeID].Soundtrack = average(soundtrack) + } + + // Save + for animeID := range finalRating { + anime, err := arn.GetAnime(animeID) + arn.PanicOnError(err) + anime.Rating = finalRating[animeID] + arn.PanicOnError(anime.Save()) + } + + color.Green("Finished.") +} + +func average(floatSlice []float64) float64 { + if len(floatSlice) == 0 { + return arn.DefaultAverageRating + } + + var sum float64 + + for _, value := range floatSlice { + sum += value + } + + return sum / float64(len(floatSlice)) +} + +func extractRatings(animeList *arn.AnimeList) { + for _, item := range animeList.Items { + if item.Rating.IsNotRated() { + continue + } + + _, found := ratings[item.AnimeID] + + if !found { + ratings[item.AnimeID] = []*arn.AnimeRating{} + finalRating[item.AnimeID] = &arn.AnimeRating{} + } + + ratings[item.AnimeID] = append(ratings[item.AnimeID], item.Rating) + } +} diff --git a/jobs/main.go b/jobs/main.go index ebb8afa1..7bf89335 100644 --- a/jobs/main.go +++ b/jobs/main.go @@ -25,7 +25,8 @@ var colorPool = []*color.Color{ var jobs = map[string]time.Duration{ "active-users": 1 * time.Minute, "forum-activity": 1 * time.Minute, - "avatars": 1 * time.Hour, + "anime-ratings": 15 * time.Minute, + "avatars": 30 * time.Minute, "refresh-track-titles": 10 * time.Hour, "sync-anime": 12 * time.Hour, "popular-anime": 12 * time.Hour, diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/main.go index f1d5bb08..bfc72884 100644 --- a/jobs/sync-anime/main.go +++ b/jobs/sync-anime/main.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "fmt" - "strconv" "strings" "github.com/animenotifier/arn" @@ -83,13 +82,13 @@ func sync(data *kitsu.Anime) { } // Rating - overall, convertError := strconv.ParseFloat(attr.AverageRating, 64) - - if convertError != nil { - overall = 0 + if anime.Rating == nil { + anime.Rating = &arn.AnimeRating{} } - anime.Rating.Overall = overall + if anime.Rating.IsNotRated() { + anime.Rating.Reset() + } // Trailers anime.Trailers = []*arn.ExternalMedia{} diff --git a/mixins/Rating.pixy b/mixins/Rating.pixy index de63ee71..e2a76cd3 100644 --- a/mixins/Rating.pixy +++ b/mixins/Rating.pixy @@ -1,2 +1,2 @@ component Rating(value float64) - .anime-rating= int(value / 10 + 0.5) \ No newline at end of file + .anime-rating= int(value + 0.5) \ No newline at end of file diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index b412156e..ee57a586 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -37,16 +37,16 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis h3.anime-section-name Ratings .anime-rating-categories - .anime-rating-category(title=toString(anime.Rating.Overall / 10)) + .anime-rating-category(title=toString(anime.Rating.Overall)) .anime-rating-category-name Overall Rating(anime.Rating.Overall) - .anime-rating-category(title=toString(anime.Rating.Story / 10)) + .anime-rating-category(title=toString(anime.Rating.Story)) .anime-rating-category-name Story Rating(anime.Rating.Story) - .anime-rating-category(title=toString(anime.Rating.Visuals / 10)) + .anime-rating-category(title=toString(anime.Rating.Visuals)) .anime-rating-category-name Visuals Rating(anime.Rating.Visuals) - .anime-rating-category(title=toString(anime.Rating.Soundtrack / 10)) + .anime-rating-category(title=toString(anime.Rating.Soundtrack)) .anime-rating-category-name Soundtrack Rating(anime.Rating.Soundtrack) diff --git a/patches/clear-anime-ratings/main.go b/patches/clear-anime-ratings/main.go new file mode 100644 index 00000000..359dea2c --- /dev/null +++ b/patches/clear-anime-ratings/main.go @@ -0,0 +1,10 @@ +package main + +import "github.com/animenotifier/arn" + +func main() { + for anime := range arn.MustStreamAnime() { + anime.Rating.Reset() + anime.MustSave() + } +} From df01fb891bd006b94224c79adc437839b3d9e3b6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 21:52:11 +0200 Subject: [PATCH 073/527] New job scheduling times --- jobs/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/main.go b/jobs/main.go index 7bf89335..e98738d6 100644 --- a/jobs/main.go +++ b/jobs/main.go @@ -25,12 +25,12 @@ var colorPool = []*color.Color{ var jobs = map[string]time.Duration{ "active-users": 1 * time.Minute, "forum-activity": 1 * time.Minute, - "anime-ratings": 15 * time.Minute, + "anime-ratings": 10 * time.Minute, + "airing-anime": 10 * time.Minute, "avatars": 30 * time.Minute, "refresh-track-titles": 10 * time.Hour, "sync-anime": 12 * time.Hour, "popular-anime": 12 * time.Hour, - "airing-anime": 12 * time.Hour, "search-index": 12 * time.Hour, } From 633b5942f53ab75bddf1a0d7a48af1087ab8eb29 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 23:52:42 +0200 Subject: [PATCH 074/527] Inline editing --- mixins/Input.pixy | 8 +++---- pages/animelist/animelist.pixy | 31 +++++++++++++++++--------- pages/animelist/animelist.scarlet | 2 +- pages/animelistitem/animelistitem.pixy | 2 +- pages/embed/embed.go | 2 +- scripts/Actions.ts | 26 ++++++++++++++------- scripts/AnimeNotifier.ts | 16 ++++++++++++- utils/user.go | 17 ++++++++++++++ 8 files changed, 78 insertions(+), 26 deletions(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index 01553a40..766b67af 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -1,19 +1,19 @@ component InputText(id string, value string, label string, placeholder string) .widget-input label(for=id)= label + ":" - input.widget-element.action(id=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") + input.widget-element.action(id=id, data-field=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") component InputTextArea(id string, value string, label string, placeholder string) .widget-input label(for=id)= label + ":" - textarea.widget-element.action(id=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")= value + textarea.widget-element.action(id=id, data-field=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")= value 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, step=step, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") + input.widget-element.action(id=id, data-field=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, values []string) .widget-input label(for=id)= label + ":" - select.widget-element.action(id=id, value=value, title=placeholder, data-action="save", data-trigger="change") + select.widget-element.action(id=id, data-field=id, value=value, title=placeholder, data-action="save", data-trigger="change") diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 281cc8a0..cf8eb30c 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -4,58 +4,69 @@ component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, u if len(animeLists[arn.AnimeListStatusWatching].Items) > 0 .anime-list-container h3.status-name Watching - AnimeList(animeLists[arn.AnimeListStatusWatching], user) + AnimeList(animeLists[arn.AnimeListStatusWatching], viewUser, user) if len(animeLists[arn.AnimeListStatusCompleted].Items) > 0 .anime-list-container h3.status-name Completed - AnimeList(animeLists[arn.AnimeListStatusCompleted], user) + AnimeList(animeLists[arn.AnimeListStatusCompleted], viewUser, user) if len(animeLists[arn.AnimeListStatusPlanned].Items) > 0 .anime-list-container h3.status-name Planned - AnimeList(animeLists[arn.AnimeListStatusPlanned], user) + AnimeList(animeLists[arn.AnimeListStatusPlanned], viewUser, user) if len(animeLists[arn.AnimeListStatusHold].Items) > 0 .anime-list-container h3.status-name On hold - AnimeList(animeLists[arn.AnimeListStatusHold], user) + AnimeList(animeLists[arn.AnimeListStatusHold], viewUser, user) if len(animeLists[arn.AnimeListStatusDropped].Items) > 0 .anime-list-container h3.status-name Dropped - AnimeList(animeLists[arn.AnimeListStatusDropped], user) + AnimeList(animeLists[arn.AnimeListStatusDropped], viewUser, user) //- for status, animeList := range animeLists //- h3= status //- AnimeList(animeList, user) -component AnimeList(animeList *arn.AnimeList, user *arn.User) +component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User) table.anime-list thead tr th.anime-list-item-name Anime th.anime-list-item-airing-date Airing th.anime-list-item-episodes Episodes - th.anime-list-item-rating Rating + th.anime-list-item-rating Overall + //- th.anime-list-item-rating Story + //- th.anime-list-item-rating Visuals + //- th.anime-list-item-rating Soundtrack if user != nil th.anime-list-item-actions Actions tbody each item in animeList.Items - tr.anime-list-item.mountable(title=item.Notes) + tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical td.anime-list-item-airing-date if item.Anime().UpcomingEpisode() != nil span.utc-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) td.anime-list-item-episodes - .anime-list-item-episodes-watched= item.Episodes + .anime-list-item-episodes-watched + .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save")= 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() + td.anime-list-item-rating(title="Overall rating") + .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Overall) + //- td.anime-list-item-rating(title="Story rating") + //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Story", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Story) + //- td.anime-list-item-rating(title="Visuals rating") + //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Visuals", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Visuals) + //- td.anime-list-item-rating(title="Soundtrack rating") + //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Soundtrack", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Soundtrack) if user != nil td.anime-list-item-actions a(href=arn.Nyaa.GetLink(item.Anime()), title="Search on Nyaa", target="_blank", rel="noopener") diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index fa271dd9..40d76e0e 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -46,7 +46,7 @@ // margin-bottom -2px .anime-list-item-rating - flex-basis 100px + flex-basis 50px text-align center .anime-list-item-actions diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 7194f5f6..305bb9f6 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -14,7 +14,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. option(value=arn.AnimeListStatusDropped) Dropped .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.Overall", item.Rating.Overall, item.OverallRatingName(), "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") diff --git a/pages/embed/embed.go b/pages/embed/embed.go index 53ba9158..483c3430 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -26,5 +26,5 @@ func Get(ctx *aero.Context) string { animeList.Sort() watchingList := animeList.SplitByStatus()[arn.AnimeListStatusWatching] - return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(watchingList, user))) + return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(watchingList, animeList.User(), user))) } diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 28c7d0b6..a2431b15 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -6,17 +6,18 @@ import { Diff } from "./Diff" export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) { arn.loading(true) + let isContentEditable = input.isContentEditable let obj = {} - let value = input.value + let value = isContentEditable ? input.innerText : input.value - if(input.type === "number") { - if(input.getAttribute("step") === "1") { - obj[input.id] = parseInt(value) + if(input.type === "number" || input.dataset.type === "number") { + if(input.getAttribute("step") === "1" || input.dataset.step === "1") { + obj[input.dataset.field] = parseInt(value) } else { - obj[input.id] = parseFloat(value) + obj[input.dataset.field] = parseFloat(value) } } else { - obj[input.id] = value + obj[input.dataset.field] = value } // console.log(input.type, input.dataset.api, obj, JSON.stringify(obj)) @@ -35,7 +36,11 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE throw "API object not found" } - input.disabled = true + if(isContentEditable) { + input.contentEditable = "false" + } else { + input.disabled = true + } fetch(apiObject.dataset.api, { method: "POST", @@ -51,7 +56,12 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE .catch(console.error) .then(() => { arn.loading(false) - input.disabled = false + + if(isContentEditable) { + input.contentEditable = "true" + } else { + input.disabled = false + } return arn.reloadContent() }) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 70e0df4d..666a87a5 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -265,13 +265,26 @@ export class AnimeNotifier { } onKeyDown(e: KeyboardEvent) { + let activeElement = document.activeElement + // Ignore hotkeys on input elements - switch(document.activeElement.tagName) { + switch(activeElement.tagName) { case "INPUT": case "TEXTAREA": return } + // Disallow Enter key in contenteditables + if(activeElement.getAttribute("contenteditable") === "true" && e.keyCode == 13) { + if("blur" in activeElement) { + activeElement["blur"]() + } + + e.preventDefault() + e.stopPropagation() + return + } + // F = Search if(e.keyCode == 70) { let search = this.app.find("search") as HTMLInputElement @@ -281,6 +294,7 @@ export class AnimeNotifier { e.preventDefault() e.stopPropagation() + return } } } \ No newline at end of file diff --git a/utils/user.go b/utils/user.go index 3e665f7e..74d39297 100644 --- a/utils/user.go +++ b/utils/user.go @@ -9,3 +9,20 @@ import ( func GetUser(ctx *aero.Context) *arn.User { return arn.GetUserFromContext(ctx) } + +// SameUser returns "true" or "false" depending on if the users are the same. +func SameUser(a *arn.User, b *arn.User) string { + if a == nil { + return "false" + } + + if b == nil { + return "false" + } + + if a.ID == b.ID { + return "true" + } + + return "false" +} From d40d4a576a11555a7a3bcb731fecac2a1a9439a5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 30 Jun 2017 23:54:18 +0200 Subject: [PATCH 075/527] Fixed status editing --- pages/animelistitem/animelistitem.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 305bb9f6..b2739f27 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -6,7 +6,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") label(for="Status") Status: - select.widget-element.action(id="Status", value=item.Status, data-action="save", data-trigger="change") + select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") option(value=arn.AnimeListStatusWatching) Watching option(value=arn.AnimeListStatusCompleted) Completed option(value=arn.AnimeListStatusPlanned) Plan to watch From 10ef00a81032f399261c4e77d9db2ed7c39a0349 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 01:16:25 +0200 Subject: [PATCH 076/527] Cleanup --- .../active-users/{main.go => active-users.go} | 0 .../airing-anime/{main.go => airing-anime.go} | 0 .../{main.go => anime-ratings.go} | 0 jobs/avatars/{main.go => avatars.go} | 0 jobs/discord/{main.go => discord.go} | 0 .../{main.go => forum-activity.go} | 0 jobs/{main.go => jobs.go} | 2 +- .../{main.go => popular-anime.go} | 0 .../{main.go => refresh-track-titles.go} | 0 .../search-index/{main.go => search-index.go} | 0 jobs/sync-anime/{main.go => sync-anime.go} | 0 jobs/sync-shoboi/{main.go => sync-shoboi.go} | 0 main.go | 4 ++-- mixins/Navigation.pixy | 4 ++-- .../popular.go => explore/explore.go} | 4 ++-- pages/explore/explore.pixy | 9 +++++++++ pages/popularanime/old/popular.pixy | 16 ---------------- pages/popularanime/old/popular.scarlet | 19 ------------------- pages/popularanime/popular.pixy | 6 ------ .../{main.go => add-anime-lists.go} | 0 .../{main.go => add-empty-episodes.go} | 0 .../{main.go => add-last-seen.go} | 0 .../add-mappings/{main.go => add-mappings.go} | 0 .../{main.go => anime-list-item-status.go} | 0 .../{main.go => clear-anime-ratings.go} | 0 .../{main.go => clear-sessions.go} | 0 .../{main.go => delete-private-data.go} | 0 patches/post-texts/{main.go => post-texts.go} | 0 .../thread-posts/{main.go => thread-posts.go} | 0 .../{main.go => user-references.go} | 0 .../{main.go => video-id-to-service-id.go} | 0 31 files changed, 16 insertions(+), 48 deletions(-) rename jobs/active-users/{main.go => active-users.go} (100%) rename jobs/airing-anime/{main.go => airing-anime.go} (100%) rename jobs/anime-ratings/{main.go => anime-ratings.go} (100%) rename jobs/avatars/{main.go => avatars.go} (100%) rename jobs/discord/{main.go => discord.go} (100%) rename jobs/forum-activity/{main.go => forum-activity.go} (100%) rename jobs/{main.go => jobs.go} (98%) rename jobs/popular-anime/{main.go => popular-anime.go} (100%) rename jobs/refresh-track-titles/{main.go => refresh-track-titles.go} (100%) rename jobs/search-index/{main.go => search-index.go} (100%) rename jobs/sync-anime/{main.go => sync-anime.go} (100%) rename jobs/sync-shoboi/{main.go => sync-shoboi.go} (100%) rename pages/{popularanime/popular.go => explore/explore.go} (81%) create mode 100644 pages/explore/explore.pixy delete mode 100644 pages/popularanime/old/popular.pixy delete mode 100644 pages/popularanime/old/popular.scarlet delete mode 100644 pages/popularanime/popular.pixy rename patches/add-anime-lists/{main.go => add-anime-lists.go} (100%) rename patches/add-empty-episodes/{main.go => add-empty-episodes.go} (100%) rename patches/add-last-seen/{main.go => add-last-seen.go} (100%) rename patches/add-mappings/{main.go => add-mappings.go} (100%) rename patches/anime-list-item-status/{main.go => anime-list-item-status.go} (100%) rename patches/clear-anime-ratings/{main.go => clear-anime-ratings.go} (100%) rename patches/clear-sessions/{main.go => clear-sessions.go} (100%) rename patches/delete-private-data/{main.go => delete-private-data.go} (100%) rename patches/post-texts/{main.go => post-texts.go} (100%) rename patches/thread-posts/{main.go => thread-posts.go} (100%) rename patches/user-references/{main.go => user-references.go} (100%) rename patches/video-id-to-service-id/{main.go => video-id-to-service-id.go} (100%) diff --git a/jobs/active-users/main.go b/jobs/active-users/active-users.go similarity index 100% rename from jobs/active-users/main.go rename to jobs/active-users/active-users.go diff --git a/jobs/airing-anime/main.go b/jobs/airing-anime/airing-anime.go similarity index 100% rename from jobs/airing-anime/main.go rename to jobs/airing-anime/airing-anime.go diff --git a/jobs/anime-ratings/main.go b/jobs/anime-ratings/anime-ratings.go similarity index 100% rename from jobs/anime-ratings/main.go rename to jobs/anime-ratings/anime-ratings.go diff --git a/jobs/avatars/main.go b/jobs/avatars/avatars.go similarity index 100% rename from jobs/avatars/main.go rename to jobs/avatars/avatars.go diff --git a/jobs/discord/main.go b/jobs/discord/discord.go similarity index 100% rename from jobs/discord/main.go rename to jobs/discord/discord.go diff --git a/jobs/forum-activity/main.go b/jobs/forum-activity/forum-activity.go similarity index 100% rename from jobs/forum-activity/main.go rename to jobs/forum-activity/forum-activity.go diff --git a/jobs/main.go b/jobs/jobs.go similarity index 98% rename from jobs/main.go rename to jobs/jobs.go index e98738d6..861992dc 100644 --- a/jobs/main.go +++ b/jobs/jobs.go @@ -27,10 +27,10 @@ var jobs = map[string]time.Duration{ "forum-activity": 1 * time.Minute, "anime-ratings": 10 * time.Minute, "airing-anime": 10 * time.Minute, + "popular-anime": 20 * time.Minute, "avatars": 30 * time.Minute, "refresh-track-titles": 10 * time.Hour, "sync-anime": 12 * time.Hour, - "popular-anime": 12 * time.Hour, "search-index": 12 * time.Hour, } diff --git a/jobs/popular-anime/main.go b/jobs/popular-anime/popular-anime.go similarity index 100% rename from jobs/popular-anime/main.go rename to jobs/popular-anime/popular-anime.go diff --git a/jobs/refresh-track-titles/main.go b/jobs/refresh-track-titles/refresh-track-titles.go similarity index 100% rename from jobs/refresh-track-titles/main.go rename to jobs/refresh-track-titles/refresh-track-titles.go diff --git a/jobs/search-index/main.go b/jobs/search-index/search-index.go similarity index 100% rename from jobs/search-index/main.go rename to jobs/search-index/search-index.go diff --git a/jobs/sync-anime/main.go b/jobs/sync-anime/sync-anime.go similarity index 100% rename from jobs/sync-anime/main.go rename to jobs/sync-anime/sync-anime.go diff --git a/jobs/sync-shoboi/main.go b/jobs/sync-shoboi/sync-shoboi.go similarity index 100% rename from jobs/sync-shoboi/main.go rename to jobs/sync-shoboi/sync-shoboi.go diff --git a/main.go b/main.go index c5c0127e..21e4e7d6 100644 --- a/main.go +++ b/main.go @@ -16,13 +16,13 @@ import ( "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/embed" + "github.com/animenotifier/notify.moe/pages/explore" "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/login" "github.com/animenotifier/notify.moe/pages/music" "github.com/animenotifier/notify.moe/pages/newsoundtrack" "github.com/animenotifier/notify.moe/pages/newthread" - "github.com/animenotifier/notify.moe/pages/popularanime" "github.com/animenotifier/notify.moe/pages/posts" "github.com/animenotifier/notify.moe/pages/profile" "github.com/animenotifier/notify.moe/pages/search" @@ -57,7 +57,7 @@ func configure(app *aero.Application) *aero.Application { // Ajax routes app.Ajax("/", dashboard.Get) - app.Ajax("/anime", popularanime.Get) + app.Ajax("/anime", explore.Get) app.Ajax("/anime/:id", anime.Get) app.Ajax("/anime/:id/edit", editanime.Get) app.Ajax("/forum", forums.Get) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index dbf99a82..d57e9aa8 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -15,7 +15,7 @@ component LoggedOutMenu .extra-navigation NavigationButton("Users", "/users", "globe") - NavigationButton("Airing", "/airing", "th") + NavigationButton("Anime", "/anime", "th") NavigationButton("Login", "/login", "sign-in") component LoggedInMenu(user *arn.User) @@ -34,7 +34,7 @@ component LoggedInMenu(user *arn.User) NavigationButton("Users", "/users", "globe") .extra-navigation - NavigationButton("Airing", "/airing", "th") + NavigationButton("Anime", "/anime", "th") NavigationButton("Settings", "/settings", "cog") diff --git a/pages/popularanime/popular.go b/pages/explore/explore.go similarity index 81% rename from pages/popularanime/popular.go rename to pages/explore/explore.go index 1d80e7ef..18896117 100644 --- a/pages/popularanime/popular.go +++ b/pages/explore/explore.go @@ -1,4 +1,4 @@ -package popularanime +package explore import ( "net/http" @@ -16,5 +16,5 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) } - return ctx.HTML(components.PopularAnime(animeList)) + return ctx.HTML(components.ExploreAnime(nil, nil, animeList)) } diff --git a/pages/explore/explore.pixy b/pages/explore/explore.pixy new file mode 100644 index 00000000..744423f0 --- /dev/null +++ b/pages/explore/explore.pixy @@ -0,0 +1,9 @@ +component ExploreAnime(overall []*arn.Anime, story []*arn.Anime, airing []*arn.Anime) + h2 Highest Rating + AnimeGrid(overall) + + h2 Best Story + AnimeGrid(story) + + h2 Currently Airing + AnimeGrid(airing) \ No newline at end of file diff --git a/pages/popularanime/old/popular.pixy b/pages/popularanime/old/popular.pixy deleted file mode 100644 index 4d650c83..00000000 --- a/pages/popularanime/old/popular.pixy +++ /dev/null @@ -1,16 +0,0 @@ -component OldPopularAnime(popularAnime []*arn.Anime, titleCount int, animeCount int) - //- h2 Anime - - //- #search-container - //- input#search(type="text", placeholder="Search...", onkeyup="$.searchAnime();", onfocus="this.select();", disabled="disabled", data-count=titleCount, data-anime-count=animeCount) - - //- #search-results-container - //- #search-results - - //- if popularAnime != nil - //- h3.popular-title Popular - - //- .popular-anime-list - //- each anime in popularAnime - //- a.popular-anime.ajax(href="/anime/" + toString(anime.ID), title=anime.Title.Romaji + " (" + arn.Plural(anime.Watching(), "user") + " watching)") - //- img.anime-image.popular-anime-image(src=anime.Image, alt=anime.Title.Romaji) \ No newline at end of file diff --git a/pages/popularanime/old/popular.scarlet b/pages/popularanime/old/popular.scarlet deleted file mode 100644 index 6ee9f2dd..00000000 --- a/pages/popularanime/old/popular.scarlet +++ /dev/null @@ -1,19 +0,0 @@ -// .popular-title -// text-align center - -// .popular-anime-list -// display flex -// flex-flow row wrap -// justify-content center - -// .popular-anime -// padding 0.5em -// display block - -// .popular-anime-image -// width 100px !important -// height 141px !important -// border-radius 3px -// object-fit cover -// default-transition -// shadow-up \ No newline at end of file diff --git a/pages/popularanime/popular.pixy b/pages/popularanime/popular.pixy deleted file mode 100644 index 3d5aeec7..00000000 --- a/pages/popularanime/popular.pixy +++ /dev/null @@ -1,6 +0,0 @@ -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/patches/add-anime-lists/main.go b/patches/add-anime-lists/add-anime-lists.go similarity index 100% rename from patches/add-anime-lists/main.go rename to patches/add-anime-lists/add-anime-lists.go diff --git a/patches/add-empty-episodes/main.go b/patches/add-empty-episodes/add-empty-episodes.go similarity index 100% rename from patches/add-empty-episodes/main.go rename to patches/add-empty-episodes/add-empty-episodes.go diff --git a/patches/add-last-seen/main.go b/patches/add-last-seen/add-last-seen.go similarity index 100% rename from patches/add-last-seen/main.go rename to patches/add-last-seen/add-last-seen.go diff --git a/patches/add-mappings/main.go b/patches/add-mappings/add-mappings.go similarity index 100% rename from patches/add-mappings/main.go rename to patches/add-mappings/add-mappings.go diff --git a/patches/anime-list-item-status/main.go b/patches/anime-list-item-status/anime-list-item-status.go similarity index 100% rename from patches/anime-list-item-status/main.go rename to patches/anime-list-item-status/anime-list-item-status.go diff --git a/patches/clear-anime-ratings/main.go b/patches/clear-anime-ratings/clear-anime-ratings.go similarity index 100% rename from patches/clear-anime-ratings/main.go rename to patches/clear-anime-ratings/clear-anime-ratings.go diff --git a/patches/clear-sessions/main.go b/patches/clear-sessions/clear-sessions.go similarity index 100% rename from patches/clear-sessions/main.go rename to patches/clear-sessions/clear-sessions.go diff --git a/patches/delete-private-data/main.go b/patches/delete-private-data/delete-private-data.go similarity index 100% rename from patches/delete-private-data/main.go rename to patches/delete-private-data/delete-private-data.go diff --git a/patches/post-texts/main.go b/patches/post-texts/post-texts.go similarity index 100% rename from patches/post-texts/main.go rename to patches/post-texts/post-texts.go diff --git a/patches/thread-posts/main.go b/patches/thread-posts/thread-posts.go similarity index 100% rename from patches/thread-posts/main.go rename to patches/thread-posts/thread-posts.go diff --git a/patches/user-references/main.go b/patches/user-references/user-references.go similarity index 100% rename from patches/user-references/main.go rename to patches/user-references/user-references.go diff --git a/patches/video-id-to-service-id/main.go b/patches/video-id-to-service-id/video-id-to-service-id.go similarity index 100% rename from patches/video-id-to-service-id/main.go rename to patches/video-id-to-service-id/video-id-to-service-id.go From 750302b60eab6d1fced1820b6dc637a91a12a3d5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 02:14:14 +0200 Subject: [PATCH 077/527] Improved exploration --- jobs/popular-anime/popular-anime.go | 54 ++++++++++++++++---------- main.go | 6 +-- mixins/Navigation.pixy | 4 +- pages/airing/airing.go | 21 ---------- pages/airing/airing.pixy | 3 -- pages/anime/anime.pixy | 5 ++- pages/animelistitem/animelistitem.pixy | 2 +- pages/best/best.go | 46 ++++++++++++++++++++++ pages/best/best.pixy | 15 +++++++ pages/explore/explore.go | 13 ++++--- pages/explore/explore.pixy | 12 ++---- 11 files changed, 114 insertions(+), 67 deletions(-) delete mode 100644 pages/airing/airing.go delete mode 100644 pages/airing/airing.pixy create mode 100644 pages/best/best.go create mode 100644 pages/best/best.pixy diff --git a/jobs/popular-anime/popular-anime.go b/jobs/popular-anime/popular-anime.go index 9a1a0143..10a5483a 100644 --- a/jobs/popular-anime/popular-anime.go +++ b/jobs/popular-anime/popular-anime.go @@ -14,37 +14,49 @@ const maxPopularAnime = 10 func main() { color.Yellow("Caching popular anime") + // Fetch all anime animeList, err := arn.AllAnime() + arn.PanicOnError(err) - if err != nil { - color.Red("Failed fetching anime channel") - color.Red(err.Error()) - return - } - + // Overall sort.Slice(animeList, func(i, j int) bool { return animeList[i].Rating.Overall > animeList[j].Rating.Overall }) - // Change size of anime list to 10 - animeList = animeList[:maxPopularAnime] + saveAs(animeList[:maxPopularAnime], "best anime overall") - // Convert to small anime list + // Story + sort.Slice(animeList, func(i, j int) bool { + return animeList[i].Rating.Story > animeList[j].Rating.Story + }) + + saveAs(animeList[:maxPopularAnime], "best anime story") + + // Visuals + sort.Slice(animeList, func(i, j int) bool { + return animeList[i].Rating.Visuals > animeList[j].Rating.Visuals + }) + + saveAs(animeList[:maxPopularAnime], "best anime visuals") + + // Soundtrack + sort.Slice(animeList, func(i, j int) bool { + return animeList[i].Rating.Soundtrack > animeList[j].Rating.Soundtrack + }) + + saveAs(animeList[:maxPopularAnime], "best anime soundtrack") + + // Done. + color.Green("Finished.") +} + +// Convert to ListOfIDs and save in cache. +func saveAs(list []*arn.Anime, cacheKey string) { cache := &arn.ListOfIDs{} - for _, anime := range animeList { + for _, anime := range list { cache.IDList = append(cache.IDList, anime.ID) } - println(len(cache.IDList)) - - saveErr := arn.DB.Set("Cache", "popular anime", cache) - - if saveErr != nil { - color.Red("Error saving popular anime") - color.Red(saveErr.Error()) - return - } - - color.Green("Finished.") + arn.PanicOnError(arn.DB.Set("Cache", cacheKey, cache)) } diff --git a/main.go b/main.go index 21e4e7d6..36de93c6 100644 --- a/main.go +++ b/main.go @@ -9,10 +9,10 @@ import ( "github.com/animenotifier/notify.moe/layout" "github.com/animenotifier/notify.moe/middleware" "github.com/animenotifier/notify.moe/pages/admin" - "github.com/animenotifier/notify.moe/pages/airing" "github.com/animenotifier/notify.moe/pages/anime" "github.com/animenotifier/notify.moe/pages/animelist" "github.com/animenotifier/notify.moe/pages/animelistitem" + "github.com/animenotifier/notify.moe/pages/best" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/embed" @@ -57,9 +57,10 @@ func configure(app *aero.Application) *aero.Application { // Ajax routes app.Ajax("/", dashboard.Get) - app.Ajax("/anime", explore.Get) app.Ajax("/anime/:id", anime.Get) app.Ajax("/anime/:id/edit", editanime.Get) + app.Ajax("/best/anime", best.Get) + app.Ajax("/explore", explore.Get) app.Ajax("/forum", forums.Get) app.Ajax("/forum/:tag", forum.Get) app.Ajax("/threads/:id", threads.Get) @@ -81,7 +82,6 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/search/:term", search.Get) app.Ajax("/users", users.Get) app.Ajax("/login", login.Get) - app.Ajax("/airing", airing.Get) app.Ajax("/webdev", webdev.Get) app.Ajax("/extension/embed", embed.Get) // app.Ajax("/genres", genres.Get) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index d57e9aa8..c70c4178 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -15,7 +15,7 @@ component LoggedOutMenu .extra-navigation NavigationButton("Users", "/users", "globe") - NavigationButton("Anime", "/anime", "th") + NavigationButton("Explore", "/explore", "th") NavigationButton("Login", "/login", "sign-in") component LoggedInMenu(user *arn.User) @@ -34,7 +34,7 @@ component LoggedInMenu(user *arn.User) NavigationButton("Users", "/users", "globe") .extra-navigation - NavigationButton("Anime", "/anime", "th") + NavigationButton("Explore", "/explore", "th") NavigationButton("Settings", "/settings", "cog") diff --git a/pages/airing/airing.go b/pages/airing/airing.go deleted file mode 100644 index 05f68638..00000000 --- a/pages/airing/airing.go +++ /dev/null @@ -1,21 +0,0 @@ -package airing - -import ( - "github.com/aerogo/aero" - "github.com/animenotifier/arn" - "github.com/animenotifier/notify.moe/components" -) - -// Get ... -func Get(ctx *aero.Context) string { - var cache arn.ListOfIDs - err := arn.DB.GetObject("Cache", "airing anime", &cache) - - airing, err := arn.GetAiringAnimeCached() - - if err != nil { - return ctx.Error(500, "Couldn't fetch airing anime", err) - } - - return ctx.HTML(components.Airing(airing)) -} diff --git a/pages/airing/airing.pixy b/pages/airing/airing.pixy deleted file mode 100644 index 81f5d4c6..00000000 --- a/pages/airing/airing.pixy +++ /dev/null @@ -1,3 +0,0 @@ -component Airing(animeList []*arn.Anime) - h2.page-title(title=toString(len(animeList)) + " anime") Airing - AnimeGrid(animeList) \ No newline at end of file diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index ee57a586..854807bd 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -38,7 +38,10 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis h3.anime-section-name Ratings .anime-rating-categories .anime-rating-category(title=toString(anime.Rating.Overall)) - .anime-rating-category-name Overall + if anime.Status == "upcoming" + .anime-rating-category-name Hype + else + .anime-rating-category-name Overall Rating(anime.Rating.Overall) .anime-rating-category(title=toString(anime.Rating.Story)) .anime-rating-category-name Story diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index b2739f27..d4b95315 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -14,7 +14,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. option(value=arn.AnimeListStatusDropped) Dropped .anime-list-item-rating-edit - InputNumber("Rating.Overall", item.Rating.Overall, item.OverallRatingName(), "Overall rating on a scale of 0 to 10", "0", "10", "0.1") + InputNumber("Rating.Overall", item.Rating.Overall, arn.OverallRatingName(item.Episodes), "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") diff --git a/pages/best/best.go b/pages/best/best.go new file mode 100644 index 00000000..e5ce245d --- /dev/null +++ b/pages/best/best.go @@ -0,0 +1,46 @@ +package best + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +const maxEntries = 7 + +// Get search page. +func Get(ctx *aero.Context) string { + overall, err := arn.GetListOfAnimeCached("best anime overall") + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) + } + + story, err := arn.GetListOfAnimeCached("best anime story") + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) + } + + visuals, err := arn.GetListOfAnimeCached("best anime visuals") + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) + } + + soundtrack, err := arn.GetListOfAnimeCached("best anime soundtrack") + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) + } + + airing, err := arn.GetAiringAnimeCached() + + if err != nil { + return ctx.Error(500, "Couldn't fetch airing anime", err) + } + + return ctx.HTML(components.BestAnime(overall[:maxEntries], story[:maxEntries], visuals[:maxEntries], soundtrack[:maxEntries], airing[:maxEntries])) +} diff --git a/pages/best/best.pixy b/pages/best/best.pixy new file mode 100644 index 00000000..3455ed32 --- /dev/null +++ b/pages/best/best.pixy @@ -0,0 +1,15 @@ +component BestAnime(overall []*arn.Anime, story []*arn.Anime, visuals []*arn.Anime, soundtrack []*arn.Anime, airing []*arn.Anime) + h2 Currently Airing + AnimeGrid(airing) + + h2 Best Overall + AnimeGrid(overall) + + h2 Best Story + AnimeGrid(story) + + h2 Best Visuals + AnimeGrid(visuals) + + h2 Best Soundtrack + AnimeGrid(soundtrack) \ No newline at end of file diff --git a/pages/explore/explore.go b/pages/explore/explore.go index 18896117..529bbb15 100644 --- a/pages/explore/explore.go +++ b/pages/explore/explore.go @@ -1,20 +1,21 @@ package explore import ( - "net/http" - "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" ) -// Get search page. +// Get ... func Get(ctx *aero.Context) string { - animeList, err := arn.GetPopularAnimeCached() + var cache arn.ListOfIDs + err := arn.DB.GetObject("Cache", "airing anime", &cache) + + airing, err := arn.GetAiringAnimeCached() if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) + return ctx.Error(500, "Couldn't fetch airing anime", err) } - return ctx.HTML(components.ExploreAnime(nil, nil, animeList)) + return ctx.HTML(components.Airing(airing)) } diff --git a/pages/explore/explore.pixy b/pages/explore/explore.pixy index 744423f0..202a7817 100644 --- a/pages/explore/explore.pixy +++ b/pages/explore/explore.pixy @@ -1,9 +1,3 @@ -component ExploreAnime(overall []*arn.Anime, story []*arn.Anime, airing []*arn.Anime) - h2 Highest Rating - AnimeGrid(overall) - - h2 Best Story - AnimeGrid(story) - - h2 Currently Airing - AnimeGrid(airing) \ No newline at end of file +component Airing(animeList []*arn.Anime) + h2.page-title(title=toString(len(animeList)) + " anime") Explore + AnimeGrid(animeList) \ No newline at end of file From 49e87607685c51fd544cd360dc582f939c9eb11d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 02:28:50 +0200 Subject: [PATCH 078/527] Higher scan priority --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 36de93c6..d82885fb 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,8 @@ func configure(app *aero.Application) *aero.Application { // Domain if arn.IsDevelopment() { app.Config.Domain = "beta.notify.moe" + } else { + arn.DB.SetScanPriority("high") } // Authentication From 05ac41670760239e37a5b6849765636461fcb27e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 02:32:35 +0200 Subject: [PATCH 079/527] Slightly increased transition speed --- styles/include/config.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index f6ecc921..5f713fb0 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -61,4 +61,4 @@ nav-height = 3.11rem // Timings fade-speed = 200ms -transition-speed = 250ms \ No newline at end of file +transition-speed = 270ms \ No newline at end of file From 355ec3202bd250f90865e90b08e15ea1461746ac Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 13:35:21 +0200 Subject: [PATCH 080/527] Improved airing dates --- pages/anime/episode.scarlet | 2 +- pages/animelist/animelist.pixy | 4 +- scripts/AnimeNotifier.ts | 2 + scripts/DateView.ts | 133 ++++++++++++++++++++++++++------- scripts/Utils.ts | 9 ++- styles/embedded.scarlet | 2 +- 6 files changed, 115 insertions(+), 37 deletions(-) diff --git a/pages/anime/episode.scarlet b/pages/anime/episode.scarlet index 16310866..f8353360 100644 --- a/pages/anime/episode.scarlet +++ b/pages/anime/episode.scarlet @@ -12,7 +12,7 @@ flex 1 .episode-airing-date-start - flex-basis 180px + flex-basis 150px text-align right < 800px diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index cf8eb30c..5952e544 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -53,14 +53,14 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User span.utc-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) td.anime-list-item-episodes .anime-list-item-episodes-watched - .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save")= item.Episodes + .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save", title="Click to edit episodes")= 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(title="Overall rating") - .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Overall) + .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save", title="Click to edit overall rating")= fmt.Sprintf("%.1f", item.Rating.Overall) //- td.anime-list-item-rating(title="Story rating") //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Story", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Story) //- td.anime-list-item-rating(title="Visuals rating") diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 666a87a5..f51a828b 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -131,8 +131,10 @@ export class AnimeNotifier { loading(isLoading: boolean) { if(isLoading) { + document.body.style.cursor = "progress" this.app.loading.classList.remove(this.app.fadeOutClass) } else { + document.body.style.cursor = "auto" this.app.loading.classList.add(this.app.fadeOutClass) } } diff --git a/scripts/DateView.ts b/scripts/DateView.ts index ad34e15b..2c7360df 100644 --- a/scripts/DateView.ts +++ b/scripts/DateView.ts @@ -1,4 +1,12 @@ -const oneDay = 24 * 60 * 60 * 1000 +import { plural } from "./Utils" + +const oneSecond = 1000 +const oneMinute = 60 * oneSecond +const oneHour = 60 * oneMinute +const oneDay = 24 * oneHour +const oneWeek = 7 * oneDay +const oneMonth = 30 * oneDay +const oneYear = 365.25 * oneDay export var monthNames = [ "January", "February", "March", @@ -17,6 +25,40 @@ export var dayNames = [ "Saturday" ] +function getRemainingTime(remaining: number): string { + let remainingAbs = Math.abs(remaining) + + if(remainingAbs >= oneYear) { + return plural(Math.round(remaining / oneYear), "year") + } + + if(remainingAbs >= oneMonth) { + return plural(Math.round(remaining / oneMonth), "month") + } + + if(remainingAbs >= oneWeek) { + return plural(Math.round(remaining / oneWeek), "week") + } + + if(remainingAbs >= oneDay) { + return plural(Math.round(remaining / oneDay), "day") + } + + if(remainingAbs >= oneHour) { + return plural(Math.round(remaining / oneHour), " hour") + } + + if(remainingAbs >= oneMinute) { + return plural(Math.round(remaining / oneMinute), " minute") + } + + if(remainingAbs >= oneSecond) { + return plural(Math.round(remaining / oneSecond), " second") + } + + return "Just now" +} + export function displayLocalDate(element: HTMLElement, now: Date) { let startDate = new Date(element.dataset.startDate) let endDate = new Date(element.dataset.endDate) @@ -29,37 +71,70 @@ export function displayLocalDate(element: HTMLElement, now: Date) { m = endDate.getMinutes() let endTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) - let dayDifference = Math.round((startDate.getTime() - now.getTime()) / oneDay) - - if(isNaN(dayDifference)) { - element.style.opacity = "0" - return - } - - let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() - let airingVerb = "will be airing" - switch(dayDifference) { - case 0: - element.innerText = "Today" - break - case 1: - element.innerText = "Tomorrow" - break - case -1: - element.innerText = "Yesterday" - break - default: - let text = Math.abs(dayDifference) + " days" - if(dayDifference < 0) { - text += " ago" - airingVerb = "aired" - } else { - element.innerText = text - } + // let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() + + let remaining = startDate.getTime() - now.getTime() + let remainingString = getRemainingTime(remaining) + + // Add "ago" if the date is in the past + if(remainingString.startsWith("-")) { + remainingString = remainingString.substring(1) + " ago" } - element.title = "Episode " + element.dataset.episodeNumber + " " + airingVerb + " " + startTime + " - " + endTime + " your time" + element.innerText = remainingString + + // let remainingString = seconds + plural(seconds, 'second') + + // let days = seconds / (60 * ) + // if(Math.abs(days) >= 1) { + // remainingString = plural(days, 'day') + // } else { + // let hours = arn.inHours(now, timeStamp) + // if(Math.abs(hours) >= 1) { + // remainingString = plural(hours, 'hour') + // } else { + // let minutes = arn.inMinutes(now, timeStamp) + // if(Math.abs(minutes) >= 1) { + // remainingString = plural(minutes, 'minute') + // } else { + // let seconds = arn.inSeconds(now, timeStamp) + // remainingString = plural(seconds, 'second') + // } + // } + // } + + // if(isNaN(oneHour)) { + // element.style.opacity = "0" + // return + // } + + // switch(Math.floor(dayDifference)) { + // case 0: + // element.innerText = "Today" + // break + // case 1: + // element.innerText = "Tomorrow" + // break + // case -1: + // element.innerText = "Yesterday" + // break + // default: + // let text = Math.abs(dayDifference) + " days" + + // if(dayDifference < 0) { + // text += " ago" + // airingVerb = "aired" + // } else { + // element.innerText = text + // } + // } + + if(remaining < 0) { + airingVerb = "aired" + } + + element.title = "Episode " + element.dataset.episodeNumber + " " + airingVerb + " " + dayNames[startDate.getDay()] + " from " + startTime + " - " + endTime } \ No newline at end of file diff --git a/scripts/Utils.ts b/scripts/Utils.ts index b97017d3..b349dc84 100644 --- a/scripts/Utils.ts +++ b/scripts/Utils.ts @@ -1,7 +1,4 @@ -export function* findAll(className: string) { - // getElementsByClassName failed for some reason. - // TODO: Test getElementsByClassName again. - // let elements = document.querySelectorAll("." + className) +export function* findAll(className: string): IterableIterator { let elements = document.getElementsByClassName(className) for(let i = 0; i < elements.length; ++i) { @@ -11,4 +8,8 @@ export function* findAll(className: string) { export function delay(millis: number, value?: T): Promise { return new Promise(resolve => setTimeout(() => resolve(value), millis)) +} + +export function plural(count: number, singular: string): string { + return (count === 1 || count === -1) ? (count + ' ' + singular) : (count + ' ' + singular + 's') } \ No newline at end of file diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet index 6c37a093..c7bf7e9a 100644 --- a/styles/embedded.scarlet +++ b/styles/embedded.scarlet @@ -14,7 +14,7 @@ remove-margin = 1.1rem width calc(100% + remove-margin * 2) td - padding 0.25rem 0.5rem + padding 0.4rem 0.8rem .anime-list-owner display none From 9c0518c278ad25926f6ca163e949d7ce17abaea4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 13:45:23 +0200 Subject: [PATCH 081/527] Removed dropped entries from schedule --- pages/dashboard/dashboard.go | 2 ++ pages/embed/embed.go | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 1b68a404..cdb780a3 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -46,6 +46,8 @@ func dashboard(ctx *aero.Context) string { return } + animeList = animeList.WatchingAndPlanned() + var keys []string for _, item := range animeList.Items { diff --git a/pages/embed/embed.go b/pages/embed/embed.go index 483c3430..7a3ffd57 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/aerogo/aero" - "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" ) @@ -23,8 +22,8 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime list not found", nil) } - animeList.Sort() - watchingList := animeList.SplitByStatus()[arn.AnimeListStatusWatching] + watchingList := animeList.WatchingAndPlanned() + watchingList.Sort() return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(watchingList, animeList.User(), user))) } From 715686ddee05647e5c7ca393c3fc5e52bbeeee26 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 13:53:55 +0200 Subject: [PATCH 082/527] Removed title --- pages/animelist/animelist.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 5952e544..cf8eb30c 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -53,14 +53,14 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User span.utc-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) td.anime-list-item-episodes .anime-list-item-episodes-watched - .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save", title="Click to edit episodes")= item.Episodes + .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save")= 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(title="Overall rating") - .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save", title="Click to edit overall rating")= fmt.Sprintf("%.1f", item.Rating.Overall) + .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Overall) //- td.anime-list-item-rating(title="Story rating") //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Story", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Story) //- td.anime-list-item-rating(title="Visuals rating") From 6e41c08e601730fa92b2db85cf88df7542e1645a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 14:03:10 +0200 Subject: [PATCH 083/527] Made /api page --- main.go | 2 ++ pages/admin/admin.go | 13 +------------ pages/admin/admin.pixy | 16 ++-------------- pages/apiview/api.go | 22 ++++++++++++++++++++++ pages/apiview/api.pixy | 14 ++++++++++++++ 5 files changed, 41 insertions(+), 26 deletions(-) create mode 100644 pages/apiview/api.go create mode 100644 pages/apiview/api.pixy diff --git a/main.go b/main.go index d82885fb..d519efce 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "github.com/animenotifier/notify.moe/pages/anime" "github.com/animenotifier/notify.moe/pages/animelist" "github.com/animenotifier/notify.moe/pages/animelistitem" + "github.com/animenotifier/notify.moe/pages/apiview" "github.com/animenotifier/notify.moe/pages/best" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" @@ -59,6 +60,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/", dashboard.Get) app.Ajax("/anime/:id", anime.Get) app.Ajax("/anime/:id/edit", editanime.Get) + app.Ajax("/api", apiview.Get) app.Ajax("/best/anime", best.Get) app.Ajax("/explore", explore.Get) app.Ajax("/forum", forums.Get) diff --git a/pages/admin/admin.go b/pages/admin/admin.go index df83aa5b..9ef619e3 100644 --- a/pages/admin/admin.go +++ b/pages/admin/admin.go @@ -1,10 +1,7 @@ package admin import ( - "sort" - "github.com/aerogo/aero" - "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" ) @@ -17,13 +14,5 @@ func Get(ctx *aero.Context) string { return ctx.Redirect("/") } - types := []string{} - - for typeName := range arn.DB.Types() { - types = append(types, typeName) - } - - sort.Strings(types) - - return ctx.HTML(components.Admin(user, types)) + return ctx.HTML(components.Admin(user)) } diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index 2fd92953..5790361d 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -1,4 +1,4 @@ -component Admin(user *arn.User, types []string) +component Admin(user *arn.User) h2.page-title Admin Panel h3 Server @@ -16,16 +16,4 @@ component Admin(user *arn.User, types []string) td= runtime.NumGoroutine() tr td Go version: - td= runtime.Version() - - h3 Types - table - //- thead - //- tr - //- th Table - tbody - each typeName in types - tr - td= typeName - td - a(href="/api/" + strings.ToLower(typeName) + "/")= "/api/" + strings.ToLower(typeName) + "/" \ No newline at end of file + td= runtime.Version() \ No newline at end of file diff --git a/pages/apiview/api.go b/pages/apiview/api.go new file mode 100644 index 00000000..31213c6e --- /dev/null +++ b/pages/apiview/api.go @@ -0,0 +1,22 @@ +package apiview + +import ( + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +// Get api page. +func Get(ctx *aero.Context) string { + types := []string{} + + for typeName := range arn.DB.Types() { + types = append(types, typeName) + } + + sort.Strings(types) + + return ctx.HTML(components.API(types)) +} diff --git a/pages/apiview/api.pixy b/pages/apiview/api.pixy new file mode 100644 index 00000000..daf887a8 --- /dev/null +++ b/pages/apiview/api.pixy @@ -0,0 +1,14 @@ +component API(types []string) + h2.page-title API + + h3 Types + table + //- thead + //- tr + //- th Table + tbody + each typeName in types + tr + td= typeName + td + a(href="/api/" + strings.ToLower(typeName) + "/")= "/api/" + strings.ToLower(typeName) + "/" \ No newline at end of file From a58e99c950f78da66eb25c4a98965b272f9b9383 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 14:54:53 +0200 Subject: [PATCH 084/527] Minor changes --- pages/apiview/api.pixy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/apiview/api.pixy b/pages/apiview/api.pixy index daf887a8..1a3e0df3 100644 --- a/pages/apiview/api.pixy +++ b/pages/apiview/api.pixy @@ -1,7 +1,6 @@ component API(types []string) - h2.page-title API + h3 API - h3 Types table //- thead //- tr From 861c052610ff39f62c74d001a37d735897d05897 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 18:33:20 +0200 Subject: [PATCH 085/527] Minor changes --- patches/import-anilist/import-anilist.go | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 patches/import-anilist/import-anilist.go diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go new file mode 100644 index 00000000..fc774215 --- /dev/null +++ b/patches/import-anilist/import-anilist.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + arn.PanicOnError(arn.AniList.Authorize()) +} From f575410de327caca7600473fac93444652db812f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 18:34:30 +0200 Subject: [PATCH 086/527] Improved navigation --- mixins/Navigation.pixy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index c70c4178..a8f6f0a6 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -26,15 +26,16 @@ component LoggedInMenu(user *arn.User) NavigationButton("Dash", "/", "dashboard") NavigationButton("Profile", "/+", "user") NavigationButton("Forum", "/forum", "comment") - NavigationButton("Music", "/music", "headphones") + + .extra-navigation + NavigationButton("Music", "/music", "headphones") FuzzySearch .extra-navigation NavigationButton("Users", "/users", "globe") - .extra-navigation - NavigationButton("Explore", "/explore", "th") + NavigationButton("Explore", "/explore", "th") NavigationButton("Settings", "/settings", "cog") From f3c7ee272ac3b74460f4c1e95400475f22b1150e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 21:29:33 +0200 Subject: [PATCH 087/527] Basic Anilist importer --- patches/import-anilist/import-anilist.go | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go index fc774215..9d9864ee 100644 --- a/patches/import-anilist/import-anilist.go +++ b/patches/import-anilist/import-anilist.go @@ -1,9 +1,85 @@ package main import ( + "fmt" + "strings" + "github.com/animenotifier/arn" + "github.com/fatih/color" ) +var allAnime []*arn.Anime + +func init() { + allAnime, _ = arn.AllAnime() +} + func main() { arn.PanicOnError(arn.AniList.Authorize()) + println(arn.AniList.AccessToken) + + user, _ := arn.GetUserByNick("Boltasar") + animeList, err := arn.AniList.GetAnimeList(user) + arn.PanicOnError(err) + + importList(animeList.Lists.Watching) + importList(animeList.Lists.Completed) +} + +func importList(animeListItems []*arn.AniListAnimeListItem) { + imported := []*arn.Anime{} + + for _, item := range animeListItems { + anime := findAnimeByName(item.Anime) + if anime != nil { + // fmt.Println(item.Anime.TitleRomaji, "=>", anime.Title.Romaji) + imported = append(imported, anime) + } + } + + color.Green("%d / %d", len(imported), len(animeListItems)) +} + +func findAnimeByName(search *arn.AniListAnime) *arn.Anime { + var mostSimilar *arn.Anime + var similarity float64 + + for _, anime := range allAnime { + anime.Title.Japanese = strings.Replace(anime.Title.Japanese, "2ndシーズン", "2", 1) + anime.Title.Romaji = strings.Replace(anime.Title.Romaji, " 2nd Season", " 2", 1) + search.TitleJapanese = strings.TrimSpace(strings.Replace(search.TitleJapanese, "2ndシーズン", "2", 1)) + search.TitleRomaji = strings.TrimSpace(strings.Replace(search.TitleRomaji, " 2nd Season", " 2", 1)) + + titleSimilarity := arn.StringSimilarity(anime.Title.Romaji, search.TitleRomaji) + + if strings.ToLower(anime.Title.Japanese) == strings.ToLower(search.TitleJapanese) { + titleSimilarity += 1.0 + } + + if strings.ToLower(anime.Title.Romaji) == strings.ToLower(search.TitleRomaji) { + titleSimilarity += 1.0 + } + + if strings.ToLower(anime.Title.English) == strings.ToLower(search.TitleEnglish) { + titleSimilarity += 1.0 + } + + if titleSimilarity > similarity { + mostSimilar = anime + similarity = titleSimilarity + } + } + + if mostSimilar.EpisodeCount != search.TotalEpisodes { + similarity -= 0.02 + } + + if similarity >= 0.92 { + fmt.Printf("MATCH: %s => %s (%.2f)\n", search.TitleRomaji, mostSimilar.Title.Romaji, similarity) + return mostSimilar + } + + color.Red("MISMATCH: %s => %s (%.2f)", search.TitleRomaji, mostSimilar.Title.Romaji, similarity) + + return nil } From f3654c1aadd9b8daa4dc59726b09595c7aa1a866 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 23:08:12 +0200 Subject: [PATCH 088/527] Improved importer --- .../delete-custom-anime.go | 15 ++++ .../import-old/import-old-matches.go | 79 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 patches/delete-anilist-mappings/delete-custom-anime.go create mode 100644 patches/import-anilist/import-old/import-old-matches.go diff --git a/patches/delete-anilist-mappings/delete-custom-anime.go b/patches/delete-anilist-mappings/delete-custom-anime.go new file mode 100644 index 00000000..1160d3f8 --- /dev/null +++ b/patches/delete-anilist-mappings/delete-custom-anime.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + for anime := range arn.MustStreamAnime() { + providerID := anime.GetMapping("anilist/anime") + _, err := arn.DB.Delete("AniListToAnime", providerID) + arn.PanicOnError(err) + anime.RemoveMapping("anilist/anime", providerID) + arn.PanicOnError(anime.Save()) + } +} diff --git a/patches/import-anilist/import-old/import-old-matches.go b/patches/import-anilist/import-old/import-old-matches.go new file mode 100644 index 00000000..d10288a3 --- /dev/null +++ b/patches/import-anilist/import-old/import-old-matches.go @@ -0,0 +1,79 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strconv" + + "github.com/animenotifier/arn" +) + +// OldMatch ... +type OldMatch struct { + ID int `json:"id"` + ProviderID int `json:"providerId"` + Title string `json:"title"` + ProviderTitle string `json:"providerTitle"` + Similarity float64 `json:"similarity"` + Edited string `json:"edited"` + EditedBy string `json:"editedBy"` +} + +// ProviderMatch ... +type ProviderMatch struct { + AnimeID string `json:"animeId"` + ProviderID string `json:"providerId"` + Edited string `json:"edited"` + EditedBy string `json:"editedBy"` +} + +// AniListToAnime ... +type AniListToAnime ProviderMatch + +func main() { + matches := []OldMatch{} + data, _ := ioutil.ReadFile("MatchKitsu.json") + json.Unmarshal(data, &matches) + + for _, match := range matches { + // Custom anime in 3.0 + if match.ID >= 1000000 { + continue + } + + // New match type + newMatch := &ProviderMatch{ + AnimeID: strconv.Itoa(match.ProviderID), + ProviderID: strconv.Itoa(match.ID), + Edited: match.Edited, + EditedBy: match.EditedBy, + } + + // Get anime + anime, err := arn.GetAnime(newMatch.AnimeID) + + if err != nil { + continue + } + + anime.Mappings = append(anime.Mappings, &arn.Mapping{ + Service: "anilist/anime", + ServiceID: newMatch.ProviderID, + Created: newMatch.Edited, + CreatedBy: newMatch.EditedBy, + }) + + // Save + fmt.Println(anime.Title.Canonical) + arn.PanicOnError(anime.Save()) + arn.PanicOnError(arn.DB.Set("AniListToAnime", newMatch.ProviderID, newMatch)) + } +} + +// AnilistToAnime +/* +AnimeID +ProviderID + +*/ From f3d55e224a79b7b50bba8048041af974858fb207 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 23:19:57 +0200 Subject: [PATCH 089/527] Anilist changes --- pages/editanime/editanime.pixy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pages/editanime/editanime.pixy b/pages/editanime/editanime.pixy index adbf6c61..b969e041 100644 --- a/pages/editanime/editanime.pixy +++ b/pages/editanime/editanime.pixy @@ -4,7 +4,8 @@ component EditAnime(anime *arn.Anime) .widgets .widget(data-api="/api/anime/" + anime.ID) h3.anime-section-name Mappings - InputText("Custom:ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on http://cal.syoboi.jp") + InputText("Custom:ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on cal.syoboi.jp") + InputText("Custom:AniListID", anime.GetMapping("anilist/anime"), "AniList ID", "ID on anilist.co") .buttons a.button.ajax(href="/anime/" + anime.ID) From dfa72d5df0ef9986225254babc7b7825bdcaa3ac Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Jul 2017 23:33:41 +0200 Subject: [PATCH 090/527] Improved importer --- .../import-old/import-old-matches.go | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/patches/import-anilist/import-old/import-old-matches.go b/patches/import-anilist/import-old/import-old-matches.go index d10288a3..de9f5629 100644 --- a/patches/import-anilist/import-old/import-old-matches.go +++ b/patches/import-anilist/import-old/import-old-matches.go @@ -11,26 +11,15 @@ import ( // OldMatch ... type OldMatch struct { - ID int `json:"id"` - ProviderID int `json:"providerId"` - Title string `json:"title"` - ProviderTitle string `json:"providerTitle"` - Similarity float64 `json:"similarity"` - Edited string `json:"edited"` - EditedBy string `json:"editedBy"` + ID int `json:"id"` + ServiceID int `json:"providerId"` + Title string `json:"title"` + ServiceTitle string `json:"providerTitle"` + Similarity float64 `json:"similarity"` + Edited string `json:"edited"` + EditedBy string `json:"editedBy"` } -// ProviderMatch ... -type ProviderMatch struct { - AnimeID string `json:"animeId"` - ProviderID string `json:"providerId"` - Edited string `json:"edited"` - EditedBy string `json:"editedBy"` -} - -// AniListToAnime ... -type AniListToAnime ProviderMatch - func main() { matches := []OldMatch{} data, _ := ioutil.ReadFile("MatchKitsu.json") @@ -43,9 +32,10 @@ func main() { } // New match type - newMatch := &ProviderMatch{ - AnimeID: strconv.Itoa(match.ProviderID), - ProviderID: strconv.Itoa(match.ID), + newMatch := &arn.AniListToAnime{ + AnimeID: strconv.Itoa(match.ServiceID), + ServiceID: strconv.Itoa(match.ID), + Similarity: match.Similarity, Edited: match.Edited, EditedBy: match.EditedBy, } @@ -57,9 +47,13 @@ func main() { continue } + if anime.GetMapping("anilist/anime") != "" { + continue + } + anime.Mappings = append(anime.Mappings, &arn.Mapping{ Service: "anilist/anime", - ServiceID: newMatch.ProviderID, + ServiceID: newMatch.ServiceID, Created: newMatch.Edited, CreatedBy: newMatch.EditedBy, }) @@ -67,13 +61,13 @@ func main() { // Save fmt.Println(anime.Title.Canonical) arn.PanicOnError(anime.Save()) - arn.PanicOnError(arn.DB.Set("AniListToAnime", newMatch.ProviderID, newMatch)) + arn.PanicOnError(arn.DB.Set("AniListToAnime", newMatch.ServiceID, newMatch)) } } // AnilistToAnime /* AnimeID -ProviderID +ServiceID */ From 9950adbf9e0f034ea3b45f91cfec201c35fecc4b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 00:27:58 +0200 Subject: [PATCH 091/527] Minor changes --- patches/import-anilist/import-anilist.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go index 9d9864ee..e2f90083 100644 --- a/patches/import-anilist/import-anilist.go +++ b/patches/import-anilist/import-anilist.go @@ -1,7 +1,7 @@ package main import ( - "fmt" + "strconv" "strings" "github.com/animenotifier/arn" @@ -30,7 +30,7 @@ func importList(animeListItems []*arn.AniListAnimeListItem) { imported := []*arn.Anime{} for _, item := range animeListItems { - anime := findAnimeByName(item.Anime) + anime := findAniListAnime(item.Anime) if anime != nil { // fmt.Println(item.Anime.TitleRomaji, "=>", anime.Title.Romaji) imported = append(imported, anime) @@ -40,7 +40,14 @@ func importList(animeListItems []*arn.AniListAnimeListItem) { color.Green("%d / %d", len(imported), len(animeListItems)) } -func findAnimeByName(search *arn.AniListAnime) *arn.Anime { +func findAniListAnime(search *arn.AniListAnime) *arn.Anime { + match, err := arn.GetAniListToAnime(strconv.Itoa(search.ID)) + + if err == nil { + anime, _ := arn.GetAnime(match.AnimeID) + return anime + } + var mostSimilar *arn.Anime var similarity float64 @@ -75,7 +82,7 @@ func findAnimeByName(search *arn.AniListAnime) *arn.Anime { } if similarity >= 0.92 { - fmt.Printf("MATCH: %s => %s (%.2f)\n", search.TitleRomaji, mostSimilar.Title.Romaji, similarity) + // fmt.Printf("MATCH: %s => %s (%.2f)\n", search.TitleRomaji, mostSimilar.Title.Romaji, similarity) return mostSimilar } From 3309e5fb63995ef5af696a7af10007c71a0a7ed5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 00:49:05 +0200 Subject: [PATCH 092/527] Improved search index --- jobs/search-index/search-index.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jobs/search-index/search-index.go b/jobs/search-index/search-index.go index b3f6d244..8a16e1d9 100644 --- a/jobs/search-index/search-index.go +++ b/jobs/search-index/search-index.go @@ -40,6 +40,10 @@ func updateAnimeIndex() { animeSearchIndex.TextToID[strings.ToLower(anime.Title.Japanese)] = anime.ID } + if anime.Title.Canonical != "" { + animeSearchIndex.TextToID[strings.ToLower(anime.Title.Canonical)] = anime.ID + } + for _, synonym := range anime.Title.Synonyms { synonym = strings.ToLower(synonym) From f9bbf7e6db2cd84cd0f499b9ce1e1416ddd36e8f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 01:44:10 +0200 Subject: [PATCH 093/527] Improved import --- .../import-anilist-user.go | 42 ++++++++++ .../import-old/import-old-matches.go | 0 patches/import-anilist/import-anilist.go | 83 +++---------------- 3 files changed, 52 insertions(+), 73 deletions(-) create mode 100644 patches/import-anilist-user/import-anilist-user.go rename patches/{import-anilist => import-anilist-user}/import-old/import-old-matches.go (100%) diff --git a/patches/import-anilist-user/import-anilist-user.go b/patches/import-anilist-user/import-anilist-user.go new file mode 100644 index 00000000..80a13763 --- /dev/null +++ b/patches/import-anilist-user/import-anilist-user.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +var userName = "Akyoto" +var allAnime []*arn.Anime + +func init() { + allAnime, _ = arn.AllAnime() +} + +func main() { + arn.PanicOnError(arn.AniList.Authorize()) + println(arn.AniList.AccessToken) + + user, _ := arn.GetUserByNick(userName) + animeList, err := arn.AniList.GetAnimeList(user) + arn.PanicOnError(err) + + importList(animeList.Lists.Watching) + importList(animeList.Lists.Completed) +} + +func importList(animeListItems []*arn.AniListAnimeListItem) { + imported := []*arn.Anime{} + + for _, item := range animeListItems { + anime := arn.FindAniListAnime(item.Anime, allAnime) + + if anime != nil { + fmt.Println(item.Anime.TitleRomaji, "=>", anime.Title.Romaji) + imported = append(imported, anime) + } + } + + color.Green("%d / %d", len(imported), len(animeListItems)) +} diff --git a/patches/import-anilist/import-old/import-old-matches.go b/patches/import-anilist-user/import-old/import-old-matches.go similarity index 100% rename from patches/import-anilist/import-old/import-old-matches.go rename to patches/import-anilist-user/import-old/import-old-matches.go diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go index e2f90083..da1a1276 100644 --- a/patches/import-anilist/import-anilist.go +++ b/patches/import-anilist/import-anilist.go @@ -1,92 +1,29 @@ package main import ( - "strconv" - "strings" + "fmt" "github.com/animenotifier/arn" "github.com/fatih/color" ) -var allAnime []*arn.Anime - -func init() { - allAnime, _ = arn.AllAnime() -} - func main() { arn.PanicOnError(arn.AniList.Authorize()) - println(arn.AniList.AccessToken) + color.Green(arn.AniList.AccessToken) - user, _ := arn.GetUserByNick("Boltasar") - animeList, err := arn.AniList.GetAnimeList(user) + allAnime, err := arn.AllAnime() arn.PanicOnError(err) - importList(animeList.Lists.Watching) - importList(animeList.Lists.Completed) -} + count := 0 + stream := arn.AniList.StreamAnime() -func importList(animeListItems []*arn.AniListAnimeListItem) { - imported := []*arn.Anime{} + for aniListAnime := range stream { + anime := arn.FindAniListAnime(aniListAnime, allAnime) - for _, item := range animeListItems { - anime := findAniListAnime(item.Anime) if anime != nil { - // fmt.Println(item.Anime.TitleRomaji, "=>", anime.Title.Romaji) - imported = append(imported, anime) + fmt.Println(aniListAnime.TitleRomaji, "=>", anime.Title.Canonical) } - } - color.Green("%d / %d", len(imported), len(animeListItems)) -} - -func findAniListAnime(search *arn.AniListAnime) *arn.Anime { - match, err := arn.GetAniListToAnime(strconv.Itoa(search.ID)) - - if err == nil { - anime, _ := arn.GetAnime(match.AnimeID) - return anime - } - - var mostSimilar *arn.Anime - var similarity float64 - - for _, anime := range allAnime { - anime.Title.Japanese = strings.Replace(anime.Title.Japanese, "2ndシーズン", "2", 1) - anime.Title.Romaji = strings.Replace(anime.Title.Romaji, " 2nd Season", " 2", 1) - search.TitleJapanese = strings.TrimSpace(strings.Replace(search.TitleJapanese, "2ndシーズン", "2", 1)) - search.TitleRomaji = strings.TrimSpace(strings.Replace(search.TitleRomaji, " 2nd Season", " 2", 1)) - - titleSimilarity := arn.StringSimilarity(anime.Title.Romaji, search.TitleRomaji) - - if strings.ToLower(anime.Title.Japanese) == strings.ToLower(search.TitleJapanese) { - titleSimilarity += 1.0 - } - - if strings.ToLower(anime.Title.Romaji) == strings.ToLower(search.TitleRomaji) { - titleSimilarity += 1.0 - } - - if strings.ToLower(anime.Title.English) == strings.ToLower(search.TitleEnglish) { - titleSimilarity += 1.0 - } - - if titleSimilarity > similarity { - mostSimilar = anime - similarity = titleSimilarity - } - } - - if mostSimilar.EpisodeCount != search.TotalEpisodes { - similarity -= 0.02 - } - - if similarity >= 0.92 { - // fmt.Printf("MATCH: %s => %s (%.2f)\n", search.TitleRomaji, mostSimilar.Title.Romaji, similarity) - return mostSimilar - } - - color.Red("MISMATCH: %s => %s (%.2f)", search.TitleRomaji, mostSimilar.Title.Romaji, similarity) - - return nil + count++ + } } From f82e1ea9619c3ae06b3eda1ddedcfaa8c4c812e8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 14:03:56 +0200 Subject: [PATCH 094/527] Refresh episodes --- jobs/refresh-episodes/refresh-episodes.go | 20 ++++++++++++++++++++ jobs/sync-shoboi/sync-shoboi.go | 3 +-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 jobs/refresh-episodes/refresh-episodes.go diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go new file mode 100644 index 00000000..0f48c369 --- /dev/null +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Refreshing episode information for each anime.") + + for anime := range arn.MustStreamAnime() { + err := anime.RefreshEpisodes() + + if err != nil { + color.Red(err.Error()) + } + } + + color.Green("Finished.") +} diff --git a/jobs/sync-shoboi/sync-shoboi.go b/jobs/sync-shoboi/sync-shoboi.go index 5f871852..341eb451 100644 --- a/jobs/sync-shoboi/sync-shoboi.go +++ b/jobs/sync-shoboi/sync-shoboi.go @@ -1,7 +1,6 @@ package main import ( - "os" "time" "github.com/animenotifier/arn" @@ -39,7 +38,7 @@ func sync(anime *arn.Anime) bool { } // Log ID and title - os.Stdout.Write([]byte(anime.ID + " | [JP] " + anime.Title.Japanese + " | [EN] " + anime.Title.English)) + print(anime.ID + " | [JP] " + anime.Title.Japanese + " | [EN] " + anime.Title.Canonical) // Search Japanese title if anime.GetMapping("shoboi/anime") == "" && anime.Title.Japanese != "" { From 771c16066aebcc92e2445f35d962865f9e32e654 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 14:08:25 +0200 Subject: [PATCH 095/527] Improved refresh --- jobs/refresh-episodes/refresh-episodes.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 0f48c369..65ab886a 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -1,6 +1,10 @@ package main import ( + "fmt" + "strconv" + "strings" + "github.com/animenotifier/arn" "github.com/fatih/color" ) @@ -9,10 +13,18 @@ func main() { color.Yellow("Refreshing episode information for each anime.") for anime := range arn.MustStreamAnime() { + episodeCount := len(anime.Episodes) + err := anime.RefreshEpisodes() if err != nil { + if strings.Contains(err.Error(), "missing a Shoboi ID") { + continue + } + color.Red(err.Error()) + } else { + fmt.Println(anime.ID, "|", anime.Title.Canonical, "+"+strconv.Itoa(len(anime.Episodes)-episodeCount)) } } From 367d31aadb34ba0eb36e423a9fe004a734ee0ab4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 15:11:42 +0200 Subject: [PATCH 096/527] Added new background jobs --- jobs/jobs.go | 3 +++ jobs/refresh-osu/refresh-osu.go | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 jobs/refresh-osu/refresh-osu.go diff --git a/jobs/jobs.go b/jobs/jobs.go index 861992dc..74e4662f 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -29,7 +29,10 @@ var jobs = map[string]time.Duration{ "airing-anime": 10 * time.Minute, "popular-anime": 20 * time.Minute, "avatars": 30 * time.Minute, + "sync-shoboi": 8 * time.Hour, + "refresh-episodes": 10 * time.Hour, "refresh-track-titles": 10 * time.Hour, + "refresh-osu": 12 * time.Hour, "sync-anime": 12 * time.Hour, "search-index": 12 * time.Hour, } diff --git a/jobs/refresh-osu/refresh-osu.go b/jobs/refresh-osu/refresh-osu.go new file mode 100644 index 00000000..e2ac7d19 --- /dev/null +++ b/jobs/refresh-osu/refresh-osu.go @@ -0,0 +1,27 @@ +package main + +import ( + "time" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Refreshing osu information") + + ticker := time.NewTicker(500 * time.Millisecond) + + for user := range arn.MustStreamUsers() { + // Get osu info + if user.RefreshOsuInfo() == nil { + arn.PrettyPrint(user.Accounts.Osu) + user.Save() + } + + // Wait for rate limiter + <-ticker.C + } + + color.Green("Finished.") +} From 464a6ec26be018a1d0b6c53587f21e8298ea6905 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 16:24:26 +0200 Subject: [PATCH 097/527] Added osu account info --- pages/profile/profile.pixy | 4 ++-- pages/profile/profile.scarlet | 7 +++---- pages/settings/settings.pixy | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index aebde1c9..05125822 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -23,9 +23,9 @@ component ProfileHeader(viewUser *arn.User, user *arn.User) a(href=viewUser.WebsiteURL(), target="_blank", rel="nofollow")= viewUser.Website if viewUser.Accounts.Osu.Nick != "" && viewUser.Accounts.Osu.PP >= 1000 - p.profile-field.osu(title="osu! performance points") + p.profile-field.osu(title="osu! Level " + toString(int(viewUser.Accounts.Osu.Level)) + " | Accuracy: " + fmt.Sprintf("%.1f", viewUser.Accounts.Osu.Accuracy) + "%") Icon("trophy") - span= toString(int(viewUser.Accounts.Osu.PP)) + " pp" + a(href="https://osu.ppy.sh/u/" + viewUser.Accounts.Osu.Nick, target="_blank", rel="noopener")= toString(int(viewUser.Accounts.Osu.PP)) + " pp" //- if viewUser.dataEditCount //- p.profile-field.editor-contribution(title="Anime data modifications") diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 80742404..0ec8b188 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -20,6 +20,9 @@ profile-boot-duration = 2s .profile-field text-align center + a + color white + < 600px .profile vertical @@ -80,10 +83,6 @@ 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/settings/settings.pixy b/pages/settings/settings.pixy index d00e2e1c..879de0af 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -19,6 +19,7 @@ component Settings(user *arn.User) InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net") InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") + InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh") //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title From 326c4161aa947bb40260332d26c39fee48c0a7be Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 17:51:17 +0200 Subject: [PATCH 098/527] Improved search --- jobs/search-index/search-index.go | 4 +++- main_test.go | 2 +- pages/search/search.go | 6 +++--- pages/search/search.pixy | 5 +++-- rewrite.go | 18 +++++++++++++++++- scripts/Actions.ts | 8 ++++---- scripts/AnimeNotifier.ts | 14 ++++++++++++-- 7 files changed, 43 insertions(+), 14 deletions(-) diff --git a/jobs/search-index/search-index.go b/jobs/search-index/search-index.go index 8a16e1d9..fd52dfe6 100644 --- a/jobs/search-index/search-index.go +++ b/jobs/search-index/search-index.go @@ -36,7 +36,9 @@ func updateAnimeIndex() { animeSearchIndex.TextToID[strings.ToLower(anime.Title.English)] = anime.ID } - if anime.Title.Japanese != "" { + // Make sure we only include Japanese titles that actually contain unicode letters + // because otherwise they might overlap with the English titles. + if anime.Title.Japanese != "" && arn.ContainsUnicodeLetters(anime.Title.Japanese) { animeSearchIndex.TextToID[strings.ToLower(anime.Title.Japanese)] = anime.ID } diff --git a/main_test.go b/main_test.go index a766bc9f..7598d804 100644 --- a/main_test.go +++ b/main_test.go @@ -11,7 +11,7 @@ import ( func TestRoutes(t *testing.T) { app := configure(aero.New()) - for _, examples := range tests { + for _, examples := range routeTests { for _, example := range examples { request, err := http.NewRequest("GET", example, nil) diff --git a/pages/search/search.go b/pages/search/search.go index a7dd0516..df5f1a59 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -6,12 +6,12 @@ import ( "github.com/animenotifier/notify.moe/components" ) -const maxUsers = 9 * 7 -const maxAnime = 9 * 7 +const maxUsers = 9 * 4 +const maxAnime = 9 * 4 // Get search page. func Get(ctx *aero.Context) string { - term := ctx.Get("term") + term := ctx.Query("q") userResults, animeResults := arn.Search(term, maxUsers, maxAnime) return ctx.HTML(components.SearchResults(userResults, animeResults)) diff --git a/pages/search/search.pixy b/pages/search/search.pixy index 6e12b652..8439d587 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -7,7 +7,8 @@ component SearchResults(users []*arn.User, animeResults []*arn.Anime) p No users found. else each user in users - Avatar(user) + .mountable(data-mountable-type="user") + Avatar(user) //- a.ajax(href=user.Link())= user.Nick .widget @@ -17,5 +18,5 @@ component SearchResults(users []*arn.User, animeResults []*arn.Anime) p No anime found. else each anime in animeResults - a.profile-watching-list-item.ajax(href=anime.Link(), title=anime.Title.Canonical) + a.profile-watching-list-item.mountable.ajax(href=anime.Link(), title=anime.Title.Canonical, data-mountable-type="anime") img.anime-cover-image.anime-search-result(src=anime.Image.Tiny, alt=anime.Title.Canonical) \ No newline at end of file diff --git a/rewrite.go b/rewrite.go index a171a56b..8d66ef8a 100644 --- a/rewrite.go +++ b/rewrite.go @@ -18,10 +18,26 @@ func init() { newURI := "/user/" userName := requestURI[2:] ctx.SetURI(newURI + userName) - } else if strings.HasPrefix(requestURI, plusRouteAjax) { + return + } + + if strings.HasPrefix(requestURI, plusRouteAjax) { newURI := "/_/user/" userName := requestURI[4:] ctx.SetURI(newURI + userName) + return + } + + if strings.HasPrefix(requestURI, "/search/") { + searchTerm := requestURI[len("/search/"):] + ctx.Request.URL.RawQuery = "q=" + searchTerm + ctx.SetURI("/search") + } + + if strings.HasPrefix(requestURI, "/_/search/") { + searchTerm := requestURI[len("/_/search/"):] + ctx.Request.URL.RawQuery = "q=" + searchTerm + ctx.SetURI("/_/search") } }) } diff --git a/scripts/Actions.ts b/scripts/Actions.ts index a2431b15..69673e42 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -145,10 +145,10 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard let term = search.value - if(!window.location.pathname.startsWith("/search/")) { - history.pushState("search", null, "/search/" + term) - } else { + if(window.location.pathname.startsWith("/search/")) { history.replaceState("search", null, "/search/" + term) + } else { + history.pushState("search", null, "/search/" + term) } if(!term || term.length < 2) { @@ -165,7 +165,7 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard arn.app.content.appendChild(results) } - arn.app.get("/_/search/" + encodeURI(term)) + arn.app.get("/_/search/" + term) .then(html => { if(!search.value) { return diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index f51a828b..71e8a660 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -199,13 +199,23 @@ export class AnimeNotifier { } modifyDelayed(className: string, func: (element: HTMLElement) => void) { + let mountableTypes = { + general: 0 + } + const delay = 20 - const maxDelay = 500 + const maxDelay = 1000 let time = 0 for(let element of findAll(className)) { - time += delay + let type = element.dataset.mountableType || "general" + + if(type in mountableTypes) { + time = mountableTypes[element.dataset.mountableType] += delay + } else { + time = mountableTypes[element.dataset.mountableType] = 0 + } if(time > maxDelay) { func(element) From 8f3c03a5c506d492ae731aadf98406a61f8a5821 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 18:05:12 +0200 Subject: [PATCH 099/527] Fixed transitions --- scripts/AnimeNotifier.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 71e8a660..ed691ee5 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -212,9 +212,9 @@ export class AnimeNotifier { let type = element.dataset.mountableType || "general" if(type in mountableTypes) { - time = mountableTypes[element.dataset.mountableType] += delay + time = mountableTypes[type] += delay } else { - time = mountableTypes[element.dataset.mountableType] = 0 + time = mountableTypes[type] = 0 } if(time > maxDelay) { From 6c7fc902c0a1396c654be17ec9de4c6c86a677d9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 2 Jul 2017 23:42:46 +0200 Subject: [PATCH 100/527] Facebook login --- auth/auth.go | 3 + auth/facebook.go | 172 +++++++++++++++++++++ auth/google.go | 14 +- pages/animelistitem/animelistitem.pixy | 15 +- pages/login/login.pixy | 5 +- pages/login/login.scarlet | 2 +- pages/newsoundtrack/newsoundtrack.pixy | 21 ++- pages/settings/settings.pixy | 28 ++++ pages/settings/settings.scarlet | 2 + patches/user-references/user-references.go | 10 +- styles/input.scarlet | 55 ++----- tests.go | 18 ++- 12 files changed, 267 insertions(+), 78 deletions(-) create mode 100644 auth/facebook.go create mode 100644 pages/settings/settings.scarlet diff --git a/auth/auth.go b/auth/auth.go index 2a855fd2..9cad95f6 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -8,6 +8,9 @@ func Install(app *aero.Application) { // Google InstallGoogleAuth(app) + // Facebook + InstallFacebookAuth(app) + // Logout app.Get("/logout", func(ctx *aero.Context) string { if ctx.HasSession() { diff --git a/auth/facebook.go b/auth/facebook.go new file mode 100644 index 00000000..1ac99d60 --- /dev/null +++ b/auth/facebook.go @@ -0,0 +1,172 @@ +package auth + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strings" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/utils" + "golang.org/x/oauth2" + "golang.org/x/oauth2/facebook" +) + +// FacebookUser is the user data we receive from Facebook +type FacebookUser struct { + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Gender string `json:"gender"` +} + +// InstallFacebookAuth enables Facebook login for the app. +func InstallFacebookAuth(app *aero.Application) { + config := &oauth2.Config{ + ClientID: arn.APIKeys.Facebook.ID, + ClientSecret: arn.APIKeys.Facebook.Secret, + RedirectURL: "https://" + app.Config.Domain + "/auth/facebook/callback", + Scopes: []string{ + "public_profile", + "email", + }, + Endpoint: facebook.Endpoint, + } + + // Auth + app.Get("/auth/facebook", func(ctx *aero.Context) string { + state := ctx.Session().ID() + url := config.AuthCodeURL(state) + ctx.Redirect(url) + return "" + }) + + // Auth Callback + app.Get("/auth/facebook/callback", func(ctx *aero.Context) string { + if !ctx.HasSession() { + return ctx.Error(http.StatusUnauthorized, "Facebook login failed", errors.New("Session does not exist")) + } + + session := ctx.Session() + + if session.ID() != ctx.Query("state") { + return ctx.Error(http.StatusUnauthorized, "Facebook login failed", errors.New("Incorrect state")) + } + + // Handle the exchange code to initiate a transport + token, err := config.Exchange(oauth2.NoContext, ctx.Query("code")) + + if err != nil { + return ctx.Error(http.StatusBadRequest, "Could not obtain OAuth token", err) + } + + // Construct the OAuth client + client := config.Client(oauth2.NoContext, token) + + // Fetch user data from Facebook + resp, err := client.Get("https://graph.facebook.com/me?fields=email,first_name,last_name,gender") + + if err != nil { + return ctx.Error(http.StatusBadRequest, "Failed requesting user data from Facebook", err) + } + + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + + // Construct a FacebookUser object + fbUser := FacebookUser{} + err = json.Unmarshal(body, &fbUser) + + if err != nil { + return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err) + } + + // Change googlemail.com to gmail.com + fbUser.Email = strings.Replace(fbUser.Email, "googlemail.com", "gmail.com", 1) + + // Is this an existing user connecting another social account? + user := utils.GetUser(ctx) + + if user != nil { + // Add FacebookToUser reference + err = user.ConnectFacebook(fbUser.ID) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Could not connect account to Facebook account", err) + } + + authLog.Info("Added Facebook ID to existing account", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) + + return ctx.Redirect("/") + } + + var getErr error + + // Try to find an existing user via the Facebook user ID + user, getErr = arn.GetUserFromTable("FacebookToUser", fbUser.ID) + + if getErr == nil && user != nil { + authLog.Info("User logged in via Facebook ID", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) + + user.LastLogin = arn.DateTimeUTC() + user.Save() + + session.Set("userId", user.ID) + return ctx.Redirect("/") + } + + // Try to find an existing user via the associated e-mail address + user, getErr = arn.GetUserByEmail(fbUser.Email) + + if getErr == nil && user != nil { + authLog.Info("User logged in via Email", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) + + user.LastLogin = arn.DateTimeUTC() + user.Save() + + session.Set("userId", user.ID) + return ctx.Redirect("/") + } + + // Register new user + user = arn.NewUser() + user.Nick = "fb" + fbUser.ID + user.Email = fbUser.Email + user.FirstName = fbUser.FirstName + user.LastName = fbUser.LastName + user.Gender = fbUser.Gender + user.LastLogin = arn.DateTimeUTC() + + // Save basic user info already to avoid data inconsistency problems + user.Save() + + // Register user + err = arn.RegisterUser(user) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Could not register a new user", err) + } + + // Connect account to a Facebook account + err = user.ConnectFacebook(fbUser.ID) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Could not connect account to Facebook account", err) + } + + // Save user object again with updated data + user.Save() + + // Login + session.Set("userId", user.ID) + + // Log + authLog.Info("Registered new user via Facebook", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) + + // Redirect to frontpage + return ctx.Redirect("/") + }) +} diff --git a/auth/google.go b/auth/google.go index 11e9717b..5037140c 100644 --- a/auth/google.go +++ b/auth/google.go @@ -5,6 +5,7 @@ import ( "errors" "io/ioutil" "net/http" + "strings" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -43,8 +44,8 @@ func InstallGoogleAuth(app *aero.Application) { // Auth app.Get("/auth/google", func(ctx *aero.Context) string { - sessionID := ctx.Session().ID() - url := config.AuthCodeURL(sessionID) + state := ctx.Session().ID() + url := config.AuthCodeURL(state) ctx.Redirect(url) return "" }) @@ -89,12 +90,13 @@ func InstallGoogleAuth(app *aero.Application) { return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err) } + // Change googlemail.com to gmail.com + googleUser.Email = strings.Replace(googleUser.Email, "googlemail.com", "gmail.com", 1) + // Is this an existing user connecting another social account? user := utils.GetUser(ctx) if user != nil { - println("Connected") - // Add GoogleToUser reference err = user.ConnectGoogle(googleUser.Sub) @@ -102,6 +104,8 @@ func InstallGoogleAuth(app *aero.Application) { ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err) } + authLog.Info("Added Google ID to existing account", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) + return ctx.Redirect("/") } @@ -166,7 +170,7 @@ func InstallGoogleAuth(app *aero.Application) { session.Set("userId", user.ID) // Log - authLog.Info("Registered new user", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) + authLog.Info("Registered new user via Google", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) // Redirect to frontpage return ctx.Redirect("/") diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index d4b95315..4a293b93 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -5,13 +5,14 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") - label(for="Status") Status: - select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") - option(value=arn.AnimeListStatusWatching) Watching - option(value=arn.AnimeListStatusCompleted) Completed - option(value=arn.AnimeListStatusPlanned) Plan to watch - option(value=arn.AnimeListStatusHold) On hold - option(value=arn.AnimeListStatusDropped) Dropped + .widget-input + label(for="Status") Status: + select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") + option(value=arn.AnimeListStatusWatching) Watching + option(value=arn.AnimeListStatusCompleted) Completed + option(value=arn.AnimeListStatusPlanned) Plan to watch + option(value=arn.AnimeListStatusHold) On hold + option(value=arn.AnimeListStatusDropped) Dropped .anime-list-item-rating-edit InputNumber("Rating.Overall", item.Rating.Overall, arn.OverallRatingName(item.Episodes), "Overall rating on a scale of 0 to 10", "0", "10", "0.1") diff --git a/pages/login/login.pixy b/pages/login/login.pixy index ce41d9f9..883cd798 100644 --- a/pages/login/login.pixy +++ b/pages/login/login.pixy @@ -1,4 +1,7 @@ component Login .login-buttons a.login-button(href="/auth/google") - img.login-button-image(src="/images/login/google", alt="Google Login", title="Login with your Google account") \ No newline at end of file + img.login-button-image(src="/images/login/google", alt="Google Login", title="Login with your Google account") + + a.login-button(href="/auth/facebook") + img.login-button-image(src="/images/login/facebook", alt="Facebook Login", title="Login with your Facebook account") \ No newline at end of file diff --git a/pages/login/login.scarlet b/pages/login/login.scarlet index 2a43abf7..36972b8f 100644 --- a/pages/login/login.scarlet +++ b/pages/login/login.scarlet @@ -4,7 +4,7 @@ justify-content center .login-button - // + padding 0.5rem .login-button-image max-width 236px diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy index 2237f08a..ca2e65e8 100644 --- a/pages/newsoundtrack/newsoundtrack.pixy +++ b/pages/newsoundtrack/newsoundtrack.pixy @@ -2,17 +2,22 @@ component NewSoundTrack(user *arn.User) .widgets .widget h3 New soundtrack - label(for="soundcloud-link") Soundcloud link: - input#soundcloud-link.widget-element(type="text", placeholder="https://soundcloud.com/abc/123") - label(for="youtube-link") Youtube link: - input#youtube-link.widget-element(type="text", placeholder="https://www.youtube.com/watch?v=123") + .widget-input + label(for="soundcloud-link") Soundcloud link: + input#soundcloud-link.widget-element(type="text", placeholder="https://soundcloud.com/abc/123") - label(for="anime-link") Anime link: - input#anime-link.widget-element(type="text", placeholder="https://notify.moe/anime/123") + .widget-input + label(for="youtube-link") Youtube link: + input#youtube-link.widget-element(type="text", placeholder="https://www.youtube.com/watch?v=123") - label(for="osu-link") Osu beatmap (optional): - input#osu-link.widget-element(type="text", placeholder="https://osu.ppy.sh/s/123") + .widget-input + label(for="anime-link") Anime link: + input#anime-link.widget-element(type="text", placeholder="https://notify.moe/anime/123") + + .widget-input + label(for="osu-link") Osu beatmap (optional): + input#osu-link.widget-element(type="text", placeholder="https://osu.ppy.sh/s/123") .buttons button.action(data-action="createSoundTrack", data-trigger="click") diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 879de0af..5897ec3b 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -21,6 +21,34 @@ component Settings(user *arn.User) InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh") + .widget.mountable + h3.widget-title + Icon("user-plus") + span Connect + + .widget-input.social-account + label(for="google") Google: + + a#google.button.social-account-button(href="/auth/google") + if user.Accounts.Google.ID != "" + Icon("check") + span Connected + else + Icon("circle-o") + span Not connected + + .widget-input.social-account + label(for="facebook") Facebook: + + a#facebook.button.social-account-button(href="/auth/facebook") + if user.Accounts.Facebook.ID != "" + Icon("check") + span Connected + else + + Icon("circle-o") + span Not connected + //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title //- Icon("cogs") diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet new file mode 100644 index 00000000..301a0549 --- /dev/null +++ b/pages/settings/settings.scarlet @@ -0,0 +1,2 @@ +.social-account-button + margin-bottom 1rem \ No newline at end of file diff --git a/patches/user-references/user-references.go b/patches/user-references/user-references.go index 31a7d454..c25e37a8 100644 --- a/patches/user-references/user-references.go +++ b/patches/user-references/user-references.go @@ -11,6 +11,7 @@ func main() { arn.DB.DeleteTable("NickToUser") arn.DB.DeleteTable("EmailToUser") arn.DB.DeleteTable("GoogleToUser") + arn.DB.DeleteTable("FacebookToUser") // Get a stream of all users allUsers, err := arn.StreamUsers() @@ -32,10 +33,11 @@ func main() { } if user.Accounts.Google.ID != "" { - arn.DB.Set("GoogleToUser", user.Accounts.Google.ID, &arn.GoogleToUser{ - ID: user.Accounts.Google.ID, - UserID: user.ID, - }) + user.ConnectGoogle(user.Accounts.Google.ID) + } + + if user.Accounts.Facebook.ID != "" { + user.ConnectFacebook(user.Accounts.Facebook.ID) } } diff --git a/styles/input.scarlet b/styles/input.scarlet index 4270571b..2f5581e4 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -5,44 +5,29 @@ mixin input-focus // TODO: Replace with alpha(main-color, 20%) function box-shadow 0 0 6px rgba(248, 165, 130, 0.2) -input, textarea, button, select +input, textarea, button, .button, select + ui-element font-family inherit font-size 1em - padding 0.4em 0.8em - border-radius 3px + line-height 1.25em color text-color -input, textarea - border ui-border - background white - box-shadow none - width 100% +input, textarea, select 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, -textarea.widget-element - input-focus - -textarea - height 10rem - button, .button - ui-element horizontal - font-size 1rem - line-height 1rem - padding 0.75rem 1rem + line-height 1.5em + padding 0.5rem 1rem color link-color + align-items center :hover, &.active @@ -52,35 +37,17 @@ button, .button :active transform translateY(3px) - - // box-shadow 0 0 2px white, 0 -2px 5px rgba(0, 0, 0, 0.08) inset - // :active - // background-color black - // color white - // :focus - // color rgb(0, 0, 0) - // // box-shadow 0 0 6px alpha(mainColor, 20%) - // border 1px solid main-color select - ui-element appearance none -webkit-appearance none -moz-appearance none - font-size 1rem - // padding 0.5em 1em label width 100% padding 0.5rem 0 text-align left -// input[type="submit"]:hover, -// button:hover -// cursor pointer -// text-decoration none - -// button[disabled] -// opacity 0.5 -// :hover -// cursor not-allowed \ No newline at end of file +textarea + line-height 1.5em + height 10rem \ No newline at end of file diff --git a/tests.go b/tests.go index 47160cd1..2658213f 100644 --- a/tests.go +++ b/tests.go @@ -150,14 +150,16 @@ var routeTests = map[string][]string{ }, // Disable these tests because they require authorization - "/auth/google": nil, - "/auth/google/callback": nil, - "/anime/:id/edit": nil, - "/new/thread": nil, - "/new/soundtrack": nil, - "/user": nil, - "/settings": nil, - "/extension/embed": nil, + "/auth/google": nil, + "/auth/google/callback": nil, + "/auth/facebook": nil, + "/auth/facebook/callback": nil, + "/anime/:id/edit": nil, + "/new/thread": nil, + "/new/soundtrack": nil, + "/user": nil, + "/settings": nil, + "/extension/embed": nil, } // API interfaces From 4ed9cb7012b018a1ffc062392146a77bad6f42f4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 00:00:41 +0200 Subject: [PATCH 101/527] Fixed account connect --- auth/facebook.go | 4 ++++ auth/google.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/auth/facebook.go b/auth/facebook.go index 1ac99d60..4bb761cb 100644 --- a/auth/facebook.go +++ b/auth/facebook.go @@ -98,6 +98,10 @@ func InstallFacebookAuth(app *aero.Application) { ctx.Error(http.StatusInternalServerError, "Could not connect account to Facebook account", err) } + // Save in DB + user.Save() + + // Log authLog.Info("Added Facebook ID to existing account", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) return ctx.Redirect("/") diff --git a/auth/google.go b/auth/google.go index 5037140c..7839beb9 100644 --- a/auth/google.go +++ b/auth/google.go @@ -104,6 +104,10 @@ func InstallGoogleAuth(app *aero.Application) { ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err) } + // Save in DB + user.Save() + + // Log authLog.Info("Added Google ID to existing account", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) return ctx.Redirect("/") From db94850ce238c3e12a72d0ce6fbe637dca133691 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 01:15:07 +0200 Subject: [PATCH 102/527] Fixed textarea --- styles/input.scarlet | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/styles/input.scarlet b/styles/input.scarlet index 2f5581e4..9b0702d8 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -49,5 +49,8 @@ label text-align left textarea + padding 0.4em 0.8em + width 100% line-height 1.5em - height 10rem \ No newline at end of file + height 10rem + transition none \ No newline at end of file From e6d18f2e1d3e34588a77f7751ba014696d6af410 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 15:34:50 +0200 Subject: [PATCH 103/527] Changed forum-tags to tabs --- config.json | 1 + mixins/ForumTags.pixy | 6 +++--- pages/forum/forum.scarlet | 23 ----------------------- pages/profile/profile.go | 30 +++++++++++++++--------------- styles/forum.scarlet | 4 ---- styles/navigation.scarlet | 2 +- styles/tabs.scarlet | 26 ++++++++++++++++++++++++++ 7 files changed, 46 insertions(+), 46 deletions(-) create mode 100644 styles/tabs.scarlet diff --git a/config.json b/config.json index b4ecbbf9..470a98c4 100644 --- a/config.json +++ b/config.json @@ -17,6 +17,7 @@ "input", "grid", "forum", + "tabs", "user", "video", "loading", diff --git a/mixins/ForumTags.pixy b/mixins/ForumTags.pixy index ff367246..44f40672 100644 --- a/mixins/ForumTags.pixy +++ b/mixins/ForumTags.pixy @@ -1,5 +1,5 @@ component ForumTags - .buttons.forum-tags + .buttons.tabs ForumTag("All", "", "list") ForumTag("General", "general", "list") ForumTag("News", "news", "list") @@ -9,6 +9,6 @@ component ForumTags ForumTag("Bugs", "bug", "list") component ForumTag(title string, category string, icon string) - a.button.forum-tag.action(href=strings.TrimSuffix("/forum/" + category, "/"), data-action="diff", data-trigger="click") + a.button.tab.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 + span.tab-text= title \ No newline at end of file diff --git a/pages/forum/forum.scarlet b/pages/forum/forum.scarlet index 79f1ee73..68aba8a8 100644 --- a/pages/forum/forum.scarlet +++ b/pages/forum/forum.scarlet @@ -6,29 +6,6 @@ width 100% max-width forum-width -.forum-tag - color text-color !important - - :hover, - &.active - color white !important - 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 - .icon - margin-right 0 - - .forum-tag-text - display none - > 1250px #new-thread position fixed diff --git a/pages/profile/profile.go b/pages/profile/profile.go index aaa39dfe..16117f39 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -36,27 +36,27 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { }, func() { animeList = viewUser.AnimeList() }, func() { - threads = viewUser.Threads() + // threads = viewUser.Threads() - arn.SortThreadsLatestFirst(threads) + // arn.SortThreadsLatestFirst(threads) - if len(threads) > maxPosts { - threads = threads[:maxPosts] - } + // if len(threads) > maxPosts { + // threads = threads[:maxPosts] + // } }, func() { - posts = viewUser.Posts() - arn.SortPostsLatestFirst(posts) + // posts = viewUser.Posts() + // arn.SortPostsLatestFirst(posts) - if len(posts) > maxPosts { - posts = posts[:maxPosts] - } + // if len(posts) > maxPosts { + // posts = posts[:maxPosts] + // } }, func() { - tracks = viewUser.SoundTracks() - arn.SortSoundTracksLatestFirst(tracks) + // tracks = viewUser.SoundTracks() + // arn.SortSoundTracksLatestFirst(tracks) - if len(tracks) > maxTracks { - tracks = tracks[:maxTracks] - } + // if len(tracks) > maxTracks { + // tracks = tracks[:maxTracks] + // } }) return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts, tracks)) diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 75c239fa..3ba161ef 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -63,10 +63,6 @@ .thread-link-title color text-color -.forum-tags - // justify-content flex-start !important - margin-bottom 1rem - .post-author horizontal justify-content center diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index d22110d4..a2204384 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -83,7 +83,7 @@ .extra-navigation display block -< 400px height +< 380px height #navigation vertical height 100% diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet new file mode 100644 index 00000000..ed305f6b --- /dev/null +++ b/styles/tabs.scarlet @@ -0,0 +1,26 @@ +.tab + color text-color !important + + :hover, + &.active + color white !important + 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 + .tab + .icon + margin-right 0 + + .tab-text + display none + +.tabs + // justify-content flex-start !important + margin-bottom 1rem \ No newline at end of file From dd974ed99a6cf0fab2b8a7cdc98f4b11803bd2b2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 17:21:00 +0200 Subject: [PATCH 104/527] Improved profile --- pages/animelist/animelist.pixy | 47 ++++++++------ pages/forum/forum.pixy | 2 +- pages/profile/posts.go | 7 +- pages/profile/posts.pixy | 6 +- pages/profile/profile.pixy | 114 +++++++++++++++++++-------------- pages/profile/profile.scarlet | 4 +- pages/profile/threads.go | 7 +- pages/profile/threads.pixy | 5 ++ pages/profile/tracks.pixy | 14 ++-- pages/profile/watching.scarlet | 7 +- pages/search/search.pixy | 4 +- scripts/Actions.ts | 36 ++++++++++- scripts/AnimeNotifier.ts | 16 ++++- styles/tabs.scarlet | 3 +- 14 files changed, 180 insertions(+), 92 deletions(-) create mode 100644 pages/profile/threads.pixy diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index cf8eb30c..e58a792c 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -1,30 +1,35 @@ component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User) - h2.anime-list-owner= viewUser.Nick + "'s collection" + ProfileHeader(viewUser, user) - if len(animeLists[arn.AnimeListStatusWatching].Items) > 0 - .anime-list-container - h3.status-name Watching - AnimeList(animeLists[arn.AnimeListStatusWatching], viewUser, user) + h2.page-title.anime-list-owner= viewUser.Nick + "'s collection" - if len(animeLists[arn.AnimeListStatusCompleted].Items) > 0 - .anime-list-container - h3.status-name Completed - AnimeList(animeLists[arn.AnimeListStatusCompleted], viewUser, user) + if len(animeLists[arn.AnimeListStatusWatching].Items) == 0 && len(animeLists[arn.AnimeListStatusCompleted].Items) == 0 && len(animeLists[arn.AnimeListStatusPlanned].Items) == 0 && len(animeLists[arn.AnimeListStatusHold].Items) == 0 && len(animeLists[arn.AnimeListStatusDropped].Items) == 0 + p.no-data No anime in the collection. + else + if len(animeLists[arn.AnimeListStatusWatching].Items) > 0 + .anime-list-container + h3.status-name Watching + AnimeList(animeLists[arn.AnimeListStatusWatching], viewUser, user) - if len(animeLists[arn.AnimeListStatusPlanned].Items) > 0 - .anime-list-container - h3.status-name Planned - AnimeList(animeLists[arn.AnimeListStatusPlanned], viewUser, user) + if len(animeLists[arn.AnimeListStatusCompleted].Items) > 0 + .anime-list-container + h3.status-name Completed + AnimeList(animeLists[arn.AnimeListStatusCompleted], viewUser, user) - if len(animeLists[arn.AnimeListStatusHold].Items) > 0 - .anime-list-container - h3.status-name On hold - AnimeList(animeLists[arn.AnimeListStatusHold], viewUser, user) + if len(animeLists[arn.AnimeListStatusPlanned].Items) > 0 + .anime-list-container + h3.status-name Planned + AnimeList(animeLists[arn.AnimeListStatusPlanned], viewUser, user) - if len(animeLists[arn.AnimeListStatusDropped].Items) > 0 - .anime-list-container - h3.status-name Dropped - AnimeList(animeLists[arn.AnimeListStatusDropped], viewUser, user) + if len(animeLists[arn.AnimeListStatusHold].Items) > 0 + .anime-list-container + h3.status-name On hold + AnimeList(animeLists[arn.AnimeListStatusHold], viewUser, user) + + if len(animeLists[arn.AnimeListStatusDropped].Items) > 0 + .anime-list-container + h3.status-name Dropped + AnimeList(animeLists[arn.AnimeListStatusDropped], viewUser, user) //- for status, animeList := range animeLists //- h3= status diff --git a/pages/forum/forum.pixy b/pages/forum/forum.pixy index a248d5ed..97fdff94 100644 --- a/pages/forum/forum.pixy +++ b/pages/forum/forum.pixy @@ -16,7 +16,7 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) component ThreadList(threads []*arn.Thread) if len(threads) == 0 - p No threads found. + p.no-data No threads found. else each thread in threads ThreadLink(thread) \ No newline at end of file diff --git a/pages/profile/posts.go b/pages/profile/posts.go index 9f91239a..d2668f45 100644 --- a/pages/profile/posts.go +++ b/pages/profile/posts.go @@ -6,6 +6,7 @@ import ( "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" ) const postLimit = 10 @@ -13,13 +14,13 @@ const postLimit = 10 // GetPostsByUser shows all forum posts of a particular user. func GetPostsByUser(ctx *aero.Context) string { nick := ctx.Get("nick") - user, err := arn.GetUserByNick(nick) + viewUser, err := arn.GetUserByNick(nick) if err != nil { return ctx.Error(http.StatusNotFound, "User not found", err) } - posts := user.Posts() + posts := viewUser.Posts() arn.SortPostsLatestFirst(posts) var postables []arn.Postable @@ -34,6 +35,6 @@ func GetPostsByUser(ctx *aero.Context) string { postables[i] = arn.ToPostable(post) } - return ctx.HTML(components.LatestPosts(postables, user)) + return ctx.HTML(components.LatestPosts(postables, viewUser, utils.GetUser(ctx))) } diff --git a/pages/profile/posts.pixy b/pages/profile/posts.pixy index 5d18a568..534addb8 100644 --- a/pages/profile/posts.pixy +++ b/pages/profile/posts.pixy @@ -1,6 +1,8 @@ -component LatestPosts(postables []arn.Postable, viewUser *arn.User) +component LatestPosts(postables []arn.Postable, viewUser *arn.User, user *arn.User) + ProfileHeader(viewUser, user) + if len(postables) > 0 - h2.thread-title= len(postables), " latest posts by ", postables[0].Author().Nick + h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick PostableList(postables) else p= viewUser.Nick + " hasn't written any posts yet." \ No newline at end of file diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 05125822..457f533e 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -2,10 +2,10 @@ component ProfileHeader(viewUser *arn.User, user *arn.User) .profile img.profile-cover(src=viewUser.CoverImageURL(), alt="Cover image") - .image-container.mountable + .image-container.mountable.never-unmount ProfileImage(viewUser) - .intro-container.mountable + .intro-container.mountable.never-unmount h2#nick= viewUser.Nick if viewUser.Tagline != "" @@ -43,58 +43,78 @@ component ProfileHeader(viewUser *arn.User, user *arn.User) p.profile-field.role Icon("rocket") span= arn.Capitalize(viewUser.Role) + + ProfileNavigation(viewUser) + +component ProfileNavigation(viewUser *arn.User) + .buttons.tabs + a.button.tab.action(href="/+" + viewUser.Nick, data-action="diff", data-trigger="click") + Icon("th") + span.tab-text Anime + + a.button.tab.action(href="/+" + viewUser.Nick + "/animelist", data-action="diff", data-trigger="click") + Icon("list") + span.tab-text List + + a.button.tab.action(href="/+" + viewUser.Nick + "/threads", data-action="diff", data-trigger="click") + Icon("comment") + span.tab-text Threads + + a.button.tab.action(href="/+" + viewUser.Nick + "/posts", data-action="diff", data-trigger="click") + Icon("comments") + span.tab-text Posts + + a.button.tab.action(href="/+" + viewUser.Nick + "/tracks", data-action="diff", data-trigger="click") + Icon("music") + span.tab-text Tracks component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack) ProfileHeader(viewUser, user) - .profile-category.mountable - h3 - a.ajax(href="/+" + viewUser.Nick + "/animelist", title="View all anime") Anime - - .profile-watching-list - if len(animeList.Items) == 0 - p No anime in the collection. - else - each item in animeList.Items - a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") - img.anime-cover-image.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) - - .profile-category.mountable - h3 - a.ajax(href="/+" + viewUser.Nick + "/threads", title="View all threads") Threads - - if len(threads) == 0 - p No threads on the forum. + .profile-watching-list.mountable + if len(animeList.Items) == 0 + p.no-data No anime in the collection. else - each thread in threads - ThreadLink(thread) + each item in animeList.Items + a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") + img.anime-cover-image.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) - .profile-category.mountable - h3 - a.ajax(href="/+" + viewUser.Nick + "/posts", title="View all posts") Posts - if len(posts) == 0 - p No posts on the forum. - else - each post in posts - .post - .post-author - Avatar(post.Author()) - .post-content - div!= post.HTML() - .post-toolbar.active - .spacer - .post-likes= len(post.Likes) - - .profile-category.mountable - h3 - a.ajax(href="/+" + viewUser.Nick + "/tracks", title="View all tracks") Tracks + //- .profile-category.mountable + //- h3 + //- a.ajax(href="/+" + viewUser.Nick + "/threads", title="View all threads") Threads - if len(tracks) == 0 - p No soundtracks posted yet. - else - .sound-tracks - each track in tracks - SoundTrack(track) + //- if len(threads) == 0 + //- p No threads on the forum. + //- else + //- each thread in threads + //- ThreadLink(thread) + + //- .profile-category.mountable + //- h3 + //- a.ajax(href="/+" + viewUser.Nick + "/posts", title="View all posts") Posts + //- if len(posts) == 0 + //- p No posts on the forum. + //- else + //- each post in posts + //- .post + //- .post-author + //- Avatar(post.Author()) + //- .post-content + //- div!= post.HTML() + //- .post-toolbar.active + //- .spacer + //- .post-likes= len(post.Likes) + + //- .profile-category.mountable + //- h3 + //- a.ajax(href="/+" + viewUser.Nick + "/tracks", title="View all tracks") Tracks + + //- if len(tracks) == 0 + //- p No soundtracks posted yet. + //- else + //- .sound-tracks + //- each track in tracks + //- SoundTrack(track) //- if user != nil && user.Role == "admin" //- .footer diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 0ec8b188..e0d74bcb 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -88,5 +88,5 @@ profile-boot-duration = 2s // Categories -.profile-category - margin-bottom content-padding \ No newline at end of file +// .profile-category +// margin-bottom content-padding \ No newline at end of file diff --git a/pages/profile/threads.go b/pages/profile/threads.go index 5a0a8e9e..5f267439 100644 --- a/pages/profile/threads.go +++ b/pages/profile/threads.go @@ -6,19 +6,20 @@ import ( "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" ) // GetThreadsByUser shows all forum threads of a particular user. func GetThreadsByUser(ctx *aero.Context) string { nick := ctx.Get("nick") - user, err := arn.GetUserByNick(nick) + viewUser, err := arn.GetUserByNick(nick) if err != nil { return ctx.Error(http.StatusNotFound, "User not found", err) } - threads := user.Threads() + threads := viewUser.Threads() arn.SortThreadsLatestFirst(threads) - return ctx.HTML(components.ThreadList(threads)) + return ctx.HTML(components.ProfileThreads(threads, viewUser, utils.GetUser(ctx))) } diff --git a/pages/profile/threads.pixy b/pages/profile/threads.pixy new file mode 100644 index 00000000..993a7216 --- /dev/null +++ b/pages/profile/threads.pixy @@ -0,0 +1,5 @@ +component ProfileThreads(threads []*arn.Thread, viewUser *arn.User, user *arn.User) + ProfileHeader(viewUser, user) + + .forum + ThreadList(threads) diff --git a/pages/profile/tracks.pixy b/pages/profile/tracks.pixy index f554fca8..13981fdf 100644 --- a/pages/profile/tracks.pixy +++ b/pages/profile/tracks.pixy @@ -1,6 +1,12 @@ component TrackList(tracks []*arn.SoundTrack, viewUser *arn.User, user *arn.User) - h2= "Tracks added by " + viewUser.Nick - .sound-tracks - each track in tracks - SoundTrack(track) + ProfileHeader(viewUser, user) + + h2.page-title= "Tracks added by " + viewUser.Nick + + if len(tracks) == 0 + p.no-data No tracks added. + else + .sound-tracks + each track in tracks + SoundTrack(track) \ No newline at end of file diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 6b3b9b13..c77377f6 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -1,5 +1,6 @@ .profile-watching-list horizontal-wrap + justify-content center .profile-watching-list-item margin 0.25rem @@ -8,6 +9,6 @@ width 55px !important border-radius 2px -< 380px - .profile-watching-list - justify-content center \ No newline at end of file +// < 380px +// .profile-watching-list +// justify-content center \ No newline at end of file diff --git a/pages/search/search.pixy b/pages/search/search.pixy index 8439d587..a7e8b16c 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -4,7 +4,7 @@ component SearchResults(users []*arn.User, animeResults []*arn.Anime) h3 Users .user-avatars.user-search if len(users) == 0 - p No users found. + p.no-data No users found. else each user in users .mountable(data-mountable-type="user") @@ -15,7 +15,7 @@ component SearchResults(users []*arn.User, animeResults []*arn.Anime) h3 Anime .profile-watching-list.anime-search if len(animeResults) == 0 - p No anime found. + p.no-data No anime found. else each anime in animeResults a.profile-watching-list-item.mountable.ajax(href=anime.Link(), title=anime.Title.Canonical, data-mountable-type="anime") diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 69673e42..f56e96e3 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -76,7 +76,41 @@ export function load(arn: AnimeNotifier, element: HTMLElement) { // Diff export function diff(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") - arn.diff(url) + + arn.diff(url).then(() => { + arn.requestIdleCallback(() => { + const duration = 300.0 + const steps = 60 + const interval = duration / steps + const fullSin = Math.PI / 2 + const contentPadding = 25 + + let target = element + let scrollHandle: number + let oldScroll = arn.app.content.parentElement.scrollTop + let newScroll = Math.max(target.offsetTop - contentPadding, 0) + let scrollDistance = newScroll - oldScroll + let timeStart = performance.now() + let timeEnd = timeStart + duration + + let scroll = () => { + let time = performance.now() + let progress = (time - timeStart) / duration + + if(progress > 1.0) { + progress = 1.0 + } + + arn.app.content.parentElement.scrollTop = oldScroll + scrollDistance * Math.sin(progress * fullSin) + + if(time >= timeEnd || arn.app.content.parentElement.scrollTop == newScroll) { + clearInterval(scrollHandle) + } + } + + scrollHandle = setInterval(scroll, interval) + }) + }) } // Forum reply diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index ed691ee5..955ea59f 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -44,10 +44,14 @@ export class AnimeNotifier { document.addEventListener("keydown", this.onKeyDown.bind(this), false) window.addEventListener("popstate", this.onPopState.bind(this)) + this.requestIdleCallback(this.onIdle.bind(this)) + } + + requestIdleCallback(func: Function) { if("requestIdleCallback" in window) { - window["requestIdleCallback"](this.onIdle.bind(this)) + window["requestIdleCallback"](func) } else { - this.onIdle() + func() } } @@ -194,6 +198,10 @@ export class AnimeNotifier { unmountMountables() { for(let element of findAll("mountable")) { + if(element.classList.contains("never-unmount")) { + continue + } + element.classList.remove("mounted") } } @@ -228,6 +236,10 @@ export class AnimeNotifier { } diff(url: string) { + if(url == this.app.currentPath) { + return + } + let request = fetch("/_" + url, { credentials: "same-origin" }) diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index ed305f6b..7881b8bf 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -23,4 +23,5 @@ .tabs // justify-content flex-start !important - margin-bottom 1rem \ No newline at end of file + margin-bottom 1rem + margin-top -0.6rem \ No newline at end of file From 4f6ffd969d7f84788b426f502a561e8e7a62a678 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 17:33:57 +0200 Subject: [PATCH 105/527] Improved user profiles --- pages/animelist/animelist.pixy | 2 +- pages/forum/forum.pixy | 3 +-- pages/profile/posts.pixy | 2 +- pages/profile/profile.pixy | 10 +++++----- pages/profile/profile.scarlet | 3 +++ pages/profile/threads.go | 6 ++++++ pages/profile/threads.pixy | 7 +++++-- pages/profile/tracks.pixy | 2 +- pages/search/search.pixy | 4 ++-- 9 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index e58a792c..bf85ef74 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -4,7 +4,7 @@ component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, u h2.page-title.anime-list-owner= viewUser.Nick + "'s collection" if len(animeLists[arn.AnimeListStatusWatching].Items) == 0 && len(animeLists[arn.AnimeListStatusCompleted].Items) == 0 && len(animeLists[arn.AnimeListStatusPlanned].Items) == 0 && len(animeLists[arn.AnimeListStatusHold].Items) == 0 && len(animeLists[arn.AnimeListStatusDropped].Items) == 0 - p.no-data No anime in the collection. + p.no-data.mountable= viewUser.Nick + " hasn't added any anime yet." else if len(animeLists[arn.AnimeListStatusWatching].Items) > 0 .anime-list-container diff --git a/pages/forum/forum.pixy b/pages/forum/forum.pixy index 97fdff94..1b04acc3 100644 --- a/pages/forum/forum.pixy +++ b/pages/forum/forum.pixy @@ -4,7 +4,6 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) .forum ThreadList(threads) - .buttons button#new-thread.action(data-action="load", data-trigger="click", data-url="/new/thread") Icon("plus") @@ -16,7 +15,7 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) component ThreadList(threads []*arn.Thread) if len(threads) == 0 - p.no-data No threads found. + p.no-data.mountable No threads found. else each thread in threads ThreadLink(thread) \ No newline at end of file diff --git a/pages/profile/posts.pixy b/pages/profile/posts.pixy index 534addb8..e6a4e9de 100644 --- a/pages/profile/posts.pixy +++ b/pages/profile/posts.pixy @@ -5,4 +5,4 @@ component LatestPosts(postables []arn.Postable, viewUser *arn.User, user *arn.Us h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick PostableList(postables) else - p= viewUser.Nick + " hasn't written any posts yet." \ No newline at end of file + p.no-data.mountable= viewUser.Nick + " hasn't written any posts yet." \ No newline at end of file diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 457f533e..483c923b 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -54,7 +54,7 @@ component ProfileNavigation(viewUser *arn.User) a.button.tab.action(href="/+" + viewUser.Nick + "/animelist", data-action="diff", data-trigger="click") Icon("list") - span.tab-text List + span.tab-text Collection a.button.tab.action(href="/+" + viewUser.Nick + "/threads", data-action="diff", data-trigger="click") Icon("comment") @@ -71,10 +71,10 @@ component ProfileNavigation(viewUser *arn.User) component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack) ProfileHeader(viewUser, user) - .profile-watching-list.mountable - if len(animeList.Items) == 0 - p.no-data No anime in the collection. - else + if len(animeList.Items) == 0 + p.no-data.mountable= viewUser.Nick + " hasn't added any anime yet." + else + .profile-watching-list.mountable each item in animeList.Items a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") img.anime-cover-image.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index e0d74bcb..7ec638c4 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -86,6 +86,9 @@ profile-boot-duration = 2s #nick margin-bottom 1rem +.no-data + text-align center + // Categories // .profile-category diff --git a/pages/profile/threads.go b/pages/profile/threads.go index 5f267439..b886c6bd 100644 --- a/pages/profile/threads.go +++ b/pages/profile/threads.go @@ -9,6 +9,8 @@ import ( "github.com/animenotifier/notify.moe/utils" ) +const maxThreads = 20 + // GetThreadsByUser shows all forum threads of a particular user. func GetThreadsByUser(ctx *aero.Context) string { nick := ctx.Get("nick") @@ -21,5 +23,9 @@ func GetThreadsByUser(ctx *aero.Context) string { threads := viewUser.Threads() arn.SortThreadsLatestFirst(threads) + if len(threads) > maxThreads { + threads = threads[:maxThreads] + } + return ctx.HTML(components.ProfileThreads(threads, viewUser, utils.GetUser(ctx))) } diff --git a/pages/profile/threads.pixy b/pages/profile/threads.pixy index 993a7216..daad69f8 100644 --- a/pages/profile/threads.pixy +++ b/pages/profile/threads.pixy @@ -1,5 +1,8 @@ component ProfileThreads(threads []*arn.Thread, viewUser *arn.User, user *arn.User) ProfileHeader(viewUser, user) - .forum - ThreadList(threads) + if len(threads) == 0 + p.no-data.mountable= viewUser.Nick + " hasn't written any threads yet." + else + .forum + ThreadList(threads) diff --git a/pages/profile/tracks.pixy b/pages/profile/tracks.pixy index 13981fdf..3adc47ee 100644 --- a/pages/profile/tracks.pixy +++ b/pages/profile/tracks.pixy @@ -4,7 +4,7 @@ component TrackList(tracks []*arn.SoundTrack, viewUser *arn.User, user *arn.User h2.page-title= "Tracks added by " + viewUser.Nick if len(tracks) == 0 - p.no-data No tracks added. + p.no-data.mountable= viewUser.Nick + " hasn't added any tracks yet." else .sound-tracks each track in tracks diff --git a/pages/search/search.pixy b/pages/search/search.pixy index a7e8b16c..c588f2b5 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -4,7 +4,7 @@ component SearchResults(users []*arn.User, animeResults []*arn.Anime) h3 Users .user-avatars.user-search if len(users) == 0 - p.no-data No users found. + p.no-data.mountable No users found. else each user in users .mountable(data-mountable-type="user") @@ -15,7 +15,7 @@ component SearchResults(users []*arn.User, animeResults []*arn.Anime) h3 Anime .profile-watching-list.anime-search if len(animeResults) == 0 - p.no-data No anime found. + p.no-data.mountable No anime found. else each anime in animeResults a.profile-watching-list-item.mountable.ajax(href=anime.Link(), title=anime.Title.Canonical, data-mountable-type="anime") From 7262f4db7bd8b81d01edd0b1bcac232827bf7298 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 19:33:52 +0200 Subject: [PATCH 106/527] Added list import preview --- main.go | 4 ++ pages/listimport/listimport.go | 20 ++++++ pages/listimport/listimport.pixy | 8 +++ pages/listimport/listimportanilist/anilist.go | 62 +++++++++++++++++++ .../listimport/listimportanilist/anilist.pixy | 23 +++++++ pages/settings/settings.pixy | 7 +++ scripts/Actions.ts | 5 ++ 7 files changed, 129 insertions(+) create mode 100644 pages/listimport/listimport.go create mode 100644 pages/listimport/listimport.pixy create mode 100644 pages/listimport/listimportanilist/anilist.go create mode 100644 pages/listimport/listimportanilist/anilist.pixy diff --git a/main.go b/main.go index d519efce..83e5dd83 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,8 @@ import ( "github.com/animenotifier/notify.moe/pages/explore" "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" + "github.com/animenotifier/notify.moe/pages/listimport" + "github.com/animenotifier/notify.moe/pages/listimport/listimportanilist" "github.com/animenotifier/notify.moe/pages/login" "github.com/animenotifier/notify.moe/pages/music" "github.com/animenotifier/notify.moe/pages/newsoundtrack" @@ -79,6 +81,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) app.Ajax("/music", music.Get) + app.Ajax("/import", listimport.Get) + app.Ajax("/import/anilist/animelist", listimportanilist.Get) app.Ajax("/admin", admin.Get) app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) diff --git a/pages/listimport/listimport.go b/pages/listimport/listimport.go new file mode 100644 index 00000000..8956baf7 --- /dev/null +++ b/pages/listimport/listimport.go @@ -0,0 +1,20 @@ +package listimport + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get ... +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + return ctx.HTML(components.ImportLists(user)) +} diff --git a/pages/listimport/listimport.pixy b/pages/listimport/listimport.pixy new file mode 100644 index 00000000..3a8e7d57 --- /dev/null +++ b/pages/listimport/listimport.pixy @@ -0,0 +1,8 @@ +component ImportLists(user *arn.User) + .buttons + if user.Accounts.AniList.Nick != "" + a.button.mountable.ajax(href="/import/anilist/animelist") + Icon("refresh") + span Import AniList Anime List + else + p No imports available. \ No newline at end of file diff --git a/pages/listimport/listimportanilist/anilist.go b/pages/listimport/listimportanilist/anilist.go new file mode 100644 index 00000000..b1601214 --- /dev/null +++ b/pages/listimport/listimportanilist/anilist.go @@ -0,0 +1,62 @@ +package listimportanilist + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get ... +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + authErr := arn.AniList.Authorize() + + if authErr != nil { + return ctx.Error(http.StatusBadRequest, "Couldn't authorize the Anime Notifier app on AniList", authErr) + } + + allAnime, allErr := arn.AllAnime() + + if allErr != nil { + return ctx.Error(http.StatusBadRequest, "Couldn't load notify.moe list of all anime", allErr) + } + + animeList, err := arn.AniList.GetAnimeList(user) + + if err != nil { + return ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from AniList", err) + } + + matches := []*arn.AniListMatch{} + + matches = importList(matches, allAnime, animeList.Lists.Watching) + matches = importList(matches, allAnime, animeList.Lists.Completed) + matches = importList(matches, allAnime, animeList.Lists.PlanToWatch) + matches = importList(matches, allAnime, animeList.Lists.OnHold) + matches = importList(matches, allAnime, animeList.Lists.Dropped) + + for _, list := range animeList.CustomLists { + matches = importList(matches, allAnime, list) + } + + return ctx.HTML(components.ImportAnilist(user, matches)) +} + +func importList(matches []*arn.AniListMatch, allAnime []*arn.Anime, animeListItems []*arn.AniListAnimeListItem) []*arn.AniListMatch { + for _, item := range animeListItems { + matches = append(matches, &arn.AniListMatch{ + AniListAnime: item.Anime, + ARNAnime: arn.FindAniListAnime(item.Anime, allAnime), + }) + } + + return matches +} diff --git a/pages/listimport/listimportanilist/anilist.pixy b/pages/listimport/listimportanilist/anilist.pixy new file mode 100644 index 00000000..b9b57834 --- /dev/null +++ b/pages/listimport/listimportanilist/anilist.pixy @@ -0,0 +1,23 @@ +component ImportAnilist(user *arn.User, matches []*arn.AniListMatch) + h2= "Import: anilist.co" + + table + thead + tr + th anilist.co + th notify.moe + tbody + each match in matches + tr + td + a(href=match.AniListAnime.Link(), target="_blank", rel="noopener")= match.AniListAnime.TitleRomaji + td + if match.ARNAnime == nil + span.import-error Not found on notify.moe + else + a(href=match.ARNAnime.Link(), target="_blank", rel="noopener")= match.ARNAnime.Title.Canonical + + .buttons + .button.mountable.action(data-action="soon", data-trigger="click") + Icon("refresh") + span Import \ No newline at end of file diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 5897ec3b..42a6b118 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -48,6 +48,13 @@ component Settings(user *arn.User) Icon("circle-o") span Not connected + + .widget.mountable + h3.widget-title + Icon("refresh") + span Import + + ImportLists(user) //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title diff --git a/scripts/Actions.ts b/scripts/Actions.ts index f56e96e3..eaf3e908 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -73,6 +73,11 @@ export function load(arn: AnimeNotifier, element: HTMLElement) { arn.app.load(url) } +// Soon +export function soon() { + alert("Coming Soon™") +} + // Diff export function diff(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") From e0d2ddfff06212bf6b7a7f107f87081914948132 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 21:55:37 +0200 Subject: [PATCH 107/527] Minor changes --- scripts/Actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index eaf3e908..38d2ceca 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -88,7 +88,7 @@ export function diff(arn: AnimeNotifier, element: HTMLElement) { const steps = 60 const interval = duration / steps const fullSin = Math.PI / 2 - const contentPadding = 25 + const contentPadding = 24 let target = element let scrollHandle: number From 468956abf11f7c8ac5e656f6851ed741eb690385 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 22:50:04 +0200 Subject: [PATCH 108/527] Fixed tests --- tests.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests.go b/tests.go index 2658213f..5f631dfd 100644 --- a/tests.go +++ b/tests.go @@ -100,6 +100,10 @@ var routeTests = map[string][]string{ "/api/googletouser/106530160120373282283", }, + "/api/facebooktouser/:id": []string{ + "/api/facebooktouser/10207576239700188", + }, + "/api/nicktouser/:id": []string{ "/api/nicktouser/Akyoto", }, @@ -150,16 +154,18 @@ var routeTests = map[string][]string{ }, // Disable these tests because they require authorization - "/auth/google": nil, - "/auth/google/callback": nil, - "/auth/facebook": nil, - "/auth/facebook/callback": nil, - "/anime/:id/edit": nil, - "/new/thread": nil, - "/new/soundtrack": nil, - "/user": nil, - "/settings": nil, - "/extension/embed": nil, + "/auth/google": nil, + "/auth/google/callback": nil, + "/auth/facebook": nil, + "/auth/facebook/callback": nil, + "/import": nil, + "/import/anilist/animelist": nil, + "/anime/:id/edit": nil, + "/new/thread": nil, + "/new/soundtrack": nil, + "/user": nil, + "/settings": nil, + "/extension/embed": nil, } // API interfaces From 1ab503fc5c7b3f5fcd33b82fbc1a74d1d72db9e1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 3 Jul 2017 23:35:33 +0200 Subject: [PATCH 109/527] Lower font size for smaller resolutions --- styles/base.scarlet | 8 ++++++-- styles/forum.scarlet | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/styles/base.scarlet b/styles/base.scarlet index bf3fea68..a825b800 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -1,15 +1,19 @@ html height 100% + font-family "Ubuntu", "Trebuchet MS", sans-serif + font-size 100% body - font-family "Ubuntu", "Trebuchet MS", sans-serif - font-size 1.05rem tab-size 4 overflow hidden height 100% color text-color background-color bg-color +> 1400px + body + font-size 105% + a color link-color text-decoration none diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 3ba161ef..9066a550 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -19,7 +19,7 @@ .post-content ui-element flex-grow 1 - padding 0.8rem 1rem + padding 0.75rem 1rem position relative h1 From e2772bc2e4c4a73016ba448a549a0651f7f6e801 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 01:16:43 +0200 Subject: [PATCH 110/527] Better font size for macs --- scripts/AnimeNotifier.ts | 5 +++++ styles/base.scarlet | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 955ea59f..2a4923b1 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -45,6 +45,11 @@ export class AnimeNotifier { window.addEventListener("popstate", this.onPopState.bind(this)) this.requestIdleCallback(this.onIdle.bind(this)) + + // Add "osx" class on macs so we can set a proper font-size + if(navigator.platform.includes("Mac")) { + document.rootElement.classList.add("osx") + } } requestIdleCallback(func: Function) { diff --git a/styles/base.scarlet b/styles/base.scarlet index a825b800..c6b84135 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -9,10 +9,11 @@ body height 100% color text-color background-color bg-color + font-size 1.05rem -> 1400px +.osx body - font-size 105% + font-size 1rem a color link-color From 7bdf75cb3b6c8e02af53ff4c1f3f1289bb7c9b16 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 01:20:27 +0200 Subject: [PATCH 111/527] Fixed JS bug --- scripts/AnimeNotifier.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 2a4923b1..1e32d1f9 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -45,11 +45,6 @@ export class AnimeNotifier { window.addEventListener("popstate", this.onPopState.bind(this)) this.requestIdleCallback(this.onIdle.bind(this)) - - // Add "osx" class on macs so we can set a proper font-size - if(navigator.platform.includes("Mac")) { - document.rootElement.classList.add("osx") - } } requestIdleCallback(func: Function) { @@ -69,9 +64,17 @@ export class AnimeNotifier { } run() { + // Add "osx" class on macs so we can set a proper font-size + if(navigator.platform.includes("Mac")) { + document.rootElement.classList.add("osx") + } + + // Initiate the elements we need this.user = this.app.find("user") this.app.content = this.app.find("content") this.app.loading = this.app.find("loading") + + // Let's start this.app.run() } From a23c9fa9a528d35d6d66a965b26b50504cd1c1fe Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 01:25:20 +0200 Subject: [PATCH 112/527] Reduced font size on OSX --- scripts/AnimeNotifier.ts | 2 +- styles/base.scarlet | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 1e32d1f9..c4b5b5ed 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -66,7 +66,7 @@ export class AnimeNotifier { run() { // Add "osx" class on macs so we can set a proper font-size if(navigator.platform.includes("Mac")) { - document.rootElement.classList.add("osx") + document.documentElement.classList.add("osx") } // Initiate the elements we need diff --git a/styles/base.scarlet b/styles/base.scarlet index c6b84135..2f16d1a9 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -3,6 +3,9 @@ html font-family "Ubuntu", "Trebuchet MS", sans-serif font-size 100% +.osx + font-size 95% + body tab-size 4 overflow hidden @@ -11,10 +14,6 @@ body background-color bg-color font-size 1.05rem -.osx - body - font-size 1rem - a color link-color text-decoration none From af8c8b200f3a9445b98105cf1fb8ca0a5fc7565b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 01:42:54 +0200 Subject: [PATCH 113/527] Changed widget size --- styles/widgets.scarlet | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index f14acd84..e1a01517 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -7,7 +7,11 @@ align-items center width 100% padding 0.25rem - max-width 600px + max-width 400px + +> 1240px + .widget + max-width 30vw .widget-element vertical-wrap From 1f9503f3e7b127aad6e40b82d61882e0f7f408db Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 01:50:22 +0200 Subject: [PATCH 114/527] Fixed anime list item view --- pages/animelistitem/animelistitem.scarlet | 2 +- styles/widgets.scarlet | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/animelistitem/animelistitem.scarlet b/pages/animelistitem/animelistitem.scarlet index 8603071c..7b02a4bd 100644 --- a/pages/animelistitem/animelistitem.scarlet +++ b/pages/animelistitem/animelistitem.scarlet @@ -7,4 +7,4 @@ justify-content space-between width 100% .widget-input - max-width 120px \ No newline at end of file + max-width 20% \ No newline at end of file diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index e1a01517..43d6541a 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -13,6 +13,10 @@ .widget max-width 30vw +< 810px + .widget + max-width 600px + .widget-element vertical-wrap ui-element From 7d9106f6eaa482d8351825f14985eef69232e9c8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 10:26:18 +0200 Subject: [PATCH 115/527] For the lulz --- pages/frontpage/frontpage.pixy | 4 ++++ pages/frontpage/frontpage.scarlet | 12 ++++++++++++ pages/listimport/listimportanilist/anilist.go | 10 +++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 0a364af2..22ad730b 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -2,6 +2,10 @@ component FrontPage .frontpage h2 notify.moe + video.bg-video(autoplay="true", loop="true") + source(src="//s1.webmshare.com/ZrK1J.webm") + + 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 diff --git a/pages/frontpage/frontpage.scarlet b/pages/frontpage/frontpage.scarlet index 98cb03ae..533ec374 100644 --- a/pages/frontpage/frontpage.scarlet +++ b/pages/frontpage/frontpage.scarlet @@ -7,11 +7,23 @@ font-weight normal letter-spacing 3px text-transform uppercase + color white .footer text-align center margin-top content-padding +.bg-video + position absolute + top 50% + left 50% + transform translateX(-50%) translateY(-50%) + min-width 100% + min-height 100% + width auto + height auto + z-index -100 + .screenshot max-width 100% border-radius 3px diff --git a/pages/listimport/listimportanilist/anilist.go b/pages/listimport/listimportanilist/anilist.go index b1601214..35016ff7 100644 --- a/pages/listimport/listimportanilist/anilist.go +++ b/pages/listimport/listimportanilist/anilist.go @@ -35,6 +35,13 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from AniList", err) } + matches := findAllMatches(allAnime, animeList) + + return ctx.HTML(components.ImportAnilist(user, matches)) +} + +// findAllMatches returns all matches for the anime inside an anilist anime list. +func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*arn.AniListMatch { matches := []*arn.AniListMatch{} matches = importList(matches, allAnime, animeList.Lists.Watching) @@ -47,9 +54,10 @@ func Get(ctx *aero.Context) string { matches = importList(matches, allAnime, list) } - return ctx.HTML(components.ImportAnilist(user, matches)) + return matches } +// importList imports a single list inside an anilist anime list collection. func importList(matches []*arn.AniListMatch, allAnime []*arn.Anime, animeListItems []*arn.AniListAnimeListItem) []*arn.AniListMatch { for _, item := range animeListItems { matches = append(matches, &arn.AniListMatch{ From 6f5c78b3179feb2cba234de1e0e9d7c85c03b512 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 11:20:31 +0200 Subject: [PATCH 116/527] New frontpage --- pages/frontpage/frontpage.pixy | 12 +++++++----- pages/frontpage/frontpage.scarlet | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 22ad730b..d81b2761 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -2,15 +2,17 @@ component FrontPage .frontpage h2 notify.moe - video.bg-video(autoplay="true", loop="true") - source(src="//s1.webmshare.com/ZrK1J.webm") + p Your home for everything about anime. - - 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") + //- 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 + a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub + + video.bg-video(autoplay="true", loop="true") + source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") + source(src="//s1.webmshare.com/f/nZVby.mp4", type="video/mp4") \ No newline at end of file diff --git a/pages/frontpage/frontpage.scarlet b/pages/frontpage/frontpage.scarlet index 533ec374..5678d78c 100644 --- a/pages/frontpage/frontpage.scarlet +++ b/pages/frontpage/frontpage.scarlet @@ -1,13 +1,27 @@ .frontpage vertical align-items center + position absolute + top 50% + left 50% + transform translateX(-50%) translateY(-50%) + + a, h2, p + color white !important + text-shadow 0px 0px 4px rgb(0, 0, 0, 0.75) h2 font-size 2.5rem font-weight normal - letter-spacing 3px + letter-spacing 5px text-transform uppercase - color white + line-height 1.2em + + p + font-size 2rem + margin-bottom 1em + line-height 1.2em + text-align center .footer text-align center From a48f8b903beccb6bc3f0b6307a00f654a35f265d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 12:12:59 +0200 Subject: [PATCH 117/527] Lower brightness video --- pages/frontpage/frontpage.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index d81b2761..052b9bc0 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -14,5 +14,5 @@ component FrontPage a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub video.bg-video(autoplay="true", loop="true") - source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") - source(src="//s1.webmshare.com/f/nZVby.mp4", type="video/mp4") \ No newline at end of file + source(src="//s1.webmshare.com/qN3a9.webm", type="video/webm") + source(src="//s1.webmshare.com/f/qN3a9.mp4", type="video/mp4") \ No newline at end of file From f882b4df259d1af929119b163f205f823ce71477 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 12:19:16 +0200 Subject: [PATCH 118/527] Another video source --- pages/frontpage/frontpage.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 052b9bc0..b44b7f3d 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -14,5 +14,5 @@ component FrontPage a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub video.bg-video(autoplay="true", loop="true") - source(src="//s1.webmshare.com/qN3a9.webm", type="video/webm") - source(src="//s1.webmshare.com/f/qN3a9.mp4", type="video/mp4") \ No newline at end of file + source(src="//s1.webmshare.com/3yAQj.webm", type="video/webm") + source(src="//s1.webmshare.com/f/3yAQj.mp4", type="video/mp4") \ No newline at end of file From 780c2e1c56bd04c296a826031d215b8ffd922a92 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 12:37:06 +0200 Subject: [PATCH 119/527] Frontpage mountable --- pages/frontpage/frontpage.pixy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index b44b7f3d..792f113c 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -1,5 +1,5 @@ component FrontPage - .frontpage + .frontpage.mountable h2 notify.moe p Your home for everything about anime. @@ -14,5 +14,5 @@ component FrontPage a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub video.bg-video(autoplay="true", loop="true") - source(src="//s1.webmshare.com/3yAQj.webm", type="video/webm") - source(src="//s1.webmshare.com/f/3yAQj.mp4", type="video/mp4") \ No newline at end of file + source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") + source(src="//s1.webmshare.com/f/nZVby.mp4", type="video/mp4") \ No newline at end of file From 81d58bfd8e793347c8ded5a562b0f84a06f5b6c5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 14:34:58 +0200 Subject: [PATCH 120/527] Improved search --- jobs/search-index/search-index.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/jobs/search-index/search-index.go b/jobs/search-index/search-index.go index fd52dfe6..0edb8450 100644 --- a/jobs/search-index/search-index.go +++ b/jobs/search-index/search-index.go @@ -28,22 +28,23 @@ func updateAnimeIndex() { } for anime := range animeStream { + if anime.Title.Canonical != "" { + animeSearchIndex.TextToID[strings.ToLower(anime.Title.Canonical)] = anime.ID + } + if anime.Title.Romaji != "" { animeSearchIndex.TextToID[strings.ToLower(anime.Title.Romaji)] = anime.ID } - if anime.Title.English != "" { - animeSearchIndex.TextToID[strings.ToLower(anime.Title.English)] = anime.ID - } - - // Make sure we only include Japanese titles that actually contain unicode letters - // because otherwise they might overlap with the English titles. - if anime.Title.Japanese != "" && arn.ContainsUnicodeLetters(anime.Title.Japanese) { + // Make sure we only include Japanese titles that + // don't overlap with the English titles. + if anime.Title.Japanese != "" && animeSearchIndex.TextToID[strings.ToLower(anime.Title.Japanese)] == "" { animeSearchIndex.TextToID[strings.ToLower(anime.Title.Japanese)] = anime.ID } - if anime.Title.Canonical != "" { - animeSearchIndex.TextToID[strings.ToLower(anime.Title.Canonical)] = anime.ID + // Same with English titles, don't overwrite other stuff. + if anime.Title.English != "" && animeSearchIndex.TextToID[strings.ToLower(anime.Title.English)] == "" { + animeSearchIndex.TextToID[strings.ToLower(anime.Title.English)] = anime.ID } for _, synonym := range anime.Title.Synonyms { From 52431a0a0536b2051705deb8a65964e662fc6af2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 16:13:20 +0200 Subject: [PATCH 121/527] Fixed custom list import --- pages/animelist/animelist.scarlet | 2 +- scripts/AnimeNotifier.ts | 2 +- styles/include/config.scarlet | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 40d76e0e..8c32383f 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -1,7 +1,7 @@ .anime-list-container vertical width 100% - max-width 1200px + max-width table-width-normal margin 0 auto margin-bottom 1rem diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index c4b5b5ed..141fd677 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -318,7 +318,7 @@ export class AnimeNotifier { } // F = Search - if(e.keyCode == 70) { + if(e.keyCode == 70 && !e.ctrlKey) { let search = this.app.find("search") as HTMLInputElement search.focus() diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 5f713fb0..edda0242 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -41,6 +41,9 @@ nav-link-hover-slide-color = main-color // nav-link-color = rgb(160, 160, 160) // nav-link-hover-color = rgb(80, 80, 80) +// Tables +table-width-normal = 1200px + // Loading animation loading-anim-color = nav-link-hover-slide-color From 10c5e4af8e060ed8ecb02ab5e4295219de542289 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 16:24:46 +0200 Subject: [PATCH 122/527] Fixed import --- pages/listimport/listimportanilist/anilist.go | 8 +++++++- pages/listimport/listimportanilist/anilist.pixy | 4 ++-- pages/listimport/listimportanilist/anilist.scarlet | 3 +++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 pages/listimport/listimportanilist/anilist.scarlet diff --git a/pages/listimport/listimportanilist/anilist.go b/pages/listimport/listimportanilist/anilist.go index 35016ff7..f5372148 100644 --- a/pages/listimport/listimportanilist/anilist.go +++ b/pages/listimport/listimportanilist/anilist.go @@ -50,7 +50,13 @@ func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*a matches = importList(matches, allAnime, animeList.Lists.OnHold) matches = importList(matches, allAnime, animeList.Lists.Dropped) - for _, list := range animeList.CustomLists { + custom, ok := animeList.CustomLists.(map[string][]*arn.AniListAnimeListItem) + + if !ok { + return matches + } + + for _, list := range custom { matches = importList(matches, allAnime, list) } diff --git a/pages/listimport/listimportanilist/anilist.pixy b/pages/listimport/listimportanilist/anilist.pixy index b9b57834..51562268 100644 --- a/pages/listimport/listimportanilist/anilist.pixy +++ b/pages/listimport/listimportanilist/anilist.pixy @@ -1,7 +1,7 @@ component ImportAnilist(user *arn.User, matches []*arn.AniListMatch) - h2= "Import: anilist.co" + h2= "anilist.co Import (" + user.Accounts.AniList.Nick + ", " + toString(len(matches)) + " anime)" - table + table.import-list thead tr th anilist.co diff --git a/pages/listimport/listimportanilist/anilist.scarlet b/pages/listimport/listimportanilist/anilist.scarlet new file mode 100644 index 00000000..34b92482 --- /dev/null +++ b/pages/listimport/listimportanilist/anilist.scarlet @@ -0,0 +1,3 @@ +.import-list + max-width table-width-normal + margin 0 auto \ No newline at end of file From 131297b658d1514c08c00e870e46ef3f12afce4b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 17:08:20 +0200 Subject: [PATCH 123/527] Fixed video source --- pages/frontpage/frontpage.pixy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 792f113c..0ca59b54 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -14,5 +14,4 @@ component FrontPage a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub video.bg-video(autoplay="true", loop="true") - source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") - source(src="//s1.webmshare.com/f/nZVby.mp4", type="video/mp4") \ No newline at end of file + source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") \ No newline at end of file From 788adcaf2d59c18ba462b3b674c5050f9a89367f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 4 Jul 2017 23:44:51 +0200 Subject: [PATCH 124/527] Added mp4 version --- pages/frontpage/frontpage.pixy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 0ca59b54..9def8454 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -14,4 +14,5 @@ component FrontPage a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub video.bg-video(autoplay="true", loop="true") - source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") \ No newline at end of file + source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") + source(src="//cdn-e2.streamable.com/video/mp4/e5mx7.mp4?token=1500414089_8b2b3b0665984dcf4dc8d33e534bc1c8881b2da1", type="video/mp4") \ No newline at end of file From 5774c51ced1c07355be8a4395ddfc97999fd9d1b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 00:40:03 +0200 Subject: [PATCH 125/527] Finished anilist importer --- main.go | 3 +- pages/listimport/listimportanilist/anilist.go | 72 ++++++++++++++++--- .../listimport/listimportanilist/anilist.pixy | 6 +- pages/profile/profile.go | 22 ------ 4 files changed, 67 insertions(+), 36 deletions(-) diff --git a/main.go b/main.go index 83e5dd83..dd2b9390 100644 --- a/main.go +++ b/main.go @@ -82,7 +82,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/settings", settings.Get) app.Ajax("/music", music.Get) app.Ajax("/import", listimport.Get) - app.Ajax("/import/anilist/animelist", listimportanilist.Get) + app.Ajax("/import/anilist/animelist", listimportanilist.Preview) + app.Ajax("/import/anilist/animelist/finish", listimportanilist.Finish) app.Ajax("/admin", admin.Get) app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) diff --git a/pages/listimport/listimportanilist/anilist.go b/pages/listimport/listimportanilist/anilist.go index f5372148..e3adb193 100644 --- a/pages/listimport/listimportanilist/anilist.go +++ b/pages/listimport/listimportanilist/anilist.go @@ -9,37 +9,89 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -// Get ... -func Get(ctx *aero.Context) string { +func getMatches(ctx *aero.Context) ([]*arn.AniListMatch, string) { user := utils.GetUser(ctx) if user == nil { - return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + return nil, ctx.Error(http.StatusBadRequest, "Not logged in", nil) } authErr := arn.AniList.Authorize() if authErr != nil { - return ctx.Error(http.StatusBadRequest, "Couldn't authorize the Anime Notifier app on AniList", authErr) + return nil, ctx.Error(http.StatusBadRequest, "Couldn't authorize the Anime Notifier app on AniList", authErr) } allAnime, allErr := arn.AllAnime() if allErr != nil { - return ctx.Error(http.StatusBadRequest, "Couldn't load notify.moe list of all anime", allErr) + return nil, ctx.Error(http.StatusBadRequest, "Couldn't load notify.moe list of all anime", allErr) } - animeList, err := arn.AniList.GetAnimeList(user) + anilistAnimeList, err := arn.AniList.GetAnimeList(user) if err != nil { - return ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from AniList", err) + return nil, ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from AniList", err) } - matches := findAllMatches(allAnime, animeList) + matches := findAllMatches(allAnime, anilistAnimeList) + + return matches, "" +} + +// Preview ... +func Preview(ctx *aero.Context) string { + user := utils.GetUser(ctx) + matches, response := getMatches(ctx) + + if response != "" { + return response + } return ctx.HTML(components.ImportAnilist(user, matches)) } +// Finish ... +func Finish(ctx *aero.Context) string { + user := utils.GetUser(ctx) + matches, response := getMatches(ctx) + + if response != "" { + return response + } + + animeList := user.AnimeList() + + for _, match := range matches { + if match.ARNAnime == nil || match.AniListItem == nil { + continue + } + + item := &arn.AnimeListItem{ + AnimeID: match.ARNAnime.ID, + Status: match.AniListItem.AnimeListStatus(), + Episodes: match.AniListItem.EpisodesWatched, + Notes: match.AniListItem.Notes, + Rating: &arn.AnimeRating{ + Overall: float64(match.AniListItem.ScoreRaw) / 10.0, + }, + RewatchCount: match.AniListItem.Rewatched, + Created: arn.DateTimeUTC(), + Edited: arn.DateTimeUTC(), + } + + animeList.Import(item) + } + + err := animeList.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving your anime list", err) + } + + return ctx.Redirect("/+" + user.Nick + "/animelist") +} + // findAllMatches returns all matches for the anime inside an anilist anime list. func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*arn.AniListMatch { matches := []*arn.AniListMatch{} @@ -67,8 +119,8 @@ func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*a func importList(matches []*arn.AniListMatch, allAnime []*arn.Anime, animeListItems []*arn.AniListAnimeListItem) []*arn.AniListMatch { for _, item := range animeListItems { matches = append(matches, &arn.AniListMatch{ - AniListAnime: item.Anime, - ARNAnime: arn.FindAniListAnime(item.Anime, allAnime), + AniListItem: item, + ARNAnime: arn.FindAniListAnime(item.Anime, allAnime), }) } diff --git a/pages/listimport/listimportanilist/anilist.pixy b/pages/listimport/listimportanilist/anilist.pixy index 51562268..c8e4772f 100644 --- a/pages/listimport/listimportanilist/anilist.pixy +++ b/pages/listimport/listimportanilist/anilist.pixy @@ -10,14 +10,14 @@ component ImportAnilist(user *arn.User, matches []*arn.AniListMatch) each match in matches tr td - a(href=match.AniListAnime.Link(), target="_blank", rel="noopener")= match.AniListAnime.TitleRomaji + a(href=match.AniListItem.Anime.Link(), target="_blank", rel="noopener")= match.AniListItem.Anime.TitleRomaji td if match.ARNAnime == nil span.import-error Not found on notify.moe else a(href=match.ARNAnime.Link(), target="_blank", rel="noopener")= match.ARNAnime.Title.Canonical - + .buttons - .button.mountable.action(data-action="soon", data-trigger="click") + a.button.mountable(href="/import/anilist/animelist/finish") Icon("refresh") span Import \ No newline at end of file diff --git a/pages/profile/profile.go b/pages/profile/profile.go index 16117f39..e54912cd 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -35,28 +35,6 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { user = utils.GetUser(ctx) }, func() { animeList = viewUser.AnimeList() - }, func() { - // threads = viewUser.Threads() - - // arn.SortThreadsLatestFirst(threads) - - // if len(threads) > maxPosts { - // threads = threads[:maxPosts] - // } - }, func() { - // posts = viewUser.Posts() - // arn.SortPostsLatestFirst(posts) - - // if len(posts) > maxPosts { - // posts = posts[:maxPosts] - // } - }, func() { - // tracks = viewUser.SoundTracks() - // arn.SortSoundTracksLatestFirst(tracks) - - // if len(tracks) > maxTracks { - // tracks = tracks[:maxTracks] - // } }) return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts, tracks)) From 7b611992cb1ddeb11f8f83b7115ca3ec0095f0a3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 02:56:50 +0200 Subject: [PATCH 126/527] Improved mobile list --- pages/animelist/animelist.scarlet | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 8c32383f..5222ae79 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -52,11 +52,16 @@ .anime-list-item-actions flex-basis 40px text-align right + display none // Beautify icon alignment .raw-icon margin-bottom -4px +> 740px + .anime-list-item-actions + display block + .anime-list-item-airing-date display none From 0e42105049461872b1d591abb94134f1c2299e13 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 04:10:19 +0200 Subject: [PATCH 127/527] Added chrome extension --- .gitignore | 1 + benchmarks/Components_test.go | 14 ++++++++++++++ pages/settings/settings.pixy | 10 ++++++++++ profiler.go | 16 ++++++++++++++++ styles/widgets.scarlet | 3 ++- 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 profiler.go diff --git a/.gitignore b/.gitignore index 901b7b49..4aeada51 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ _testmain.go *.exe *.test *.prof +*.pprof # Output of the go coverage tool, specifically when used with LiteIDE *.out diff --git a/benchmarks/Components_test.go b/benchmarks/Components_test.go index ef68455e..d954fd86 100644 --- a/benchmarks/Components_test.go +++ b/benchmarks/Components_test.go @@ -25,3 +25,17 @@ func BenchmarkThread(b *testing.B) { } }) } + +func BenchmarkAnimeList(b *testing.B) { + user, _ := arn.GetUser("4J6qpK1ve") + animeList := user.AnimeList() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + components.AnimeList(animeList, user, user) + } + }) +} diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 42a6b118..0580ddc4 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -56,6 +56,16 @@ component Settings(user *arn.User) ImportLists(user) + .widget.mountable + h3.widget-title + Icon("puzzle-piece") + span Extensions + + .buttons + button.action(data-action="installExtension", data-trigger="click") + Icon("chrome") + span Get the Chrome Extension + //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title //- Icon("cogs") diff --git a/profiler.go b/profiler.go new file mode 100644 index 00000000..b77e04a3 --- /dev/null +++ b/profiler.go @@ -0,0 +1,16 @@ +package main + +func init() { + // Uncomment these if you want to enable live profiling via /debug/pprof + + // app.Router.HandlerFunc("GET", "/debug/pprof/", http.HandlerFunc(pprof.Index)) + // app.Router.HandlerFunc("GET", "/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) + // app.Router.HandlerFunc("GET", "/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) + // app.Router.HandlerFunc("GET", "/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) + // app.Router.HandlerFunc("GET", "/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) + + // app.Router.Handler("GET", "/debug/pprof/goroutine", pprof.Handler("goroutine")) + // app.Router.Handler("GET", "/debug/pprof/heap", pprof.Handler("heap")) + // app.Router.Handler("GET", "/debug/pprof/threadcreate", pprof.Handler("threadcreate")) + // app.Router.Handler("GET", "/debug/pprof/block", pprof.Handler("block")) +} diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index 43d6541a..33940d6b 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -38,4 +38,5 @@ width 100% .widget-title - // \ No newline at end of file + // We need !important here to overwrite the h3:first-child rule + margin 1rem 0 !important \ No newline at end of file From 8dbc8f17cf7bad1b19f9a671b147ac90c04300eb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 04:34:06 +0200 Subject: [PATCH 128/527] Added benchmark --- benchmarks/Components_test.go | 5 +++++ benchmarks/DB_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 benchmarks/DB_test.go diff --git a/benchmarks/Components_test.go b/benchmarks/Components_test.go index d954fd86..201a3446 100644 --- a/benchmarks/Components_test.go +++ b/benchmarks/Components_test.go @@ -30,6 +30,11 @@ func BenchmarkAnimeList(b *testing.B) { user, _ := arn.GetUser("4J6qpK1ve") animeList := user.AnimeList() + // Prefetch + for _, item := range animeList.Items { + item.Anime() + } + b.ReportAllocs() b.ResetTimer() diff --git a/benchmarks/DB_test.go b/benchmarks/DB_test.go new file mode 100644 index 00000000..63c13cfb --- /dev/null +++ b/benchmarks/DB_test.go @@ -0,0 +1,29 @@ +package benchmarks + +import ( + "testing" + + "github.com/animenotifier/arn" +) + +func BenchmarkDBGetMap(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + arn.DB.GetMap("AnimeList", "4J6qpK1ve") + } + }) +} + +func BenchmarkDBGet(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + arn.DB.Get("AnimeList", "4J6qpK1ve") + } + }) +} From 288ca35ec8510b681f8128d8fab6401fdff641cc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 13:08:27 +0200 Subject: [PATCH 129/527] Improved anime list serialization speed --- benchmarks/DB_test.go | 11 +++++++++-- styles/include/config.scarlet | 2 +- tests.go | 25 +++++++++++++------------ 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/benchmarks/DB_test.go b/benchmarks/DB_test.go index 63c13cfb..bca3ee29 100644 --- a/benchmarks/DB_test.go +++ b/benchmarks/DB_test.go @@ -7,12 +7,15 @@ import ( ) func BenchmarkDBGetMap(b *testing.B) { + user, _ := arn.GetUser("4J6qpK1ve") + b.ReportAllocs() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - arn.DB.GetMap("AnimeList", "4J6qpK1ve") + animeList, _ := arn.GetAnimeList(user) + noop(animeList) } }) } @@ -23,7 +26,11 @@ func BenchmarkDBGet(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { - arn.DB.Get("AnimeList", "4J6qpK1ve") + list, _ := arn.DB.Get("AnimeList", "4J6qpK1ve") + animeList := list.(*arn.AnimeList) + noop(animeList) } }) } + +func noop(list *arn.AnimeList) {} diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index edda0242..14decf4c 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -64,4 +64,4 @@ nav-height = 3.11rem // Timings fade-speed = 200ms -transition-speed = 270ms \ No newline at end of file +transition-speed = 250ms \ No newline at end of file diff --git a/tests.go b/tests.go index 5f631dfd..23b7cac9 100644 --- a/tests.go +++ b/tests.go @@ -154,18 +154,19 @@ var routeTests = map[string][]string{ }, // Disable these tests because they require authorization - "/auth/google": nil, - "/auth/google/callback": nil, - "/auth/facebook": nil, - "/auth/facebook/callback": nil, - "/import": nil, - "/import/anilist/animelist": nil, - "/anime/:id/edit": nil, - "/new/thread": nil, - "/new/soundtrack": nil, - "/user": nil, - "/settings": nil, - "/extension/embed": nil, + "/auth/google": nil, + "/auth/google/callback": nil, + "/auth/facebook": nil, + "/auth/facebook/callback": nil, + "/import": nil, + "/import/anilist/animelist": nil, + "/import/anilist/animelist/finish": nil, + "/anime/:id/edit": nil, + "/new/thread": nil, + "/new/soundtrack": nil, + "/user": nil, + "/settings": nil, + "/extension/embed": nil, } // API interfaces From 1f33df224b3186e36bc09110169d7c7ba3200457 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 13:46:20 +0200 Subject: [PATCH 130/527] Removed nyaa icon for now --- benchmarks/Components_test.go | 4 ++-- benchmarks/{DB_test.go => DB_AnimeList_test.go} | 4 ++-- pages/animelist/animelist.pixy | 9 +++++---- 3 files changed, 9 insertions(+), 8 deletions(-) rename benchmarks/{DB_test.go => DB_AnimeList_test.go} (84%) diff --git a/benchmarks/Components_test.go b/benchmarks/Components_test.go index 201a3446..476b77be 100644 --- a/benchmarks/Components_test.go +++ b/benchmarks/Components_test.go @@ -7,7 +7,7 @@ import ( "github.com/animenotifier/notify.moe/components" ) -func BenchmarkThread(b *testing.B) { +func BenchmarkRenderThread(b *testing.B) { thread, _ := arn.GetThread("HJgS7c2K") thread.HTML() // Pre-render markdown @@ -26,7 +26,7 @@ func BenchmarkThread(b *testing.B) { }) } -func BenchmarkAnimeList(b *testing.B) { +func BenchmarkRenderAnimeList(b *testing.B) { user, _ := arn.GetUser("4J6qpK1ve") animeList := user.AnimeList() diff --git a/benchmarks/DB_test.go b/benchmarks/DB_AnimeList_test.go similarity index 84% rename from benchmarks/DB_test.go rename to benchmarks/DB_AnimeList_test.go index bca3ee29..8d79a521 100644 --- a/benchmarks/DB_test.go +++ b/benchmarks/DB_AnimeList_test.go @@ -6,7 +6,7 @@ import ( "github.com/animenotifier/arn" ) -func BenchmarkDBGetMap(b *testing.B) { +func BenchmarkDBAnimeListGetMap(b *testing.B) { user, _ := arn.GetUser("4J6qpK1ve") b.ReportAllocs() @@ -20,7 +20,7 @@ func BenchmarkDBGetMap(b *testing.B) { }) } -func BenchmarkDBGet(b *testing.B) { +func BenchmarkDBAnimeListGet(b *testing.B) { b.ReportAllocs() b.ResetTimer() diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index bf85ef74..e3bfcf61 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -72,7 +72,8 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Visuals", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Visuals) //- td.anime-list-item-rating(title="Soundtrack rating") //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Soundtrack", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Soundtrack) - 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 + + //- 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 From 9d28c652c0ddd4978e820b9bc26d4fe18197ae93 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 13:58:37 +0200 Subject: [PATCH 131/527] Added prefetching anime objects for anime lists --- benchmarks/Components_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/benchmarks/Components_test.go b/benchmarks/Components_test.go index 476b77be..eb01f210 100644 --- a/benchmarks/Components_test.go +++ b/benchmarks/Components_test.go @@ -30,11 +30,6 @@ func BenchmarkRenderAnimeList(b *testing.B) { user, _ := arn.GetUser("4J6qpK1ve") animeList := user.AnimeList() - // Prefetch - for _, item := range animeList.Items { - item.Anime() - } - b.ReportAllocs() b.ResetTimer() From a94b69d671e8052099e9bb349b472cbd1b6dd94c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 14:34:33 +0200 Subject: [PATCH 132/527] Explicit PrefetchAnime call --- benchmarks/Components_test.go | 1 + pages/animelist/animelist.go | 1 + pages/dashboard/dashboard.go | 17 ++--------------- pages/embed/embed.go | 1 + pages/profile/profile.go | 1 + 5 files changed, 6 insertions(+), 15 deletions(-) diff --git a/benchmarks/Components_test.go b/benchmarks/Components_test.go index eb01f210..514a1ac7 100644 --- a/benchmarks/Components_test.go +++ b/benchmarks/Components_test.go @@ -29,6 +29,7 @@ func BenchmarkRenderThread(b *testing.B) { func BenchmarkRenderAnimeList(b *testing.B) { user, _ := arn.GetUser("4J6qpK1ve") animeList := user.AnimeList() + animeList.PrefetchAnime() b.ReportAllocs() b.ResetTimer() diff --git a/pages/animelist/animelist.go b/pages/animelist/animelist.go index e404d300..3f9df413 100644 --- a/pages/animelist/animelist.go +++ b/pages/animelist/animelist.go @@ -25,6 +25,7 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime list not found", nil) } + animeList.PrefetchAnime() animeList.Sort() return ctx.HTML(components.AnimeLists(animeList.SplitByStatus(), animeList.User(), user)) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index cdb780a3..31f554ce 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -47,27 +47,14 @@ func dashboard(ctx *aero.Context) string { } animeList = animeList.WatchingAndPlanned() - - var keys []string + animeList.PrefetchAnime() for _, item := range animeList.Items { - keys = append(keys, item.AnimeID) - } - - objects, getErr := arn.DB.GetMany("Anime", keys) - - if getErr != nil { - return - } - - allAnimeInList := objects.([]*arn.Anime) - - for _, anime := range allAnimeInList { if len(upcomingEpisodes) >= maxScheduleItems { break } - futureEpisodes := anime.UpcomingEpisodes() + futureEpisodes := item.Anime().UpcomingEpisodes() if len(futureEpisodes) == 0 { continue diff --git a/pages/embed/embed.go b/pages/embed/embed.go index 7a3ffd57..d191de48 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -23,6 +23,7 @@ func Get(ctx *aero.Context) string { } watchingList := animeList.WatchingAndPlanned() + watchingList.PrefetchAnime() watchingList.Sort() return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(watchingList, animeList.User(), user))) diff --git a/pages/profile/profile.go b/pages/profile/profile.go index e54912cd..a06deee9 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -35,6 +35,7 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { user = utils.GetUser(ctx) }, func() { animeList = viewUser.AnimeList() + animeList.PrefetchAnime() }) return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts, tracks)) From 59daa5404ec36ff4bb2b1c83ca8f0ccd5ff25183 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 15:00:58 +0200 Subject: [PATCH 133/527] Improved anime list rendering --- pages/animelist/animelist.pixy | 4 ++-- scripts/AnimeNotifier.ts | 4 +++- utils/{allowembed.go => AllowEmbed.go} | 0 utils/{container.go => GetContainerClass.go} | 0 utils/{user.go => GetUser.go} | 0 utils/{icons.go => Icon.go} | 0 utils/ItemCSSClass.go | 14 ++++++++++++++ 7 files changed, 19 insertions(+), 3 deletions(-) rename utils/{allowembed.go => AllowEmbed.go} (100%) rename utils/{container.go => GetContainerClass.go} (100%) rename utils/{user.go => GetUser.go} (100%) rename utils/{icons.go => Icon.go} (100%) create mode 100644 utils/ItemCSSClass.go diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index e3bfcf61..23b04269 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -49,8 +49,8 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User if user != nil th.anime-list-item-actions Actions tbody - each item in animeList.Items - tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) + for i, item := range animeList.Items + tr(class=utils.ItemCSSClass(animeList, i), title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical td.anime-list-item-airing-date diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 141fd677..03e29c10 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -223,8 +223,10 @@ export class AnimeNotifier { const maxDelay = 1000 let time = 0 + let collection = document.getElementsByClassName(className) - for(let element of findAll(className)) { + for(let i = 0; i < collection.length; i++) { + let element = collection.item(i) as HTMLElement let type = element.dataset.mountableType || "general" if(type in mountableTypes) { diff --git a/utils/allowembed.go b/utils/AllowEmbed.go similarity index 100% rename from utils/allowembed.go rename to utils/AllowEmbed.go diff --git a/utils/container.go b/utils/GetContainerClass.go similarity index 100% rename from utils/container.go rename to utils/GetContainerClass.go diff --git a/utils/user.go b/utils/GetUser.go similarity index 100% rename from utils/user.go rename to utils/GetUser.go diff --git a/utils/icons.go b/utils/Icon.go similarity index 100% rename from utils/icons.go rename to utils/Icon.go diff --git a/utils/ItemCSSClass.go b/utils/ItemCSSClass.go new file mode 100644 index 00000000..0f175ea5 --- /dev/null +++ b/utils/ItemCSSClass.go @@ -0,0 +1,14 @@ +package utils + +import ( + "github.com/animenotifier/arn" +) + +// ItemCSSClass removes mountable class if the list has too many items. +func ItemCSSClass(list *arn.AnimeList, index int) string { + if index > 20 || len(list.Items) > 50 { + return "anime-list-item" + } + + return "anime-list-item mountable" +} From 3f0e778984e0c06a76adb44ad44edd3f45f908c1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 15:29:18 +0200 Subject: [PATCH 134/527] Improved mountable performance --- scripts/Actions.ts | 4 ++-- scripts/AnimeNotifier.ts | 45 +++++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 38d2ceca..dcb31d1f 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -95,11 +95,11 @@ export function diff(arn: AnimeNotifier, element: HTMLElement) { let oldScroll = arn.app.content.parentElement.scrollTop let newScroll = Math.max(target.offsetTop - contentPadding, 0) let scrollDistance = newScroll - oldScroll - let timeStart = performance.now() + let timeStart = Date.now() let timeEnd = timeStart + duration let scroll = () => { - let time = performance.now() + let time = Date.now() let progress = (time - timeStart) / duration if(progress > 1.0) { diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 03e29c10..72fb830f 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -215,15 +215,16 @@ export class AnimeNotifier { } modifyDelayed(className: string, func: (element: HTMLElement) => void) { - let mountableTypes = { - general: 0 - } - const delay = 20 - const maxDelay = 1000 let time = 0 + let start = Date.now() let collection = document.getElementsByClassName(className) + let mutations = [] + + let mountableTypes = { + general: start + } for(let i = 0; i < collection.length; i++) { let element = collection.item(i) as HTMLElement @@ -232,17 +233,37 @@ export class AnimeNotifier { if(type in mountableTypes) { time = mountableTypes[type] += delay } else { - time = mountableTypes[type] = 0 + time = mountableTypes[type] = start } - if(time > maxDelay) { - func(element) - } else { - setTimeout(() => { - window.requestAnimationFrame(() => func(element)) - }, time) + mutations.push({ + element, + time + }) + } + + let mutationIndex = 0 + + let updateBatch = () => { + let now = Date.now() + + for(; mutationIndex < mutations.length; mutationIndex++) { + let mutation = mutations[mutationIndex] + console.log(mutation.time - now) + + if(mutation.time > now) { + break + } + + func(mutation.element) + } + + if(mutationIndex < mutations.length) { + window.requestAnimationFrame(updateBatch) } } + + window.requestAnimationFrame(updateBatch) } diff(url: string) { From 91fc121520ab43b58efa321d3a78e419648e870c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 15:41:12 +0200 Subject: [PATCH 135/527] Removed log --- scripts/AnimeNotifier.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 72fb830f..32b374e5 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -249,7 +249,6 @@ export class AnimeNotifier { for(; mutationIndex < mutations.length; mutationIndex++) { let mutation = mutations[mutationIndex] - console.log(mutation.time - now) if(mutation.time > now) { break From 57415c69872810c249735ce56c2a6b50c9a60ed4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 15:46:38 +0200 Subject: [PATCH 136/527] Faster layout --- pages/animelistitem/animelistitem.scarlet | 4 ---- pages/profile/watching.scarlet | 1 + pages/search/search.scarlet | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pages/animelistitem/animelistitem.scarlet b/pages/animelistitem/animelistitem.scarlet index 7b02a4bd..e1e7cc3e 100644 --- a/pages/animelistitem/animelistitem.scarlet +++ b/pages/animelistitem/animelistitem.scarlet @@ -1,7 +1,3 @@ -.anime-list-item-view-image - max-width 55px - margin-bottom 1rem - .anime-list-item-rating-edit horizontal-wrap justify-content space-between diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index c77377f6..8c96643a 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -7,6 +7,7 @@ .profile-watching-list-item-image width 55px !important + height 78px !important border-radius 2px // < 380px diff --git a/pages/search/search.scarlet b/pages/search/search.scarlet index 9253f4fe..ec095322 100644 --- a/pages/search/search.scarlet +++ b/pages/search/search.scarlet @@ -1,2 +1,3 @@ .anime-search-result - width 55px !important \ No newline at end of file + width 55px !important + height 78px !important \ No newline at end of file From 3df506b2743ee107d5dfbac6c5864132349493b4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 15:50:29 +0200 Subject: [PATCH 137/527] Removed planned from extension --- pages/dashboard/dashboard.go | 2 +- pages/embed/embed.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 31f554ce..3050e74a 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -46,7 +46,7 @@ func dashboard(ctx *aero.Context) string { return } - animeList = animeList.WatchingAndPlanned() + animeList = animeList.Watching() animeList.PrefetchAnime() for _, item := range animeList.Items { diff --git a/pages/embed/embed.go b/pages/embed/embed.go index d191de48..8010d842 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -22,7 +22,7 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime list not found", nil) } - watchingList := animeList.WatchingAndPlanned() + watchingList := animeList.Watching() watchingList.PrefetchAnime() watchingList.Sort() From 44803adc2de5acce5b8e10610f5a556cb5812ea8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 16:52:24 +0200 Subject: [PATCH 138/527] Started working on status filter --- main.go | 7 +++++- pages/anime/anime.pixy | 2 +- pages/animelist/status.go | 46 +++++++++++++++++++++++++++++++++++ pages/animelist/watching.pixy | 8 ++++++ pages/profile/profile.pixy | 24 ++++++++++++++++++ 5 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 pages/animelist/status.go create mode 100644 pages/animelist/watching.pixy diff --git a/main.go b/main.go index dd2b9390..dfc2eb8c 100644 --- a/main.go +++ b/main.go @@ -76,7 +76,12 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/posts", profile.GetPostsByUser) app.Ajax("/user/:nick/tracks", profile.GetSoundTracksByUser) app.Ajax("/user/:nick/animelist", animelist.Get) - app.Ajax("/user/:nick/animelist/:id", animelistitem.Get) + app.Ajax("/user/:nick/animelist/watching", animelist.FilterByStatus(arn.AnimeListStatusWatching)) + app.Ajax("/user/:nick/animelist/completed", animelist.FilterByStatus(arn.AnimeListStatusCompleted)) + app.Ajax("/user/:nick/animelist/planned", animelist.FilterByStatus(arn.AnimeListStatusPlanned)) + app.Ajax("/user/:nick/animelist/hold", animelist.FilterByStatus(arn.AnimeListStatusHold)) + app.Ajax("/user/:nick/animelist/dropped", animelist.FilterByStatus(arn.AnimeListStatusDropped)) + app.Ajax("/user/:nick/animelist/anime/:id", animelistitem.Get) app.Ajax("/new/thread", newthread.Get) app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 854807bd..72f14599 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -27,7 +27,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis span Edit anime if user.AnimeList().Contains(anime.ID) - a.button.ajax(href="/+" + user.Nick + "/animelist/" + anime.ID) + a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) Icon("pencil") span Edit in collection else diff --git a/pages/animelist/status.go b/pages/animelist/status.go new file mode 100644 index 00000000..0db10e03 --- /dev/null +++ b/pages/animelist/status.go @@ -0,0 +1,46 @@ +package animelist + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// FilterByStatus returns a handler for the given anime list item status. +func FilterByStatus(status string) aero.Handle { + return func(ctx *aero.Context) string { + user := utils.GetUser(ctx) + list, response := statusList(ctx, status) + + if response != "" { + return response + } + + return ctx.HTML(components.AnimeListFilteredByStatus(list, list.User(), user)) + } +} + +// statusList handles the request for an anime list with a given status. +func statusList(ctx *aero.Context, status string) (*arn.AnimeList, string) { + nick := ctx.Get("nick") + viewUser, err := arn.GetUserByNick(nick) + + if err != nil { + return nil, ctx.Error(http.StatusNotFound, "User not found", err) + } + + animeList := viewUser.AnimeList() + + if animeList == nil { + return nil, ctx.Error(http.StatusNotFound, "Anime list not found", nil) + } + + watchingList := animeList.FilterStatus(status) + watchingList.PrefetchAnime() + watchingList.Sort() + + return watchingList, "" +} diff --git a/pages/animelist/watching.pixy b/pages/animelist/watching.pixy new file mode 100644 index 00000000..6073b83f --- /dev/null +++ b/pages/animelist/watching.pixy @@ -0,0 +1,8 @@ +component AnimeListFilteredByStatus(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User) + ProfileHeader(viewUser, user) + + if len(animeList.Items) == 0 + p.no-data.mountable= viewUser.Nick + " hasn't added any anime to this list yet." + else + .anime-list-container + AnimeList(animeList, viewUser, user) \ No newline at end of file diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 483c923b..8385c658 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -67,6 +67,30 @@ component ProfileNavigation(viewUser *arn.User) a.button.tab.action(href="/+" + viewUser.Nick + "/tracks", data-action="diff", data-trigger="click") Icon("music") span.tab-text Tracks + + StatusTabs("/+" + viewUser.Nick + "/animelist") + +component StatusTabs(urlPrefix string) + .buttons.tabs + a.button.tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") + Icon("play") + span.tab-text Watching + + a.button.tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") + Icon("check") + span.tab-text Completed + + a.button.tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") + Icon("forward") + span.tab-text Planned + + a.button.tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") + Icon("pause") + span.tab-text On Hold + + a.button.tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") + Icon("stop") + span.tab-text Dropped component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack) ProfileHeader(viewUser, user) From 4a76c1280399ca6c8802063e6394b30a9af137a3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 21:06:38 +0200 Subject: [PATCH 139/527] Added mutation queues --- scripts/AnimeNotifier.ts | 23 ++++++++++++++++------- scripts/Diff.ts | 22 +++++++++++++++------- scripts/MutationQueue.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 scripts/MutationQueue.ts diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 32b374e5..fdaf4364 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -2,17 +2,26 @@ import { Application } from "./Application" import { Diff } from "./Diff" import { displayLocalDate } from "./DateView" import { findAll, delay } from "./Utils" +import { MutationQueue } from "./MutationQueue" import * as actions from "./Actions" export class AnimeNotifier { app: Application - visibilityObserver: IntersectionObserver user: HTMLElement + visibilityObserver: IntersectionObserver + + imageFound: MutationQueue + imageNotFound: MutationQueue + unmount: MutationQueue constructor(app: Application) { this.app = app this.user = null + this.imageFound = new MutationQueue(elem => elem.classList.add("image-found")) + this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found")) + this.unmount = new MutationQueue(elem => elem.classList.remove("mounted")) + if("IntersectionObserver" in window) { // Enable lazy load this.visibilityObserver = new IntersectionObserver( @@ -185,15 +194,15 @@ export class AnimeNotifier { img.src = img.dataset.src if(img.naturalWidth === 0) { - img.onload = function() { - this.classList.add("image-found") + img.onload = () => { + this.imageFound.queue(img) } - img.onerror = function() { - this.classList.add("image-not-found") + img.onerror = () => { + this.imageNotFound.queue(img) } } else { - img.classList.add("image-found") + this.imageFound.queue(img) } } @@ -210,7 +219,7 @@ export class AnimeNotifier { continue } - element.classList.remove("mounted") + this.unmount.queue(element) } } diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 342e8186..2326bf50 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -1,4 +1,18 @@ export class Diff { + // Reuse container for diffs to avoid memory allocation + static container: HTMLElement + + // innerHTML will diff the element with the given HTML string and apply DOM mutations. + static innerHTML(aRoot: HTMLElement, html: string) { + if(!Diff.container) { + Diff.container = document.createElement("main") + } + + Diff.container.innerHTML = html + Diff.childNodes(aRoot, Diff.container) + } + + // childNodes diffs the child nodes of 2 given elements and applies DOM mutations. static childNodes(aRoot: Node, bRoot: Node) { let aChild = [...aRoot.childNodes] let bChild = [...bRoot.childNodes] @@ -33,6 +47,7 @@ export class Diff { let elemA = a as HTMLElement let elemB = b as HTMLElement + // Skip iframes if(elemA.tagName === "IFRAME") { continue } @@ -75,11 +90,4 @@ export class Diff { Diff.childNodes(a, b) } } - - static innerHTML(aRoot: HTMLElement, html: string) { - let bRoot = document.createElement("main") - bRoot.innerHTML = html - - Diff.childNodes(aRoot, bRoot) - } } \ No newline at end of file diff --git a/scripts/MutationQueue.ts b/scripts/MutationQueue.ts new file mode 100644 index 00000000..b353ab59 --- /dev/null +++ b/scripts/MutationQueue.ts @@ -0,0 +1,29 @@ +export class MutationQueue { + elements: Array + mutation: (elem: HTMLElement) => void + + constructor(mutation: (elem: HTMLElement) => void) { + this.mutation = mutation + this.elements = [] + } + + queue(elem: HTMLElement) { + this.elements.push(elem) + + if(this.elements.length === 1) { + window.requestAnimationFrame(() => this.mutateAll()) + } + } + + mutateAll() { + for(let i = 0; i < this.elements.length; i++) { + this.mutation(this.elements[i]) + } + + this.clear() + } + + clear() { + this.elements.length = 0 + } +} \ No newline at end of file From ca1c5838bf427ae267e98b6895812d71936feba1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 21:12:33 +0200 Subject: [PATCH 140/527] Smooth scrolling --- scripts/Actions.ts | 56 +++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index dcb31d1f..11d5eb77 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -83,38 +83,38 @@ export function diff(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") arn.diff(url).then(() => { - arn.requestIdleCallback(() => { - const duration = 300.0 - const steps = 60 - const interval = duration / steps - const fullSin = Math.PI / 2 - const contentPadding = 24 - - let target = element - let scrollHandle: number - let oldScroll = arn.app.content.parentElement.scrollTop - let newScroll = Math.max(target.offsetTop - contentPadding, 0) - let scrollDistance = newScroll - oldScroll - let timeStart = Date.now() - let timeEnd = timeStart + duration + const duration = 300.0 + const steps = 60 + const interval = duration / steps + const fullSin = Math.PI / 2 + const contentPadding = 24 + + let target = element + let scrollHandle: number + let oldScroll = arn.app.content.parentElement.scrollTop + let newScroll = 0 + let finalScroll = Math.max(target.offsetTop - contentPadding, 0) + let scrollDistance = finalScroll - oldScroll + let timeStart = Date.now() + let timeEnd = timeStart + duration - let scroll = () => { - let time = Date.now() - let progress = (time - timeStart) / duration + let scroll = () => { + let time = Date.now() + let progress = (time - timeStart) / duration - if(progress > 1.0) { - progress = 1.0 - } - - arn.app.content.parentElement.scrollTop = oldScroll + scrollDistance * Math.sin(progress * fullSin) - - if(time >= timeEnd || arn.app.content.parentElement.scrollTop == newScroll) { - clearInterval(scrollHandle) - } + if(progress > 1.0) { + progress = 1.0 } - scrollHandle = setInterval(scroll, interval) - }) + newScroll = oldScroll + scrollDistance * Math.sin(progress * fullSin) + arn.app.content.parentElement.scrollTop = newScroll + + if(time < timeEnd && newScroll != finalScroll) { + window.requestAnimationFrame(scroll) + } + } + + window.requestAnimationFrame(scroll) }) } From 50c0e543d5ecd155255cee6f1ac7b88dd1171d81 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 21:23:59 +0200 Subject: [PATCH 141/527] Cleanup --- pages/animelistitem/animelistitem.pixy | 2 +- scripts/Actions.ts | 2 +- tests.go | 22 +++++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 4a293b93..adc57cfd 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -25,7 +25,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. InputTextArea("Notes", item.Notes, "Notes", "Your notes") .buttons.mountable - a.ajax.button(href="/+" + viewUser.Nick + "/animelist") + a.ajax.button(href="/+" + viewUser.Nick + "/animelist/watching") Icon("list") span View collection a.ajax.button(href=anime.Link()) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 11d5eb77..875765a8 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -257,7 +257,7 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen throw body } - return arn.app.load("/+" + userNick + "/animelist") + return arn.app.load("/+" + userNick + "/animelist/watching") }) .catch(console.error) .then(() => arn.loading(false)) diff --git a/tests.go b/tests.go index 23b7cac9..086b95e8 100644 --- a/tests.go +++ b/tests.go @@ -30,10 +30,30 @@ var routeTests = map[string][]string{ "/+Akyoto/animelist", }, - "/user/:nick/animelist/:id": []string{ + "/user/:nick/animelist/anime/:id": []string{ "/+Akyoto/animelist/7929", }, + "/user/:nick/animelist/watching": []string{ + "/+Akyoto/animelist/watching", + }, + + "/user/:nick/animelist/completed": []string{ + "/+Akyoto/animelist/completed", + }, + + "/user/:nick/animelist/planned": []string{ + "/+Akyoto/animelist/planned", + }, + + "/user/:nick/animelist/hold": []string{ + "/+Akyoto/animelist/hold", + }, + + "/user/:nick/animelist/dropped": []string{ + "/+Akyoto/animelist/dropped", + }, + // Pages "/anime/:id": []string{ "/anime/1", From 36fa0c8f128de1fe867d98eb966eb212542a705a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 21:37:38 +0200 Subject: [PATCH 142/527] Temporary update --- pages/profile/profile.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 8385c658..9e245cb2 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -68,7 +68,7 @@ component ProfileNavigation(viewUser *arn.User) Icon("music") span.tab-text Tracks - StatusTabs("/+" + viewUser.Nick + "/animelist") + //- StatusTabs("/+" + viewUser.Nick + "/animelist") component StatusTabs(urlPrefix string) .buttons.tabs From 7b8cc06419918189d6bb6d89d3420fbab2a1bcfc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 23:25:39 +0200 Subject: [PATCH 143/527] Fixed sync-anime --- jobs/sync-anime/sync-anime.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jobs/sync-anime/sync-anime.go b/jobs/sync-anime/sync-anime.go index bfc72884..6fc26f87 100644 --- a/jobs/sync-anime/sync-anime.go +++ b/jobs/sync-anime/sync-anime.go @@ -29,7 +29,10 @@ func sync(data *kitsu.Anime) { if err != nil { if strings.Contains(err.Error(), "not found") { - anime = &arn.Anime{} + anime = &arn.Anime{ + Title: &arn.AnimeTitle{}, + Image: &arn.AnimeImageTypes{}, + } } else { panic(err) } From 0a24af41b1dc93c095b0258f96e2cf64829e0732 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 5 Jul 2017 23:48:29 +0200 Subject: [PATCH 144/527] Dark Flame Master --- rewrite.go | 7 +++++++ scripts/AnimeNotifier.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rewrite.go b/rewrite.go index 8d66ef8a..c2ed4819 100644 --- a/rewrite.go +++ b/rewrite.go @@ -32,12 +32,19 @@ func init() { searchTerm := requestURI[len("/search/"):] ctx.Request.URL.RawQuery = "q=" + searchTerm ctx.SetURI("/search") + return } if strings.HasPrefix(requestURI, "/_/search/") { searchTerm := requestURI[len("/_/search/"):] ctx.Request.URL.RawQuery = "q=" + searchTerm ctx.SetURI("/_/search") + return + } + + if requestURI == "/dark-flame-master" { + ctx.SetURI("/api/analytics/new") + return } }) } diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index fdaf4364..f76afcdd 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -120,7 +120,7 @@ export class AnimeNotifier { } } - fetch("/api/analytics/new", { + fetch("/dark-flame-master", { method: "POST", credentials: "same-origin", body: JSON.stringify(analytics) From dc181ce3ba0596fae8ce8485405b7594829cd48d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 00:50:12 +0200 Subject: [PATCH 145/527] Added license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0a9295b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Eduard Urbach + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 60c2ba3069d3deadcfd0f1bdf27279b498a2f2f2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 00:53:45 +0200 Subject: [PATCH 146/527] Added code of conduct --- CODE_OF_CONDUCT.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..c1bd8cb3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at e.urbach@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ From f6f159fc416351c75b7a21516a12ef3ff07f1271 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 00:57:27 +0200 Subject: [PATCH 147/527] Added information for contributors --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..98454386 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing + +Please get in contact with the team on the [Anime Notifier Discord](https://discord.gg/0kimAmMCeXGXuzNF). +We're willing to help with installations and how to get started with contributions. +There are no stupid questions so feel free to ask anything if you encounter any troubles. From ae27c50c1f2814318c93f1217406e57d12ef4ffd Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 01:01:04 +0200 Subject: [PATCH 148/527] Updated makefile --- makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/makefile b/makefile index b1e3d012..d536e2c5 100644 --- a/makefile +++ b/makefile @@ -6,6 +6,7 @@ GOINSTALL=$(GOCMD) install GOTEST=$(GOCMD) test BUILDJOBS=@./jobs/build.sh BUILDPATCHES=@./patches/build.sh +TSCMD=@tsc IPTABLES=@sudo iptables server: @@ -14,6 +15,8 @@ jobs: $(BUILDJOBS) patches: $(BUILDPATCHES) +js: + $(TSCMD) install: $(GOINSTALL) test: @@ -24,6 +27,7 @@ versions: @go version @asd --version assets: + $(TSCMD) @pack depslist: $(GOCMD) list -f {{.Deps}} | sed -e 's/\[//g' -e 's/\]//g' | tr " " "\n" From d31b812b6e02eb963fb9fe935b18426c4bad9948 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 01:51:42 +0200 Subject: [PATCH 149/527] Updated makefile --- README.md | 8 +------- makefile | 6 ++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e9c91f65..091f8fd9 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,9 @@ notify.moe is powered by the [Aero framework](https://github.com/aerogo/aero) fr * `go get github.com/animenotifier/notify.moe` -### Install pack & run - -* `go get github.com/aerogo/pack` -* `go get github.com/aerogo/run` -* `go install github.com/aerogo/pack` -* `go install github.com/aerogo/run` - ### Build all +* Run `make tools` to install [pack](https://github.com/aerogo/pack) & [run](https://github.com/aerogo/run) * Run `make all` * Run `make ports` to set up local port forwarding *(80 to 4000, 443 to 4001)* * You should be able to start the server by executing `run` now diff --git a/makefile b/makefile index d536e2c5..d8ca17b9 100644 --- a/makefile +++ b/makefile @@ -23,6 +23,12 @@ test: $(GOTEST) bench: $(GOTEST) -bench . +tools: + go get -u golang.org/x/tools/cmd/goimports + go get -u github.com/aerogo/pack + go get -u github.com/aerogo/run + go install github.com/aerogo/pack + go install github.com/aerogo/run versions: @go version @asd --version From e183360cb84f99597c30741fce6503b108564ec8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 03:24:56 +0200 Subject: [PATCH 150/527] Few minor updates --- pages/animelist/animelist.go | 2 +- pages/animelist/animelist.pixy | 8 +++--- pages/animelist/animelist.scarlet | 5 +++- pages/animelist/status.go | 2 +- .../animelist/{watching.pixy => status.pixy} | 7 +++--- pages/profile/posts.go | 2 +- pages/profile/posts.pixy | 4 +-- pages/profile/profile.go | 9 ++++++- pages/profile/profile.pixy | 25 ++++++++++--------- pages/profile/threads.go | 2 +- pages/profile/threads.pixy | 4 +-- pages/profile/tracks.go | 2 +- pages/profile/tracks.pixy | 4 +-- pages/profile/watching.scarlet | 9 +++++++ scripts/Actions.ts | 2 +- scripts/AnimeNotifier.ts | 10 ++++++-- styles/base.scarlet | 6 ----- styles/include/config.scarlet | 3 ++- styles/mountable.scarlet | 2 -- styles/typography.scarlet | 12 +++++++++ 20 files changed, 76 insertions(+), 44 deletions(-) rename pages/animelist/{watching.pixy => status.pixy} (54%) diff --git a/pages/animelist/animelist.go b/pages/animelist/animelist.go index 3f9df413..c5daf656 100644 --- a/pages/animelist/animelist.go +++ b/pages/animelist/animelist.go @@ -28,5 +28,5 @@ func Get(ctx *aero.Context) string { animeList.PrefetchAnime() animeList.Sort() - return ctx.HTML(components.AnimeLists(animeList.SplitByStatus(), animeList.User(), user)) + return ctx.HTML(components.AnimeLists(animeList.SplitByStatus(), animeList.User(), user, ctx.URI())) } diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 23b04269..7e33b3e2 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -1,5 +1,5 @@ -component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User) - ProfileHeader(viewUser, user) +component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User, uri string) + ProfileHeader(viewUser, user, uri) h2.page-title.anime-list-owner= viewUser.Nick + "'s collection" @@ -49,8 +49,8 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User if user != nil th.anime-list-item-actions Actions tbody - for i, item := range animeList.Items - tr(class=utils.ItemCSSClass(animeList, i), title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) + each item in animeList.Items + tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical td.anime-list-item-airing-date diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 5222ae79..e47f72aa 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -71,4 +71,7 @@ < 1100px .anime-list-item-rating - display none \ No newline at end of file + display none + +.fill-screen + min-height calc(100vh - nav-height - content-padding * 2 - 1rem - 43px - 23px) \ No newline at end of file diff --git a/pages/animelist/status.go b/pages/animelist/status.go index 0db10e03..cf66fd4a 100644 --- a/pages/animelist/status.go +++ b/pages/animelist/status.go @@ -19,7 +19,7 @@ func FilterByStatus(status string) aero.Handle { return response } - return ctx.HTML(components.AnimeListFilteredByStatus(list, list.User(), user)) + return ctx.HTML(components.AnimeListFilteredByStatus(list, list.User(), user, status, ctx.URI())) } } diff --git a/pages/animelist/watching.pixy b/pages/animelist/status.pixy similarity index 54% rename from pages/animelist/watching.pixy rename to pages/animelist/status.pixy index 6073b83f..4b30fa3b 100644 --- a/pages/animelist/watching.pixy +++ b/pages/animelist/status.pixy @@ -1,8 +1,9 @@ -component AnimeListFilteredByStatus(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User) - ProfileHeader(viewUser, user) +component AnimeListFilteredByStatus(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User, status string, uri string) + ProfileHeader(viewUser, user, uri) if len(animeList.Items) == 0 p.no-data.mountable= viewUser.Nick + " hasn't added any anime to this list yet." else - .anime-list-container + .anime-list-container.fill-screen + h3.status-name= arn.ListItemStatusName(status) AnimeList(animeList, viewUser, user) \ No newline at end of file diff --git a/pages/profile/posts.go b/pages/profile/posts.go index d2668f45..c4fa922c 100644 --- a/pages/profile/posts.go +++ b/pages/profile/posts.go @@ -35,6 +35,6 @@ func GetPostsByUser(ctx *aero.Context) string { postables[i] = arn.ToPostable(post) } - return ctx.HTML(components.LatestPosts(postables, viewUser, utils.GetUser(ctx))) + return ctx.HTML(components.LatestPosts(postables, viewUser, utils.GetUser(ctx), ctx.URI())) } diff --git a/pages/profile/posts.pixy b/pages/profile/posts.pixy index e6a4e9de..6db5da81 100644 --- a/pages/profile/posts.pixy +++ b/pages/profile/posts.pixy @@ -1,5 +1,5 @@ -component LatestPosts(postables []arn.Postable, viewUser *arn.User, user *arn.User) - ProfileHeader(viewUser, user) +component LatestPosts(postables []arn.Postable, viewUser *arn.User, user *arn.User, uri string) + ProfileHeader(viewUser, user, uri) if len(postables) > 0 h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick diff --git a/pages/profile/profile.go b/pages/profile/profile.go index a06deee9..f809b69b 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -1,6 +1,8 @@ package profile import ( + "sort" + "github.com/aerogo/aero" "github.com/aerogo/flow" "github.com/animenotifier/arn" @@ -36,7 +38,12 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { }, func() { animeList = viewUser.AnimeList() animeList.PrefetchAnime() + + // Sort by rating + sort.Slice(animeList.Items, func(i, j int) bool { + return animeList.Items[i].Rating.Overall > animeList.Items[j].Rating.Overall + }) }) - return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts, tracks)) + return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts, tracks, ctx.URI())) } diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 9e245cb2..0cfce420 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -1,4 +1,4 @@ -component ProfileHeader(viewUser *arn.User, user *arn.User) +component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) .profile img.profile-cover(src=viewUser.CoverImageURL(), alt="Cover image") @@ -44,9 +44,9 @@ component ProfileHeader(viewUser *arn.User, user *arn.User) Icon("rocket") span= arn.Capitalize(viewUser.Role) - ProfileNavigation(viewUser) + ProfileNavigation(viewUser, uri) -component ProfileNavigation(viewUser *arn.User) +component ProfileNavigation(viewUser *arn.User, uri string) .buttons.tabs a.button.tab.action(href="/+" + viewUser.Nick, data-action="diff", data-trigger="click") Icon("th") @@ -68,32 +68,33 @@ component ProfileNavigation(viewUser *arn.User) Icon("music") span.tab-text Tracks - //- StatusTabs("/+" + viewUser.Nick + "/animelist") + //- if strings.Contains(uri, "/animelist") + //- StatusTabs("/+" + viewUser.Nick + "/animelist") component StatusTabs(urlPrefix string) - .buttons.tabs - a.button.tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") + .buttons.tabs.status-tabs + a.button.status-tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") Icon("play") span.tab-text Watching - a.button.tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") + a.button.status-tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") Icon("check") span.tab-text Completed - a.button.tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") + a.button.status-tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") Icon("forward") span.tab-text Planned - a.button.tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") + a.button.status-tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") Icon("pause") span.tab-text On Hold - a.button.tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") + a.button.status-tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") Icon("stop") span.tab-text Dropped -component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack) - ProfileHeader(viewUser, user) +component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack, uri string) + ProfileHeader(viewUser, user, uri) if len(animeList.Items) == 0 p.no-data.mountable= viewUser.Nick + " hasn't added any anime yet." diff --git a/pages/profile/threads.go b/pages/profile/threads.go index b886c6bd..ef7fc169 100644 --- a/pages/profile/threads.go +++ b/pages/profile/threads.go @@ -27,5 +27,5 @@ func GetThreadsByUser(ctx *aero.Context) string { threads = threads[:maxThreads] } - return ctx.HTML(components.ProfileThreads(threads, viewUser, utils.GetUser(ctx))) + return ctx.HTML(components.ProfileThreads(threads, viewUser, utils.GetUser(ctx), ctx.URI())) } diff --git a/pages/profile/threads.pixy b/pages/profile/threads.pixy index daad69f8..2899cd14 100644 --- a/pages/profile/threads.pixy +++ b/pages/profile/threads.pixy @@ -1,5 +1,5 @@ -component ProfileThreads(threads []*arn.Thread, viewUser *arn.User, user *arn.User) - ProfileHeader(viewUser, user) +component ProfileThreads(threads []*arn.Thread, viewUser *arn.User, user *arn.User, uri string) + ProfileHeader(viewUser, user, uri) if len(threads) == 0 p.no-data.mountable= viewUser.Nick + " hasn't written any threads yet." diff --git a/pages/profile/tracks.go b/pages/profile/tracks.go index abf825a1..2b723403 100644 --- a/pages/profile/tracks.go +++ b/pages/profile/tracks.go @@ -27,6 +27,6 @@ func GetSoundTracksByUser(ctx *aero.Context) string { arn.SortSoundTracksLatestFirst(tracks) - return ctx.HTML(components.TrackList(tracks, viewUser, user)) + return ctx.HTML(components.TrackList(tracks, viewUser, user, ctx.URI())) } diff --git a/pages/profile/tracks.pixy b/pages/profile/tracks.pixy index 3adc47ee..ae49399b 100644 --- a/pages/profile/tracks.pixy +++ b/pages/profile/tracks.pixy @@ -1,5 +1,5 @@ -component TrackList(tracks []*arn.SoundTrack, viewUser *arn.User, user *arn.User) - ProfileHeader(viewUser, user) +component TrackList(tracks []*arn.SoundTrack, viewUser *arn.User, user *arn.User, uri string) + ProfileHeader(viewUser, user, uri) h2.page-title= "Tracks added by " + viewUser.Nick diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 8c96643a..009c9d0e 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -10,6 +10,15 @@ height 78px !important border-radius 2px +// .status-tabs +// position fixed +// top 4.6rem +// right 1.6rem +// flex-direction column + +// .status-tab +// font-size 0.9rem + // < 380px // .profile-watching-list // justify-content center \ No newline at end of file diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 875765a8..b5aafabc 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -83,7 +83,7 @@ export function diff(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") arn.diff(url).then(() => { - const duration = 300.0 + const duration = 250.0 const steps = 60 const interval = duration / steps const fullSin = Math.PI / 2 diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index f76afcdd..57fe38ea 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -225,9 +225,11 @@ export class AnimeNotifier { modifyDelayed(className: string, func: (element: HTMLElement) => void) { const delay = 20 + const maxDelay = 1000 let time = 0 let start = Date.now() + let maxTime = start + maxDelay let collection = document.getElementsByClassName(className) let mutations = [] @@ -245,6 +247,10 @@ export class AnimeNotifier { time = mountableTypes[type] = start } + if(time > maxTime) { + time = maxTime + } + mutations.push({ element, time @@ -276,7 +282,7 @@ export class AnimeNotifier { diff(url: string) { if(url == this.app.currentPath) { - return + return Promise.reject(null) } let request = fetch("/_" + url, { @@ -292,7 +298,7 @@ export class AnimeNotifier { // Delay by transition-speed return delay(300).then(() => { - request + return request .then(html => this.app.setContent(html, true)) .then(() => this.app.markActiveLinks()) .then(() => this.app.emit("DOMContentLoaded")) diff --git a/styles/base.scarlet b/styles/base.scarlet index 2f16d1a9..3421b8bd 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -29,12 +29,6 @@ a // &.active // color link-active-color -strong - font-weight bold - -em - font-style italic - img backface-visibility hidden diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 14decf4c..9621f35a 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -64,4 +64,5 @@ nav-height = 3.11rem // Timings fade-speed = 200ms -transition-speed = 250ms \ No newline at end of file +transition-speed = 250ms +mountable-transition-speed = 400ms diff --git a/styles/mountable.scarlet b/styles/mountable.scarlet index 6fb00d36..78becc67 100644 --- a/styles/mountable.scarlet +++ b/styles/mountable.scarlet @@ -1,5 +1,3 @@ -mountable-transition-speed = 400ms - .mountable opacity 0 transform translateY(0.85rem) diff --git a/styles/typography.scarlet b/styles/typography.scarlet index 81c3b08e..19ee435a 100644 --- a/styles/typography.scarlet +++ b/styles/typography.scarlet @@ -11,6 +11,18 @@ h2 margin-top content-padding margin-bottom content-padding +strong + font-weight bold + +em + font-style italic + +hr + border none + border-bottom 1px solid text-color + opacity 0.1 + margin-bottom content-padding + p > img max-width 100% border-radius 3px From 298fe45e72cbd475e71f3b73859162b70e426ba6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 15:04:51 +0200 Subject: [PATCH 151/527] Minor changes --- patches/post-texts/post-texts.go | 3 ++- styles/forum.scarlet | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/patches/post-texts/post-texts.go b/patches/post-texts/post-texts.go index c277a66e..d448616d 100644 --- a/patches/post-texts/post-texts.go +++ b/patches/post-texts/post-texts.go @@ -2,6 +2,7 @@ package main import ( "github.com/animenotifier/arn" + "github.com/animenotifier/arn/autocorrect" "github.com/fatih/color" ) @@ -14,7 +15,7 @@ func main() { for post := range allPosts { // Fix text color.Yellow(post.Text) - post.Text = arn.FixPostText(post.Text) + post.Text = autocorrect.FixPostText(post.Text) color.Green(post.Text) // Tags diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 9066a550..297a325e 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -48,6 +48,8 @@ .thread-icons, .thread-reply-count + horizontal + align-items center opacity 0.5 text-align right font-size 0.9rem From 9de86a83f9cc72e3ce07ab61b02ddebdbe87d764 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 15:15:21 +0200 Subject: [PATCH 152/527] Using aerospike session store package now --- main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index dfc2eb8c..b91e38a9 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "github.com/aerogo/aero" "github.com/aerogo/api" + "github.com/aerogo/session-store-aerospike" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/auth" "github.com/animenotifier/notify.moe/components/css" @@ -53,7 +54,7 @@ func configure(app *aero.Application) *aero.Application { // Sessions app.Sessions.Duration = 3600 * 24 * 7 - app.Sessions.Store = arn.NewAerospikeStore("Session", app.Sessions.Duration) + app.Sessions.Store = aerospikestore.New(arn.DB, "Session", app.Sessions.Duration) // Layout app.Layout = layout.Render From 5070600964bf71b1f6c930a8b48ae921d74c6fe5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 16:54:10 +0200 Subject: [PATCH 153/527] Added forum post editing --- mixins/Postable.pixy | 42 ++++++++++-------- mixins/PostableList.pixy | 4 +- pages/posts/posts.go | 4 +- pages/posts/posts.pixy | 4 +- pages/profile/posts.pixy | 2 +- pages/threads/threads.pixy | 4 +- rewrite.go | 2 +- scripts/Actions.ts | 91 ++++++++++++++++++-------------------- scripts/AnimeNotifier.ts | 52 ++++++++++++++++++++++ styles/base.scarlet | 3 ++ styles/forum.scarlet | 6 +++ styles/input.scarlet | 1 - tests.go | 2 + 13 files changed, 139 insertions(+), 78 deletions(-) diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index baf09b73..4ed21abe 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -1,5 +1,5 @@ -component Postable(post arn.Postable, highlightAuthorID string) - .post.mountable(id=post.ID(), data-highlight=post.Author().ID == highlightAuthorID) +component Postable(post arn.Postable, user *arn.User, highlightAuthorID string) + .post.mountable(id=strings.ToLower(post.Type()) + "-" + toString(post.ID()), data-highlight=post.Author().ID == highlightAuthorID, data-api="/api/" + strings.ToLower(post.Type()) + "/" + post.ID()) .post-author Avatar(post.Author()) @@ -9,34 +9,38 @@ component Postable(post arn.Postable, highlightAuthorID string) .post-content div(id="render-" + post.ID())!= post.HTML() - //- if user && user.ID === post.authorId - //- textarea.post-input.hidden(id="source-" + post.ID)= post.text - //- a.post-save.hidden(id="save-" + post.ID, onclick=`$.saveEdit("${type.toLowerCase()}", "${post.ID}")`) - //- i.fa.fa-save - //- span Save + if user != nil && user.ID == post.Author().ID + textarea.post-input.hidden(id="source-" + post.ID())= post.Text() + .buttons.hidden(id="edit-toolbar-" + post.ID()) + a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.ID()) + Icon("save") + span Save + + a.button.post-cancel-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID()) + Icon("close") + span Cancel .post-toolbar(id="toolbar-" + post.ID()) .spacer .post-likes(id="likes-" + post.ID(), title="Likes")= len(post.Likes()) - //- if user != nil - //- if user.ID !== post.authorId - //- - var liked = post.likes && post.likes.indexOf(user.ID) !== -1 + if user != nil + //- if user.ID !== post.authorId + //- - var liked = post.likes && post.likes.indexOf(user.ID) !== -1 - //- a.post-tool.post-like(id="like-" + post.ID, onclick=`$.like("${type.toLowerCase()}", "${post.ID}")`, title="Like", class=liked ? "hidden" : ") - //- i.fa.fa-thumbs-up.fa-fw + //- a.post-tool.post-like(id="like-" + post.ID, onclick=`$.like("${type.toLowerCase()}", "${post.ID}")`, title="Like", class=liked ? "hidden" : ") + //- i.fa.fa-thumbs-up.fa-fw - //- a.post-tool.post-unlike(id="unlike-" + post.ID, onclick=`$.unlike("${type.toLowerCase()}", "${post.ID}")`, title="Unlike", class=!liked ? "hidden" : ") - //- i.fa.fa-thumbs-down.fa-fw + //- a.post-tool.post-unlike(id="unlike-" + post.ID, onclick=`$.unlike("${type.toLowerCase()}", "${post.ID}")`, title="Unlike", class=!liked ? "hidden" : ") + //- i.fa.fa-thumbs-down.fa-fw - //- if type === "Posts" || type === "Threads" - //- if user.ID === post.authorId - //- a.post-tool.post-edit(onclick=`$.edit("${post.ID}")`, title="Edit") - //- i.fa.fa-pencil.fa-fw + if user.ID == post.Author().ID + a.post-tool.post-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID(), title="Edit") + RawIcon("pencil") if post.Type() != "Thread" a.post-tool.post-permalink.ajax(href=post.Link(), title="Permalink") - Icon("link") + RawIcon("link") //- if type === "Messages" && user && (user.ID === post.authorId || user.ID === post.recipientId) //- a.post-tool.post-delete(onclick=`if(confirm("Do you really want to delete this ${typeSingular.toLowerCase()} from ${post.author.nick}?")) $.delete${typeSingular}("${post.ID}")`, title="Delete") diff --git a/mixins/PostableList.pixy b/mixins/PostableList.pixy index c2f956a9..bd31d475 100644 --- a/mixins/PostableList.pixy +++ b/mixins/PostableList.pixy @@ -1,5 +1,5 @@ -component PostableList(postables []arn.Postable) +component PostableList(postables []arn.Postable, user *arn.User) .thread .posts each post in postables - Postable(post, "") + Postable(post, user, "") diff --git a/pages/posts/posts.go b/pages/posts/posts.go index 5e705172..b6bf6aca 100644 --- a/pages/posts/posts.go +++ b/pages/posts/posts.go @@ -6,16 +6,18 @@ import ( "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" ) // Get post. func Get(ctx *aero.Context) string { id := ctx.Get("id") + user := utils.GetUser(ctx) post, err := arn.GetPost(id) if err != nil { return ctx.Error(http.StatusNotFound, "Post not found", err) } - return ctx.HTML(components.Post(post)) + return ctx.HTML(components.Post(post, user)) } diff --git a/pages/posts/posts.pixy b/pages/posts/posts.pixy index 6d02e8cc..2de3e8b1 100644 --- a/pages/posts/posts.pixy +++ b/pages/posts/posts.pixy @@ -1,5 +1,5 @@ -component Post(post *arn.Post) - Postable(post.ToPostable(), "") +component Post(post *arn.Post, user *arn.User) + Postable(post.ToPostable(), user, "") .side-note a.ajax(href=post.Thread().Link())= post.Thread().Title diff --git a/pages/profile/posts.pixy b/pages/profile/posts.pixy index 6db5da81..981c336a 100644 --- a/pages/profile/posts.pixy +++ b/pages/profile/posts.pixy @@ -3,6 +3,6 @@ component LatestPosts(postables []arn.Postable, viewUser *arn.User, user *arn.Us if len(postables) > 0 h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick - PostableList(postables) + PostableList(postables, user) else p.no-data.mountable= viewUser.Nick + " hasn't written any posts yet." \ No newline at end of file diff --git a/pages/threads/threads.pixy b/pages/threads/threads.pixy index 9a1014bb..77828501 100644 --- a/pages/threads/threads.pixy +++ b/pages/threads/threads.pixy @@ -3,10 +3,10 @@ component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User) #thread.thread(data-id=thread.ID) .posts - Postable(thread.ToPostable(), thread.Author().ID) + Postable(thread.ToPostable(), user, thread.Author().ID) each post in posts - Postable(post.ToPostable(), thread.Author().ID) + Postable(post.ToPostable(), user, thread.Author().ID) // Reply if user != nil diff --git a/rewrite.go b/rewrite.go index c2ed4819..edc0a7be 100644 --- a/rewrite.go +++ b/rewrite.go @@ -43,7 +43,7 @@ func init() { } if requestURI == "/dark-flame-master" { - ctx.SetURI("/api/analytics/new") + ctx.SetURI("/api/new/analytics") return } }) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index b5aafabc..65cb5d43 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 { findAll } from "./Utils" // Save new data from an input field export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) { @@ -20,29 +21,15 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE obj[input.dataset.field] = value } - // console.log(input.type, input.dataset.api, obj, JSON.stringify(obj)) - - let apiObject: HTMLElement - let parent = input as HTMLElement - - while(parent = parent.parentElement) { - if(parent.dataset.api !== undefined) { - apiObject = parent - break - } - } - - if(!apiObject) { - throw "API object not found" - } - if(isContentEditable) { input.contentEditable = "false" } else { input.disabled = true } - fetch(apiObject.dataset.api, { + let apiEndpoint = arn.findAPIEndpoint(input) + + fetch(apiEndpoint, { method: "POST", body: JSON.stringify(obj), credentials: "same-origin" @@ -82,40 +69,46 @@ export function soon() { export function diff(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") - arn.diff(url).then(() => { - const duration = 250.0 - const steps = 60 - const interval = duration / steps - const fullSin = Math.PI / 2 - const contentPadding = 24 - - let target = element - let scrollHandle: number - let oldScroll = arn.app.content.parentElement.scrollTop - let newScroll = 0 - let finalScroll = Math.max(target.offsetTop - contentPadding, 0) - let scrollDistance = finalScroll - oldScroll - let timeStart = Date.now() - let timeEnd = timeStart + duration + arn.diff(url).then(() => arn.scrollTo(element)) +} - let scroll = () => { - let time = Date.now() - let progress = (time - timeStart) / duration +// Edit post +export function editPost(arn: AnimeNotifier, element: HTMLElement) { + let postId = element.dataset.id - if(progress > 1.0) { - progress = 1.0 - } + let render = arn.app.find("render-" + postId) + let toolbar = arn.app.find("toolbar-" + postId) + let source = arn.app.find("source-" + postId) + let edit = arn.app.find("edit-toolbar-" + postId) - newScroll = oldScroll + scrollDistance * Math.sin(progress * fullSin) - arn.app.content.parentElement.scrollTop = newScroll + if(!render.classList.contains("hidden")) { + render.classList.add("hidden") + toolbar.classList.add("hidden") + source.classList.remove("hidden") + edit.classList.remove("hidden") + } else { + render.classList.remove("hidden") + toolbar.classList.remove("hidden") + source.classList.add("hidden") + edit.classList.add("hidden") + } +} - if(time < timeEnd && newScroll != finalScroll) { - window.requestAnimationFrame(scroll) - } - } +// Save post +export function savePost(arn: AnimeNotifier, element: HTMLElement) { + let postId = element.dataset.id + let source = arn.app.find("source-" + postId) as HTMLTextAreaElement + let text = source.value - window.requestAnimationFrame(scroll) - }) + let updates = { + Text: text, + } + + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint, updates) + .then(() => arn.reloadContent()) + .catch(console.error) } // Forum reply @@ -129,7 +122,7 @@ export function forumReply(arn: AnimeNotifier) { tags: [] } - arn.post("/api/post/new", post) + arn.post("/api/new/post", post) .then(() => arn.reloadContent()) .then(() => textarea.value = "") .catch(console.error) @@ -147,7 +140,7 @@ export function createThread(arn: AnimeNotifier) { tags: [category.value] } - arn.post("/api/thread/new", thread) + arn.post("/api/new/thread", thread) .then(() => arn.app.load("/forum/" + thread.tags[0])) .catch(console.error) } @@ -168,7 +161,7 @@ export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) button.innerText = "Adding..." button.disabled = true - arn.post("/api/soundtrack/new", soundtrack) + arn.post("/api/new/soundtrack", soundtrack) .then(() => arn.app.load("/music")) .catch(err => { console.error(err) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 57fe38ea..e293f81d 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -321,6 +321,58 @@ export class AnimeNotifier { }) } + scrollTo(target: HTMLElement) { + const duration = 250.0 + const steps = 60 + const interval = duration / steps + const fullSin = Math.PI / 2 + const contentPadding = 24 + + let scrollHandle: number + let oldScroll = this.app.content.parentElement.scrollTop + let newScroll = 0 + let finalScroll = Math.max(target.offsetTop - contentPadding, 0) + let scrollDistance = finalScroll - oldScroll + let timeStart = Date.now() + let timeEnd = timeStart + duration + + let scroll = () => { + let time = Date.now() + let progress = (time - timeStart) / duration + + if(progress > 1.0) { + progress = 1.0 + } + + newScroll = oldScroll + scrollDistance * Math.sin(progress * fullSin) + this.app.content.parentElement.scrollTop = newScroll + + if(time < timeEnd && newScroll != finalScroll) { + window.requestAnimationFrame(scroll) + } + } + + window.requestAnimationFrame(scroll) + } + + findAPIEndpoint(element: HTMLElement) { + let apiObject: HTMLElement + let parent = element + + while(parent = parent.parentElement) { + if(parent.dataset.api !== undefined) { + apiObject = parent + break + } + } + + if(!apiObject) { + throw "API object not found" + } + + return apiObject.dataset.api + } + onPopState(e: PopStateEvent) { if(e.state) { this.app.load(e.state, { diff --git a/styles/base.scarlet b/styles/base.scarlet index 3421b8bd..6892df79 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -32,5 +32,8 @@ a img backface-visibility hidden +.hidden + display none !important + .spacer flex 1 \ No newline at end of file diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 297a325e..17c2d107 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -105,6 +105,12 @@ .post-unlike color rgb(255, 32, 12) !important +.post-save + // + +.post-input + min-height 200px + // Old // #posts diff --git a/styles/input.scarlet b/styles/input.scarlet index 9b0702d8..8356881d 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -1,6 +1,5 @@ mixin input-focus :focus - color black 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) diff --git a/tests.go b/tests.go index 086b95e8..f26ca750 100644 --- a/tests.go +++ b/tests.go @@ -201,9 +201,11 @@ var interfaceImplementations = map[string][]reflect.Type{ }, "Thread": []reflect.Type{ creatable, + updatable, }, "Post": []reflect.Type{ creatable, + updatable, }, "SoundTrack": []reflect.Type{ creatable, From 598b365a33feeac7e08305c871413d8257e9afd7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 17:51:44 +0200 Subject: [PATCH 154/527] Added priority queues to episode refresh --- jobs/refresh-episodes/refresh-episodes.go | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 65ab886a..662652ee 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -12,7 +12,32 @@ import ( func main() { color.Yellow("Refreshing episode information for each anime.") + highPriority := []*arn.Anime{} + lowPriority := []*arn.Anime{} + for anime := range arn.MustStreamAnime() { + if anime.GetMapping("shoboi/anime") == "" { + continue + } + + if anime.Status == "current" || anime.Status == "upcoming" { + highPriority = append(highPriority, anime) + } else { + lowPriority = append(lowPriority, anime) + } + } + + color.Cyan("High priority queue:") + refresh(highPriority) + + color.Cyan("Low priority queue:") + refresh(lowPriority) + + color.Green("Finished.") +} + +func refresh(queue []*arn.Anime) { + for _, anime := range queue { episodeCount := len(anime.Episodes) err := anime.RefreshEpisodes() @@ -27,6 +52,4 @@ func main() { fmt.Println(anime.ID, "|", anime.Title.Canonical, "+"+strconv.Itoa(len(anime.Episodes)-episodeCount)) } } - - color.Green("Finished.") } From 4cac9f554461ae07defc23c5420bec687d4d078a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 19:33:58 +0200 Subject: [PATCH 155/527] Added thread title editing --- mixins/Postable.pixy | 19 +++++++++++-------- scripts/Actions.ts | 26 +++++++++++++++----------- styles/forum.scarlet | 9 ++++++++- styles/input.scarlet | 6 +++++- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index 4ed21abe..65da2aab 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -10,15 +10,18 @@ component Postable(post arn.Postable, user *arn.User, highlightAuthorID string) div(id="render-" + post.ID())!= post.HTML() if user != nil && user.ID == post.Author().ID - textarea.post-input.hidden(id="source-" + post.ID())= post.Text() - .buttons.hidden(id="edit-toolbar-" + post.ID()) - a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.ID()) - Icon("save") - span Save + .post-edit-interface + if post.Type() == "Thread" + input.post-title-input.hidden(id="title-" + post.ID(), value=post.Title(), type="text", placeholder="Thread title") + textarea.post-text-input.hidden(id="source-" + post.ID())= post.Text() + .buttons.hidden(id="edit-toolbar-" + post.ID()) + a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.ID()) + Icon("save") + span Save - a.button.post-cancel-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID()) - Icon("close") - span Cancel + a.button.post-cancel-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID()) + Icon("close") + span Cancel .post-toolbar(id="toolbar-" + post.ID()) .spacer diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 65cb5d43..6e437977 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -78,19 +78,17 @@ export function editPost(arn: AnimeNotifier, element: HTMLElement) { let render = arn.app.find("render-" + postId) let toolbar = arn.app.find("toolbar-" + postId) + let title = arn.app.find("title-" + postId) let source = arn.app.find("source-" + postId) let edit = arn.app.find("edit-toolbar-" + postId) - if(!render.classList.contains("hidden")) { - render.classList.add("hidden") - toolbar.classList.add("hidden") - source.classList.remove("hidden") - edit.classList.remove("hidden") - } else { - render.classList.remove("hidden") - toolbar.classList.remove("hidden") - source.classList.add("hidden") - edit.classList.add("hidden") + render.classList.toggle("hidden") + toolbar.classList.toggle("hidden") + source.classList.toggle("hidden") + edit.classList.toggle("hidden") + + if(title) { + title.classList.toggle("hidden") } } @@ -98,12 +96,18 @@ export function editPost(arn: AnimeNotifier, element: HTMLElement) { export function savePost(arn: AnimeNotifier, element: HTMLElement) { let postId = element.dataset.id let source = arn.app.find("source-" + postId) as HTMLTextAreaElement + let title = arn.app.find("title-" + postId) as HTMLInputElement let text = source.value - let updates = { + let updates: any = { Text: text, } + // Add title for threads only + if(title) { + updates.Title = title.value + } + let apiEndpoint = arn.findAPIEndpoint(element) arn.post(apiEndpoint, updates) diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 17c2d107..4107476e 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -1,6 +1,7 @@ // .forum-header // text-align left // margin-bottom 1rem +post-content-padding-y = 0.75rem .thread-link vertical @@ -108,7 +109,13 @@ .post-save // -.post-input +.post-edit-interface + vertical + +.post-title-input + margin-bottom post-content-padding-y + +.post-text-input min-height 200px // Old diff --git a/styles/input.scarlet b/styles/input.scarlet index 8356881d..e4cff2b0 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -13,10 +13,15 @@ input, textarea, button, .button, select input, textarea, select input-focus + width 100% :disabled ui-disabled +input, select + width 100% + padding 0.5rem 1rem + input :active transform translateY(3px) @@ -49,7 +54,6 @@ label textarea padding 0.4em 0.8em - width 100% line-height 1.5em height 10rem transition none \ No newline at end of file From e47f1d298f4c5883968b758b2775327cd9411a25 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 20:33:46 +0200 Subject: [PATCH 156/527] Added Open Graph data to anime --- layout/layout.go | 3 ++- layout/layout.pixy | 8 +++++++- pages/anime/anime.go | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/layout/layout.go b/layout/layout.go index b963046f..4f449a21 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -9,5 +9,6 @@ import ( // Render layout. func Render(ctx *aero.Context, content string) string { user := utils.GetUser(ctx) - return components.Layout(ctx.App, ctx, user, content) + meta, _ := ctx.Data.(map[string]string) + return components.Layout(ctx.App, ctx, user, meta, content) } diff --git a/layout/layout.pixy b/layout/layout.pixy index 7cc95122..df520f62 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -1,9 +1,15 @@ -component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, content string) +component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, meta map[string]string, content string) html(lang="en") head 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) + + if meta != nil + for property, value := range meta + meta(name=property, value=value) + link(rel="chrome-webstore-item", href="https://chrome.google.com/webstore/detail/hajchfikckiofgilinkpifobdbiajfch") link(rel="manifest", href="/manifest.json") body diff --git a/pages/anime/anime.go b/pages/anime/anime.go index c61a6de9..bc34303e 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -39,5 +39,22 @@ func Get(ctx *aero.Context) string { } } + // Open Graph + openGraph := map[string]string{ + "og:title": anime.Title.Canonical, + "og:image": anime.Image.Large, + "og:url": "https://" + ctx.App.Config.Domain + anime.Link(), + "og:site_name": "notify.moe", + } + + switch anime.Type { + case "tv": + openGraph["og:type"] = "video.tv_show" + case "movie": + openGraph["og:type"] = "video.movie" + } + + ctx.Data = openGraph + return ctx.HTML(components.Anime(anime, tracks, user, episodesReversed)) } From 0e009f434cafcd65fe8afa57f3e67c8f91ad4f0d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 20:44:47 +0200 Subject: [PATCH 157/527] Added summary to OG data --- pages/anime/anime.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index bc34303e..1c4015f2 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -40,21 +40,22 @@ func Get(ctx *aero.Context) string { } // Open Graph - openGraph := map[string]string{ + meta := map[string]string{ "og:title": anime.Title.Canonical, "og:image": anime.Image.Large, "og:url": "https://" + ctx.App.Config.Domain + anime.Link(), "og:site_name": "notify.moe", + "description": anime.Summary, } switch anime.Type { case "tv": - openGraph["og:type"] = "video.tv_show" + meta["og:type"] = "video.tv_show" case "movie": - openGraph["og:type"] = "video.movie" + meta["og:type"] = "video.movie" } - ctx.Data = openGraph + ctx.Data = meta return ctx.HTML(components.Anime(anime, tracks, user, episodesReversed)) } From 19379802540f33c530357c6ca1db0a7bda7ed21b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 20:56:37 +0200 Subject: [PATCH 158/527] Improved OG data --- layout/layout.go | 5 +++-- layout/layout.pixy | 13 ++++++++----- pages/anime/anime.go | 23 ++++++++++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/layout/layout.go b/layout/layout.go index 4f449a21..384528a8 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -2,6 +2,7 @@ package layout import ( "github.com/aerogo/aero" + "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" ) @@ -9,6 +10,6 @@ import ( // Render layout. func Render(ctx *aero.Context, content string) string { user := utils.GetUser(ctx) - meta, _ := ctx.Data.(map[string]string) - return components.Layout(ctx.App, ctx, user, meta, content) + openGraph, _ := ctx.Data.(*arn.OpenGraph) + return components.Layout(ctx.App, ctx, user, openGraph, content) } diff --git a/layout/layout.pixy b/layout/layout.pixy index df520f62..09859ab4 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -1,14 +1,17 @@ -component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, meta map[string]string, content string) +component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openGraph *arn.OpenGraph, content string) html(lang="en") head 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) - if meta != nil - for property, value := range meta - meta(name=property, value=value) + if openGraph != nil + for name, value := range openGraph.Meta + meta(name=name, content=value) + + for property, content := range openGraph.Tags + meta(property=property, content=content) link(rel="chrome-webstore-item", href="https://chrome.google.com/webstore/detail/hajchfikckiofgilinkpifobdbiajfch") link(rel="manifest", href="/manifest.json") diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 1c4015f2..6c2ea54d 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -40,22 +40,27 @@ func Get(ctx *aero.Context) string { } // Open Graph - meta := map[string]string{ - "og:title": anime.Title.Canonical, - "og:image": anime.Image.Large, - "og:url": "https://" + ctx.App.Config.Domain + anime.Link(), - "og:site_name": "notify.moe", - "description": anime.Summary, + openGraph := &arn.OpenGraph{ + Tags: map[string]string{ + "og:title": anime.Title.Canonical, + "og:image": anime.Image.Large, + "og:url": "https://" + ctx.App.Config.Domain + anime.Link(), + "og:site_name": "notify.moe", + "og:description": anime.Summary, + }, + Meta: map[string]string{ + "description": anime.Summary, + }, } switch anime.Type { case "tv": - meta["og:type"] = "video.tv_show" + openGraph.Tags["og:type"] = "video.tv_show" case "movie": - meta["og:type"] = "video.movie" + openGraph.Tags["og:type"] = "video.movie" } - ctx.Data = meta + ctx.Data = openGraph return ctx.HTML(components.Anime(anime, tracks, user, episodesReversed)) } From bfc50c5bae11ea57c7ec65ee7e976a4429f41c6c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 21:38:19 +0200 Subject: [PATCH 159/527] Improved meta data --- layout/layout.pixy | 5 ++++- pages/anime/anime.go | 12 ++++++++++-- pages/anime/anime.pixy | 4 ++-- pages/anime/anime.scarlet | 4 ++++ pages/frontpage/frontpage.go | 17 +++++++++++++++++ pages/frontpage/frontpage.pixy | 4 ++-- pages/frontpage/frontpage.scarlet | 8 +++++--- styles/content.scarlet | 2 +- styles/forum.scarlet | 5 +++++ styles/headers.scarlet | 7 ++----- styles/include/config.scarlet | 2 ++ styles/typography.scarlet | 4 ++-- 12 files changed, 56 insertions(+), 18 deletions(-) diff --git a/layout/layout.pixy b/layout/layout.pixy index 09859ab4..3ab5cb01 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -1,7 +1,10 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openGraph *arn.OpenGraph, content string) html(lang="en") head - title= app.Config.Title + if openGraph != nil + title= openGraph.Tags["og:title"] + else + 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) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 6c2ea54d..a3ad71e9 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -11,6 +11,7 @@ import ( const maxEpisodes = 26 const maxEpisodesLongSeries = 5 +const maxDescriptionLength = 170 // Get anime page. func Get(ctx *aero.Context) string { @@ -40,16 +41,23 @@ func Get(ctx *aero.Context) string { } // Open Graph + description := anime.Summary + + if len(description) > maxDescriptionLength { + description = description[:maxDescriptionLength-3] + "..." + } + openGraph := &arn.OpenGraph{ Tags: map[string]string{ "og:title": anime.Title.Canonical, "og:image": anime.Image.Large, "og:url": "https://" + ctx.App.Config.Domain + anime.Link(), "og:site_name": "notify.moe", - "og:description": anime.Summary, + "og:description": description, }, Meta: map[string]string{ - "description": anime.Summary, + "description": description, + "keywords": anime.Title.Canonical + ",anime", }, } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 72f14599..8dc41907 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -7,13 +7,13 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis .space .anime-info - h2.anime-title(title=anime.Type)= anime.Title.Canonical + h1.anime-title(title=anime.Type)= anime.Title.Canonical //- if user && user.titleLanguage === "japanese" //- span.second-title(title=anime.Title.English !== anime.Title.Romaji ? anime.Title.English : null)= anime.Title.Romaji //- else if anime.Title.Japanese != anime.Title.Canonical - .anime-alternative-title + h2.anime-alternative-title a(href="http://jisho.org/search/" + anime.Title.Japanese, target="_blank", title="Look up reading on jisho.org", rel="nofollow")= anime.Title.Japanese //- h3.anime-section-name.anime-summary-header Summary diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index b1eb03fb..c02f0ade 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -38,7 +38,11 @@ .anime-alternative-title font-size 0.9em + margin-top 0 margin-bottom 0.5rem + text-align left + font-weight normal + line-height content-line-height a color rgba(60, 60, 60, 0.5) !important diff --git a/pages/frontpage/frontpage.go b/pages/frontpage/frontpage.go index 94feb8fc..81cbf7d0 100644 --- a/pages/frontpage/frontpage.go +++ b/pages/frontpage/frontpage.go @@ -2,10 +2,27 @@ package frontpage import ( "github.com/aerogo/aero" + "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" ) // Get ... func Get(ctx *aero.Context) string { + description := "Anime list and notifier for new anime episodes. Create your own anime list and keep track of your progress as you watch." + + ctx.Data = &arn.OpenGraph{ + Tags: map[string]string{ + "og:title": ctx.App.Config.Title, + "og:description": description, + "og:type": "website", + "og:url": "https://" + ctx.App.Config.Domain, + "og:image": "https://" + ctx.App.Config.Domain + "/images/brand/600", + }, + Meta: map[string]string{ + "description": description, + "keywords": "anime,list,tracker,notifier", + }, + } + return ctx.HTML(components.FrontPage()) } diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 9def8454..7e97c625 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -1,8 +1,8 @@ component FrontPage .frontpage.mountable - h2 notify.moe + h1 notify.moe - p Your home for everything about anime. + h2 Your home for everything about anime. //- 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") diff --git a/pages/frontpage/frontpage.scarlet b/pages/frontpage/frontpage.scarlet index 5678d78c..f13e5a70 100644 --- a/pages/frontpage/frontpage.scarlet +++ b/pages/frontpage/frontpage.scarlet @@ -6,19 +6,21 @@ left 50% transform translateX(-50%) translateY(-50%) - a, h2, p + a, h1, h2 color white !important text-shadow 0px 0px 4px rgb(0, 0, 0, 0.75) - h2 + h1 font-size 2.5rem font-weight normal letter-spacing 5px text-transform uppercase line-height 1.2em - p + h2 font-size 2rem + font-weight normal + margin-top 0 margin-bottom 1em line-height 1.2em text-align center diff --git a/styles/content.scarlet b/styles/content.scarlet index 2062384e..47665a25 100644 --- a/styles/content.scarlet +++ b/styles/content.scarlet @@ -2,4 +2,4 @@ vertical padding content-padding padding-top content-padding-top - line-height 1.7em \ No newline at end of file + line-height content-line-height \ No newline at end of file diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 4107476e..c0644eba 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -26,16 +26,21 @@ post-content-padding-y = 0.75rem h1 font-size 1.5rem line-height 1.5em + text-align left + margin typography-margin 0 h2 font-size 1.3rem line-height 1.5em font-weight normal + text-align left + margin typography-margin 0 h3 font-size 1.1rem line-height 1.5em font-weight normal + text-align left :hover .post-toolbar diff --git a/styles/headers.scarlet b/styles/headers.scarlet index e16bfae9..5842d2cf 100644 --- a/styles/headers.scarlet +++ b/styles/headers.scarlet @@ -1,7 +1,4 @@ -h1 - font-size 3em - -h2 +h1, h2 font-size 2em font-weight bold text-align center @@ -13,7 +10,7 @@ h3 text-align left margin-top 0.6em -h2, h3 +h1, h2, h3 a color text-color diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 9621f35a..7ee83f3e 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -59,8 +59,10 @@ outline-shadow-heavy = 0 0 6px rgba(0, 0, 0, 0.6) // Distances content-padding = 1.6rem content-padding-top = 1.6rem +content-line-height = 1.7em hover-line-size = 3px nav-height = 3.11rem +typography-margin = 0.4rem // Timings fade-speed = 200ms diff --git a/styles/typography.scarlet b/styles/typography.scarlet index 19ee435a..044737a4 100644 --- a/styles/typography.scarlet +++ b/styles/typography.scarlet @@ -1,5 +1,5 @@ p, h1, h2, h3, h4, h5, h6 - margin 0.4rem 0 + margin typography-margin 0 :first-child margin-top 0 @@ -7,7 +7,7 @@ p, h1, h2, h3, h4, h5, h6 :last-child margin-bottom 0 -h2 +h1, h2 margin-top content-padding margin-bottom content-padding From 7efeddab55787f92e7997f92e7fb65fae075c095 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 21:44:32 +0200 Subject: [PATCH 160/527] Fixed HTML 5 problems --- pages/frontpage/frontpage.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 7e97c625..abe2a1e9 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -13,6 +13,6 @@ component FrontPage span | a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Source on GitHub - video.bg-video(autoplay="true", loop="true") + video.bg-video(autoplay="autoplay", loop="loop") source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") source(src="//cdn-e2.streamable.com/video/mp4/e5mx7.mp4?token=1500414089_8b2b3b0665984dcf4dc8d33e534bc1c8881b2da1", type="video/mp4") \ No newline at end of file From fb7d2fa24c1b447c697a9a0788dfb74285d8d1f5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 22:08:49 +0200 Subject: [PATCH 161/527] Improved page titles --- pages/animelistitem/animelistitem.pixy | 2 +- pages/editanime/editanime.pixy | 2 +- pages/forum/forum.pixy | 2 +- pages/forums/forums.pixy | 3 --- pages/listimport/listimportanilist/anilist.pixy | 2 +- pages/music/music.pixy | 2 +- pages/threads/threads.pixy | 2 +- pages/tracks/tracks.pixy | 2 +- scripts/AnimeNotifier.ts | 12 ++++++++++++ styles/forum.scarlet | 16 ++++++---------- 10 files changed, 25 insertions(+), 20 deletions(-) delete mode 100644 pages/forums/forums.pixy diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index adc57cfd..b5052d63 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -1,7 +1,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.Anime) .widgets.mountable .widget.anime-list-item-view(data-api="/api/animelist/" + viewUser.ID + "/update/" + anime.ID) - h2= anime.Title.Canonical + h1= anime.Title.Canonical InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") diff --git a/pages/editanime/editanime.pixy b/pages/editanime/editanime.pixy index b969e041..c22a8753 100644 --- a/pages/editanime/editanime.pixy +++ b/pages/editanime/editanime.pixy @@ -1,5 +1,5 @@ component EditAnime(anime *arn.Anime) - h2= anime.Title.Canonical + h1= anime.Title.Canonical .widgets .widget(data-api="/api/anime/" + anime.ID) diff --git a/pages/forum/forum.pixy b/pages/forum/forum.pixy index 1b04acc3..caffb037 100644 --- a/pages/forum/forum.pixy +++ b/pages/forum/forum.pixy @@ -1,5 +1,5 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) - h2.page-title Forum + h1.page-title Forum ForumTags .forum ThreadList(threads) diff --git a/pages/forums/forums.pixy b/pages/forums/forums.pixy deleted file mode 100644 index 2372d037..00000000 --- a/pages/forums/forums.pixy +++ /dev/null @@ -1,3 +0,0 @@ -component Forums - h2.forum-header Forum - ForumTags \ No newline at end of file diff --git a/pages/listimport/listimportanilist/anilist.pixy b/pages/listimport/listimportanilist/anilist.pixy index c8e4772f..711f7aee 100644 --- a/pages/listimport/listimportanilist/anilist.pixy +++ b/pages/listimport/listimportanilist/anilist.pixy @@ -1,5 +1,5 @@ component ImportAnilist(user *arn.User, matches []*arn.AniListMatch) - h2= "anilist.co Import (" + user.Accounts.AniList.Nick + ", " + toString(len(matches)) + " anime)" + h1= "anilist.co Import (" + user.Accounts.AniList.Nick + ", " + toString(len(matches)) + " anime)" table.import-list thead diff --git a/pages/music/music.pixy b/pages/music/music.pixy index bbda7e6e..fb81d56d 100644 --- a/pages/music/music.pixy +++ b/pages/music/music.pixy @@ -1,5 +1,5 @@ component Music(tracks []*arn.SoundTrack) - h2 Soundtracks + h1 Soundtracks .music-buttons a.button.ajax(href="/new/soundtrack") diff --git a/pages/threads/threads.pixy b/pages/threads/threads.pixy index 77828501..2236af28 100644 --- a/pages/threads/threads.pixy +++ b/pages/threads/threads.pixy @@ -1,5 +1,5 @@ component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User) - h2.thread-title= thread.Title + h1.thread-title= thread.Title #thread.thread(data-id=thread.ID) .posts diff --git a/pages/tracks/tracks.pixy b/pages/tracks/tracks.pixy index cb510210..37e9237c 100644 --- a/pages/tracks/tracks.pixy +++ b/pages/tracks/tracks.pixy @@ -1,5 +1,5 @@ component Track(track *arn.SoundTrack) - h2= track.Media[0].Title + h1= track.Media[0].Title .sound-tracks SoundTrackAllMedia(track) \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e293f81d..06ba641d 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -8,6 +8,7 @@ import * as actions from "./Actions" export class AnimeNotifier { app: Application user: HTMLElement + title: string visibilityObserver: IntersectionObserver imageFound: MutationQueue @@ -17,6 +18,7 @@ export class AnimeNotifier { constructor(app: Application) { this.app = app this.user = null + this.title = "Anime Notifier" this.imageFound = new MutationQueue(elem => elem.classList.add("image-found")) this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found")) @@ -96,6 +98,16 @@ export class AnimeNotifier { Promise.resolve().then(() => this.displayLocalDates()), Promise.resolve().then(() => this.setSelectBoxValue()), Promise.resolve().then(() => this.assignActions()) + + let headers = document.getElementsByTagName("h1") + + if(this.app.currentPath === "/" || headers.length === 0) { + if(document.title !== this.title) { + document.title = this.title + } + } else { + document.title = headers[0].innerText + } } onIdle() { diff --git a/styles/forum.scarlet b/styles/forum.scarlet index c0644eba..c882eb68 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -23,24 +23,20 @@ post-content-padding-y = 0.75rem padding 0.75rem 1rem position relative + h1, h2, h3 + font-weight normal + text-align left + line-height 1.5em + margin typography-margin 0 + h1 font-size 1.5rem - line-height 1.5em - text-align left - margin typography-margin 0 h2 font-size 1.3rem - line-height 1.5em - font-weight normal - text-align left - margin typography-margin 0 h3 font-size 1.1rem - line-height 1.5em - font-weight normal - text-align left :hover .post-toolbar From ae4a69fc2bf0bff0a8653d19fa07a68bab26e96b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 22:12:59 +0200 Subject: [PATCH 162/527] User page titles --- pages/profile/profile.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 0cfce420..87b952fc 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -6,7 +6,7 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) ProfileImage(viewUser) .intro-container.mountable.never-unmount - h2#nick= viewUser.Nick + h1#nick= viewUser.Nick if viewUser.Tagline != "" p.profile-field.tagline From f0b63aec7550d3cbffeea41a170f7333a5053964 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 22:17:03 +0200 Subject: [PATCH 163/527] Minor changes --- pages/newsoundtrack/newsoundtrack.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy index ca2e65e8..e1a85517 100644 --- a/pages/newsoundtrack/newsoundtrack.pixy +++ b/pages/newsoundtrack/newsoundtrack.pixy @@ -1,7 +1,7 @@ component NewSoundTrack(user *arn.User) .widgets .widget - h3 New soundtrack + h1 New soundtrack .widget-input label(for="soundcloud-link") Soundcloud link: From e28b11c3224d88d016776db55a159f81755df385 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 22:23:32 +0200 Subject: [PATCH 164/527] More page titles --- pages/admin/admin.pixy | 2 +- pages/animelist/animelist.pixy | 2 +- pages/dashboard/dashboard.pixy | 2 +- pages/explore/explore.pixy | 2 +- pages/genres/genres.pixy | 2 +- pages/profile/posts.pixy | 2 +- pages/profile/tracks.pixy | 2 +- pages/settings/settings.pixy | 2 +- pages/users/users.pixy | 2 +- pages/webdev/webdev.pixy | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index 5790361d..e4d08109 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -1,5 +1,5 @@ component Admin(user *arn.User) - h2.page-title Admin Panel + h1.page-title Admin Panel h3 Server table diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 7e33b3e2..0c300b61 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -1,7 +1,7 @@ component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User, uri string) ProfileHeader(viewUser, user, uri) - h2.page-title.anime-list-owner= viewUser.Nick + "'s collection" + h1.page-title.anime-list-owner= viewUser.Nick + "'s collection" if len(animeLists[arn.AnimeListStatusWatching].Items) == 0 && len(animeLists[arn.AnimeListStatusCompleted].Items) == 0 && len(animeLists[arn.AnimeListStatusPlanned].Items) == 0 && len(animeLists[arn.AnimeListStatusHold].Items) == 0 && len(animeLists[arn.AnimeListStatusDropped].Items) == 0 p.no-data.mountable= viewUser.Nick + " hasn't added any anime yet." diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index a6f5258c..c0254df9 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -1,5 +1,5 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, soundTracks []*arn.SoundTrack, following []*arn.User) - h2.page-title Dash + h1.page-title Dashboard .widgets .widget.mountable diff --git a/pages/explore/explore.pixy b/pages/explore/explore.pixy index 202a7817..8a0149a5 100644 --- a/pages/explore/explore.pixy +++ b/pages/explore/explore.pixy @@ -1,3 +1,3 @@ component Airing(animeList []*arn.Anime) - h2.page-title(title=toString(len(animeList)) + " anime") Explore + h1.page-title(title=toString(len(animeList)) + " anime") Explore AnimeGrid(animeList) \ No newline at end of file diff --git a/pages/genres/genres.pixy b/pages/genres/genres.pixy index 6a684db4..5e5663d6 100644 --- a/pages/genres/genres.pixy +++ b/pages/genres/genres.pixy @@ -1,5 +1,5 @@ component Genres(genres []*arn.Genre) - h2.page-title Genres + h1.page-title Genres .genres each genre in genres diff --git a/pages/profile/posts.pixy b/pages/profile/posts.pixy index 981c336a..d7176bd8 100644 --- a/pages/profile/posts.pixy +++ b/pages/profile/posts.pixy @@ -2,7 +2,7 @@ component LatestPosts(postables []arn.Postable, viewUser *arn.User, user *arn.Us ProfileHeader(viewUser, user, uri) if len(postables) > 0 - h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick + h1.page-title= len(postables), " latest posts by ", postables[0].Author().Nick PostableList(postables, user) else p.no-data.mountable= viewUser.Nick + " hasn't written any posts yet." \ No newline at end of file diff --git a/pages/profile/tracks.pixy b/pages/profile/tracks.pixy index ae49399b..894f06f6 100644 --- a/pages/profile/tracks.pixy +++ b/pages/profile/tracks.pixy @@ -1,7 +1,7 @@ component TrackList(tracks []*arn.SoundTrack, viewUser *arn.User, user *arn.User, uri string) ProfileHeader(viewUser, user, uri) - h2.page-title= "Tracks added by " + viewUser.Nick + h1.page-title= "Tracks added by " + viewUser.Nick if len(tracks) == 0 p.no-data.mountable= viewUser.Nick + " hasn't added any tracks yet." diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 0580ddc4..382dc1ce 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -1,5 +1,5 @@ component Settings(user *arn.User) - h2.page-title Settings + h1.page-title Settings .widgets .widget.mountable(data-api="/api/user/" + user.ID) h3.widget-title diff --git a/pages/users/users.pixy b/pages/users/users.pixy index 4284669e..6a25e9cb 100644 --- a/pages/users/users.pixy +++ b/pages/users/users.pixy @@ -1,5 +1,5 @@ component Users(users []*arn.User) - h2.page-title Users + h1.page-title Users .user-avatars each user in users diff --git a/pages/webdev/webdev.pixy b/pages/webdev/webdev.pixy index 04d9f32d..11ae5d3b 100644 --- a/pages/webdev/webdev.pixy +++ b/pages/webdev/webdev.pixy @@ -1,5 +1,5 @@ component WebDev - h2.page-title WebDev + h1.page-title WebDev .light-button-group a.light-button(href="https://developers.google.com/speed/pagespeed/insights/?url=https://notify.moe/&tab=desktop", target="_blank", rel="noopener") From 137a2270dfa3947b7ca877d0ac920ea68751567d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 6 Jul 2017 22:26:02 +0200 Subject: [PATCH 165/527] More page titles --- pages/search/search.go | 2 +- pages/search/search.pixy | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pages/search/search.go b/pages/search/search.go index df5f1a59..0044ec82 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -14,5 +14,5 @@ func Get(ctx *aero.Context) string { term := ctx.Query("q") userResults, animeResults := arn.Search(term, maxUsers, maxAnime) - return ctx.HTML(components.SearchResults(userResults, animeResults)) + return ctx.HTML(components.SearchResults(term, userResults, animeResults)) } diff --git a/pages/search/search.pixy b/pages/search/search.pixy index c588f2b5..b6ea2c5a 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -1,4 +1,6 @@ -component SearchResults(users []*arn.User, animeResults []*arn.Anime) +component SearchResults(term string, users []*arn.User, animeResults []*arn.Anime) + h1.page-title= "Search: " + term + .widgets .widget h3 Users From 567e8e63d160d103da71fbd74800d10fe9dc1c5a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 02:07:34 +0200 Subject: [PATCH 166/527] Playing around with SVG --- main.go | 2 + pages/statistics/statistics.go | 71 +++++++++++++++++++++++++++++ pages/statistics/statistics.pixy | 13 ++++++ pages/statistics/statistics.scarlet | 24 ++++++++++ utils/AnalyticsItem.go | 7 +++ utils/SVGPieChartPath.go | 27 +++++++++++ utils/ToJSON.go | 9 ++++ 7 files changed, 153 insertions(+) create mode 100644 pages/statistics/statistics.go create mode 100644 pages/statistics/statistics.pixy create mode 100644 pages/statistics/statistics.scarlet create mode 100644 utils/AnalyticsItem.go create mode 100644 utils/SVGPieChartPath.go create mode 100644 utils/ToJSON.go diff --git a/main.go b/main.go index b91e38a9..626c5003 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( "github.com/animenotifier/notify.moe/pages/profile" "github.com/animenotifier/notify.moe/pages/search" "github.com/animenotifier/notify.moe/pages/settings" + "github.com/animenotifier/notify.moe/pages/statistics" "github.com/animenotifier/notify.moe/pages/threads" "github.com/animenotifier/notify.moe/pages/tracks" "github.com/animenotifier/notify.moe/pages/user" @@ -91,6 +92,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/import/anilist/animelist", listimportanilist.Preview) app.Ajax("/import/anilist/animelist/finish", listimportanilist.Finish) app.Ajax("/admin", admin.Get) + app.Ajax("/statistics", statistics.Get) app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) app.Ajax("/users", users.Get) diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go new file mode 100644 index 00000000..d9166cae --- /dev/null +++ b/pages/statistics/statistics.go @@ -0,0 +1,71 @@ +package statistics + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get ... +func Get(ctx *aero.Context) string { + analytics, err := arn.AllAnalytics() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Couldn't retrieve analytics", err) + } + + screenSizes := map[string]int{} + + for _, info := range analytics { + size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) + screenSizes[size]++ + } + + screenSizesSorted := []*utils.AnalyticsItem{} + + for size, count := range screenSizes { + item := &utils.AnalyticsItem{ + Key: size, + Value: count, + } + + if len(screenSizesSorted) == 0 { + screenSizesSorted = append(screenSizesSorted, item) + continue + } + + found := false + + for i := 0; i < len(screenSizesSorted); i++ { + if count >= screenSizesSorted[i].Value { + // Append empty element + screenSizesSorted = append(screenSizesSorted, nil) + + // Move all elements after index "i" 1 position up + copy(screenSizesSorted[i+1:], screenSizesSorted[i:]) + + // Set value for index "i" + screenSizesSorted[i] = item + + // Set flag + found = true + + // Leave loop + break + } + } + + if !found { + screenSizesSorted = append(screenSizesSorted, item) + } + } + + if len(screenSizesSorted) > 5 { + screenSizesSorted = screenSizesSorted[:5] + } + + return ctx.HTML(components.Statistics(screenSizesSorted)) +} diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy new file mode 100644 index 00000000..269e5a05 --- /dev/null +++ b/pages/statistics/statistics.pixy @@ -0,0 +1,13 @@ +component Statistics(screenSizes []*utils.AnalyticsItem) + h1 Statistics + + .statistics + h3 Screen size + PieChart + //- canvas#screen-sizes.graph(data-values=utils.ToJSON(screenSizes)) + +component PieChart + svg.graph(viewBox="-1.05 -1.05 2.1 2.1") + path.slice.slice-1(d=utils.SVGSlicePath(0, 0.5)) + path.slice.slice-2(d=utils.SVGSlicePath(0.5, 0.8)) + path.slice.slice-3(d=utils.SVGSlicePath(0.8, 1.0)) \ No newline at end of file diff --git a/pages/statistics/statistics.scarlet b/pages/statistics/statistics.scarlet new file mode 100644 index 00000000..062fc5be --- /dev/null +++ b/pages/statistics/statistics.scarlet @@ -0,0 +1,24 @@ +.statistics + vertical + align-items center + +.graph + transform rotate(-90deg) + width 250px + height 250px + +.slice + color black + default-transition + transform scale(1) + :hover + transform scale(1.05) + +.slice-1 + opacity 0.8 + +.slice-2 + opacity 0.6 + +.slice-3 + opacity 0.4 \ No newline at end of file diff --git a/utils/AnalyticsItem.go b/utils/AnalyticsItem.go new file mode 100644 index 00000000..e7efafff --- /dev/null +++ b/utils/AnalyticsItem.go @@ -0,0 +1,7 @@ +package utils + +// AnalyticsItem ... +type AnalyticsItem struct { + Key string + Value int +} diff --git a/utils/SVGPieChartPath.go b/utils/SVGPieChartPath.go new file mode 100644 index 00000000..cc3e2b3a --- /dev/null +++ b/utils/SVGPieChartPath.go @@ -0,0 +1,27 @@ +package utils + +import ( + "fmt" + "math" +) + +// coords returns the coordinates for the given percentage. +func coords(percent float64) (float64, float64) { + x := math.Cos(2 * math.Pi * percent) + y := math.Sin(2 * math.Pi * percent) + return x, y +} + +// SVGSlicePath creates a path string for a slice in a pie chart. +func SVGSlicePath(from float64, to float64) string { + x1, y1 := coords(from) + x2, y2 := coords(to) + + largeArc := "0" + + if to-from > 0.5 { + largeArc = "1" + } + + return fmt.Sprintf("M %.2f %.2f A 1 1 0 %s 1 %.2f %.2f L 0 0", x1, y1, largeArc, x2, y2) +} diff --git a/utils/ToJSON.go b/utils/ToJSON.go new file mode 100644 index 00000000..614b4465 --- /dev/null +++ b/utils/ToJSON.go @@ -0,0 +1,9 @@ +package utils + +import "encoding/json" + +// ToJSON converts an object to a JSON string, ignoring errors. +func ToJSON(v interface{}) string { + str, _ := json.Marshal(v) + return string(str) +} From 8c438ba08bf0761c1251a6c46c5981076067e392 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 02:17:17 +0200 Subject: [PATCH 167/527] SVG tests --- pages/statistics/statistics.pixy | 12 +++++++++--- utils/SVGPieChartPath.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 269e5a05..890c8af1 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -8,6 +8,12 @@ component Statistics(screenSizes []*utils.AnalyticsItem) component PieChart svg.graph(viewBox="-1.05 -1.05 2.1 2.1") - path.slice.slice-1(d=utils.SVGSlicePath(0, 0.5)) - path.slice.slice-2(d=utils.SVGSlicePath(0.5, 0.8)) - path.slice.slice-3(d=utils.SVGSlicePath(0.8, 1.0)) \ No newline at end of file + g + title Demo (50%) + path.slice.slice-1(d=utils.SVGSlicePath(0, 0.5)) + g + title Demo (30%) + path.slice.slice-2(d=utils.SVGSlicePath(0.5, 0.8)) + g + title Demo (20%) + path.slice.slice-3(d=utils.SVGSlicePath(0.8, 1.0)) \ No newline at end of file diff --git a/utils/SVGPieChartPath.go b/utils/SVGPieChartPath.go index cc3e2b3a..b349fe46 100644 --- a/utils/SVGPieChartPath.go +++ b/utils/SVGPieChartPath.go @@ -23,5 +23,5 @@ func SVGSlicePath(from float64, to float64) string { largeArc = "1" } - return fmt.Sprintf("M %.2f %.2f A 1 1 0 %s 1 %.2f %.2f L 0 0", x1, y1, largeArc, x2, y2) + return fmt.Sprintf("M %.3f %.3f A 1 1 0 %s 1 %.3f %.3f L 0 0", x1, y1, largeArc, x2, y2) } From a80266358daad2f213106e5d5c21448ba9c77246 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 02:37:43 +0200 Subject: [PATCH 168/527] Playing around with SVG --- pages/statistics/statistics.go | 19 ++++++++++++++++--- pages/statistics/statistics.pixy | 19 +++++++------------ pages/statistics/statistics.scarlet | 11 +---------- utils/SVGPieChartPath.go | 8 ++++++++ 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index d9166cae..3bc4c9d3 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -1,6 +1,7 @@ package statistics import ( + "fmt" "net/http" "github.com/aerogo/aero" @@ -63,9 +64,21 @@ func Get(ctx *aero.Context) string { } } - if len(screenSizesSorted) > 5 { - screenSizesSorted = screenSizesSorted[:5] + slices := []*utils.PieChartSlice{} + current := 0.0 + + for _, item := range screenSizesSorted { + percentage := float64(item.Value) / float64(len(analytics)) + + slices = append(slices, &utils.PieChartSlice{ + From: current, + To: current + percentage, + Title: fmt.Sprintf("%s (%d%%)", item.Key, int(percentage*100+0.5)), + Color: fmt.Sprintf("rgba(255, 64, 0, %.3f)", 0.8-current*0.8), + }) + + current += percentage } - return ctx.HTML(components.Statistics(screenSizesSorted)) + return ctx.HTML(components.Statistics(slices)) } diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 890c8af1..1f07df54 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -1,19 +1,14 @@ -component Statistics(screenSizes []*utils.AnalyticsItem) +component Statistics(screenSizes []*utils.PieChartSlice) h1 Statistics .statistics h3 Screen size - PieChart + PieChart(screenSizes) //- canvas#screen-sizes.graph(data-values=utils.ToJSON(screenSizes)) -component PieChart +component PieChart(slices []*utils.PieChartSlice) svg.graph(viewBox="-1.05 -1.05 2.1 2.1") - g - title Demo (50%) - path.slice.slice-1(d=utils.SVGSlicePath(0, 0.5)) - g - title Demo (30%) - path.slice.slice-2(d=utils.SVGSlicePath(0.5, 0.8)) - g - title Demo (20%) - path.slice.slice-3(d=utils.SVGSlicePath(0.8, 1.0)) \ No newline at end of file + each slice in slices + g + title= slice.Title + path.slice(d=utils.SVGSlicePath(slice.From, slice.To), fill=slice.Color) \ No newline at end of file diff --git a/pages/statistics/statistics.scarlet b/pages/statistics/statistics.scarlet index 062fc5be..4061a482 100644 --- a/pages/statistics/statistics.scarlet +++ b/pages/statistics/statistics.scarlet @@ -12,13 +12,4 @@ default-transition transform scale(1) :hover - transform scale(1.05) - -.slice-1 - opacity 0.8 - -.slice-2 - opacity 0.6 - -.slice-3 - opacity 0.4 \ No newline at end of file + transform scale(1.05) \ No newline at end of file diff --git a/utils/SVGPieChartPath.go b/utils/SVGPieChartPath.go index b349fe46..b82d9729 100644 --- a/utils/SVGPieChartPath.go +++ b/utils/SVGPieChartPath.go @@ -5,6 +5,14 @@ import ( "math" ) +// PieChartSlice ... +type PieChartSlice struct { + From float64 + To float64 + Title string + Color string +} + // coords returns the coordinates for the given percentage. func coords(percent float64) (float64, float64) { x := math.Cos(2 * math.Pi * percent) From eb8ebed83039100b10ad7f30059b4a7b5954950a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 02:53:19 +0200 Subject: [PATCH 169/527] HSL model for pie charts --- pages/statistics/statistics.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index 3bc4c9d3..8cc06f90 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -66,6 +66,8 @@ func Get(ctx *aero.Context) string { slices := []*utils.PieChartSlice{} current := 0.0 + hueOffset := 0.0 + hueScaling := 60.0 for _, item := range screenSizesSorted { percentage := float64(item.Value) / float64(len(analytics)) @@ -74,7 +76,7 @@ func Get(ctx *aero.Context) string { From: current, To: current + percentage, Title: fmt.Sprintf("%s (%d%%)", item.Key, int(percentage*100+0.5)), - Color: fmt.Sprintf("rgba(255, 64, 0, %.3f)", 0.8-current*0.8), + Color: fmt.Sprintf("hsl(%.1f, 75%%, 50%%)", current*hueScaling+hueOffset), }) current += percentage From bff0f62629b3946942bdbe7ecdccb84beb623a22 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 03:22:29 +0200 Subject: [PATCH 170/527] More SVG fun --- pages/statistics/statistics.go | 72 +++-------------- pages/statistics/statistics.pixy | 13 +-- pages/statistics/statistics.scarlet | 7 +- utils/AnalyticsItem.go | 2 +- utils/PieChart.go | 118 ++++++++++++++++++++++++++++ utils/SVGPieChartPath.go | 35 --------- 6 files changed, 140 insertions(+), 107 deletions(-) create mode 100644 utils/PieChart.go delete mode 100644 utils/SVGPieChartPath.go diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index 8cc06f90..6d916c2c 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -18,69 +18,21 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Couldn't retrieve analytics", err) } - screenSizes := map[string]int{} + screenSize := map[string]float64{} + platform := map[string]float64{} + pixelRatio := map[string]float64{} for _, info := range analytics { + platform[info.System.Platform]++ + pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ + size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) - screenSizes[size]++ + screenSize[size]++ } - screenSizesSorted := []*utils.AnalyticsItem{} - - for size, count := range screenSizes { - item := &utils.AnalyticsItem{ - Key: size, - Value: count, - } - - if len(screenSizesSorted) == 0 { - screenSizesSorted = append(screenSizesSorted, item) - continue - } - - found := false - - for i := 0; i < len(screenSizesSorted); i++ { - if count >= screenSizesSorted[i].Value { - // Append empty element - screenSizesSorted = append(screenSizesSorted, nil) - - // Move all elements after index "i" 1 position up - copy(screenSizesSorted[i+1:], screenSizesSorted[i:]) - - // Set value for index "i" - screenSizesSorted[i] = item - - // Set flag - found = true - - // Leave loop - break - } - } - - if !found { - screenSizesSorted = append(screenSizesSorted, item) - } - } - - slices := []*utils.PieChartSlice{} - current := 0.0 - hueOffset := 0.0 - hueScaling := 60.0 - - for _, item := range screenSizesSorted { - percentage := float64(item.Value) / float64(len(analytics)) - - slices = append(slices, &utils.PieChartSlice{ - From: current, - To: current + percentage, - Title: fmt.Sprintf("%s (%d%%)", item.Key, int(percentage*100+0.5)), - Color: fmt.Sprintf("hsl(%.1f, 75%%, 50%%)", current*hueScaling+hueOffset), - }) - - current += percentage - } - - return ctx.HTML(components.Statistics(slices)) + return ctx.HTML(components.Statistics( + utils.NewPieChart("Screen sizes", screenSize), + utils.NewPieChart("Platforms", platform), + utils.NewPieChart("Pixel ratios", pixelRatio), + )) } diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 1f07df54..d342ebe9 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -1,13 +1,14 @@ -component Statistics(screenSizes []*utils.PieChartSlice) +component Statistics(pieCharts ...*utils.PieChart) h1 Statistics - .statistics - h3 Screen size - PieChart(screenSizes) - //- canvas#screen-sizes.graph(data-values=utils.ToJSON(screenSizes)) + .widgets.statistics + each pie in pieCharts + .widget + h3.widget-title= pie.Title + PieChart(pie.Slices) component PieChart(slices []*utils.PieChartSlice) - svg.graph(viewBox="-1.05 -1.05 2.1 2.1") + svg.pie-chart(viewBox="-1.1 -1.1 2.2 2.2") each slice in slices g title= slice.Title diff --git a/pages/statistics/statistics.scarlet b/pages/statistics/statistics.scarlet index 4061a482..2f7878ae 100644 --- a/pages/statistics/statistics.scarlet +++ b/pages/statistics/statistics.scarlet @@ -1,11 +1,8 @@ .statistics - vertical - align-items center + // -.graph +.pie-chart transform rotate(-90deg) - width 250px - height 250px .slice color black diff --git a/utils/AnalyticsItem.go b/utils/AnalyticsItem.go index e7efafff..82b3288f 100644 --- a/utils/AnalyticsItem.go +++ b/utils/AnalyticsItem.go @@ -3,5 +3,5 @@ package utils // AnalyticsItem ... type AnalyticsItem struct { Key string - Value int + Value float64 } diff --git a/utils/PieChart.go b/utils/PieChart.go new file mode 100644 index 00000000..28cab8fe --- /dev/null +++ b/utils/PieChart.go @@ -0,0 +1,118 @@ +package utils + +import ( + "fmt" + "math" +) + +// PieChart ... +type PieChart struct { + Title string + Slices []*PieChartSlice +} + +// PieChartSlice ... +type PieChartSlice struct { + From float64 + To float64 + Title string + Color string +} + +// coords returns the coordinates for the given percentage. +func coords(percent float64) (float64, float64) { + x := math.Cos(2 * math.Pi * percent) + y := math.Sin(2 * math.Pi * percent) + return x, y +} + +// SVGSlicePath creates a path string for a slice in a pie chart. +func SVGSlicePath(from float64, to float64) string { + x1, y1 := coords(from) + x2, y2 := coords(to) + + largeArc := "0" + + if to-from > 0.5 { + largeArc = "1" + } + + return fmt.Sprintf("M %.3f %.3f A 1 1 0 %s 1 %.3f %.3f L 0 0", x1, y1, largeArc, x2, y2) +} + +// NewPieChart ... +func NewPieChart(title string, data map[string]float64) *PieChart { + return &PieChart{ + Title: title, + Slices: ToPieChartSlices(data), + } +} + +// ToPieChartSlices ... +func ToPieChartSlices(data map[string]float64) []*PieChartSlice { + if len(data) == 0 { + return nil + } + + dataSorted := []*AnalyticsItem{} + sum := 0.0 + + for key, value := range data { + sum += value + + item := &AnalyticsItem{ + Key: key, + Value: value, + } + + if len(dataSorted) == 0 { + dataSorted = append(dataSorted, item) + continue + } + + found := false + + for i := 0; i < len(dataSorted); i++ { + if value >= dataSorted[i].Value { + // Append empty element + dataSorted = append(dataSorted, nil) + + // Move all elements after index "i" 1 position up + copy(dataSorted[i+1:], dataSorted[i:]) + + // Set value for index "i" + dataSorted[i] = item + + // Set flag + found = true + + // Leave loop + break + } + } + + if !found { + dataSorted = append(dataSorted, item) + } + } + + slices := []*PieChartSlice{} + current := 0.0 + hueOffset := 0.0 + hueScaling := 60.0 + + for _, item := range dataSorted { + percentage := float64(item.Value) / sum + + slices = append(slices, &PieChartSlice{ + From: current, + To: current + percentage, + Title: fmt.Sprintf("%s (%d%%)", item.Key, int(percentage*100+0.5)), + Color: fmt.Sprintf("hsl(%.2f, 75%%, 50%%)", current*hueScaling+hueOffset), + }) + + current += percentage + } + + return slices +} diff --git a/utils/SVGPieChartPath.go b/utils/SVGPieChartPath.go deleted file mode 100644 index b82d9729..00000000 --- a/utils/SVGPieChartPath.go +++ /dev/null @@ -1,35 +0,0 @@ -package utils - -import ( - "fmt" - "math" -) - -// PieChartSlice ... -type PieChartSlice struct { - From float64 - To float64 - Title string - Color string -} - -// coords returns the coordinates for the given percentage. -func coords(percent float64) (float64, float64) { - x := math.Cos(2 * math.Pi * percent) - y := math.Sin(2 * math.Pi * percent) - return x, y -} - -// SVGSlicePath creates a path string for a slice in a pie chart. -func SVGSlicePath(from float64, to float64) string { - x1, y1 := coords(from) - x2, y2 := coords(to) - - largeArc := "0" - - if to-from > 0.5 { - largeArc = "1" - } - - return fmt.Sprintf("M %.3f %.3f A 1 1 0 %s 1 %.3f %.3f L 0 0", x1, y1, largeArc, x2, y2) -} From 1fb5b4210ed98d1930c2c0eeacda9191d48911d9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 03:45:39 +0200 Subject: [PATCH 171/527] More stats --- pages/statistics/statistics.go | 47 +++++++++++++++++++++++++++++----- utils/PieChart.go | 8 +++--- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index 6d916c2c..c3063328 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -3,6 +3,7 @@ package statistics import ( "fmt" "net/http" + "strings" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -10,6 +11,8 @@ import ( "github.com/animenotifier/notify.moe/utils" ) +type stats = map[string]float64 + // Get ... func Get(ctx *aero.Context) string { analytics, err := arn.AllAnalytics() @@ -18,21 +21,51 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Couldn't retrieve analytics", err) } - screenSize := map[string]float64{} - platform := map[string]float64{} - pixelRatio := map[string]float64{} + screenSize := stats{} + // platform := stats{} + pixelRatio := stats{} + browser := stats{} + country := stats{} + gender := stats{} + os := stats{} for _, info := range analytics { - platform[info.System.Platform]++ + // platform[info.System.Platform]++ pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) screenSize[size]++ } + for user := range arn.MustStreamUsers() { + if user.Gender != "" { + gender[user.Gender]++ + } + + if user.Browser.Name != "" { + browser[user.Browser.Name]++ + } + + if user.Location.CountryName != "" { + country[user.Location.CountryName]++ + } + + if user.OS.Name != "" { + if strings.HasPrefix(user.OS.Name, "CrOS") { + user.OS.Name = "Chrome OS" + } + + os[user.OS.Name]++ + } + } + return ctx.HTML(components.Statistics( - utils.NewPieChart("Screen sizes", screenSize), - utils.NewPieChart("Platforms", platform), - utils.NewPieChart("Pixel ratios", pixelRatio), + utils.NewPieChart("OS", os), + // utils.NewPieChart("Platform", platform), + utils.NewPieChart("Screen size", screenSize), + utils.NewPieChart("Pixel ratio", pixelRatio), + utils.NewPieChart("Browser", browser), + utils.NewPieChart("Country", country), + utils.NewPieChart("Gender", gender), )) } diff --git a/utils/PieChart.go b/utils/PieChart.go index 28cab8fe..203cdad9 100644 --- a/utils/PieChart.go +++ b/utils/PieChart.go @@ -98,17 +98,17 @@ func ToPieChartSlices(data map[string]float64) []*PieChartSlice { slices := []*PieChartSlice{} current := 0.0 - hueOffset := 0.0 - hueScaling := 60.0 + hueOffset := 230.0 + hueScaling := -30.0 - for _, item := range dataSorted { + for i, item := range dataSorted { percentage := float64(item.Value) / sum slices = append(slices, &PieChartSlice{ From: current, To: current + percentage, Title: fmt.Sprintf("%s (%d%%)", item.Key, int(percentage*100+0.5)), - Color: fmt.Sprintf("hsl(%.2f, 75%%, 50%%)", current*hueScaling+hueOffset), + Color: fmt.Sprintf("hsl(%.2f, 75%%, 50%%)", float64(i)*hueScaling+hueOffset), }) current += percentage From fc2130b2df0c41bedb02240e995dcada2f03173f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 10:57:48 +0200 Subject: [PATCH 172/527] New statistics --- main.go | 1 + pages/statistics/anime.go | 48 ++++++++++++++++++++++++++++++++ pages/statistics/statistics.go | 4 +-- pages/statistics/statistics.pixy | 12 ++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 pages/statistics/anime.go diff --git a/main.go b/main.go index 626c5003..e810fd01 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/import/anilist/animelist/finish", listimportanilist.Finish) app.Ajax("/admin", admin.Get) app.Ajax("/statistics", statistics.Get) + app.Ajax("/statistics/anime", statistics.Anime) app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) app.Ajax("/users", users.Get) diff --git a/pages/statistics/anime.go b/pages/statistics/anime.go new file mode 100644 index 00000000..98fa115d --- /dev/null +++ b/pages/statistics/anime.go @@ -0,0 +1,48 @@ +package statistics + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Anime ... +func Anime(ctx *aero.Context) string { + allAnime, err := arn.AllAnime() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Couldn't fetch anime", err) + } + + shoboi := stats{} + anilist := stats{} + status := stats{} + types := stats{} + + for _, anime := range allAnime { + if anime.GetMapping("shoboi/anime") != "" { + shoboi["Connected with Shoboi"]++ + } else { + shoboi["Not connected with Shoboi"]++ + } + + if anime.GetMapping("anilist/anime") != "" { + anilist["Connected with Anilist"]++ + } else { + anilist["Not connected with Anilist"]++ + } + + status[anime.Status]++ + types[anime.Type]++ + } + + return ctx.HTML(components.Statistics( + utils.NewPieChart("Type", types), + utils.NewPieChart("Status", status), + utils.NewPieChart("Anilist", anilist), + utils.NewPieChart("Shoboi", shoboi), + )) +} diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index c3063328..4343706c 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -18,7 +18,7 @@ func Get(ctx *aero.Context) string { analytics, err := arn.AllAnalytics() if err != nil { - return ctx.Error(http.StatusInternalServerError, "Couldn't retrieve analytics", err) + return ctx.Error(http.StatusInternalServerError, "Couldn't fetch analytics", err) } screenSize := stats{} @@ -63,9 +63,9 @@ func Get(ctx *aero.Context) string { utils.NewPieChart("OS", os), // utils.NewPieChart("Platform", platform), utils.NewPieChart("Screen size", screenSize), - utils.NewPieChart("Pixel ratio", pixelRatio), utils.NewPieChart("Browser", browser), utils.NewPieChart("Country", country), utils.NewPieChart("Gender", gender), + utils.NewPieChart("Pixel ratio", pixelRatio), )) } diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index d342ebe9..0899237f 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -1,12 +1,24 @@ component Statistics(pieCharts ...*utils.PieChart) h1 Statistics + StatisticsHeader + .widgets.statistics each pie in pieCharts .widget h3.widget-title= pie.Title PieChart(pie.Slices) +component StatisticsHeader + .buttons.tabs + a.button.tab.action(href="/statistics", data-action="diff", data-trigger="click") + Icon("user") + span User + + a.button.tab.action(href="/statistics/anime", data-action="diff", data-trigger="click") + Icon("tv") + span Anime + component PieChart(slices []*utils.PieChartSlice) svg.pie-chart(viewBox="-1.1 -1.1 2.2 2.2") each slice in slices From b75ec453f5c0a1f3532ecff9f1d727876287e644 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 11:05:01 +0200 Subject: [PATCH 173/527] New statistics --- pages/statistics/anime.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pages/statistics/anime.go b/pages/statistics/anime.go index 98fa115d..c131b9f6 100644 --- a/pages/statistics/anime.go +++ b/pages/statistics/anime.go @@ -21,8 +21,31 @@ func Anime(ctx *aero.Context) string { anilist := stats{} status := stats{} types := stats{} + shoboiEdits := stats{} + anilistEdits := stats{} + rating := stats{} for _, anime := range allAnime { + for _, external := range anime.Mappings { + if external.Service == "shoboi/anime" { + if external.CreatedBy == "" { + shoboiEdits["Bot"]++ + } else { + user, _ := arn.GetUser(external.CreatedBy) + shoboiEdits[user.Nick]++ + } + } + + if external.Service == "anilist/anime" { + if external.CreatedBy == "" { + anilistEdits["Bot"]++ + } else { + user, _ := arn.GetUser(external.CreatedBy) + anilistEdits[user.Nick]++ + } + } + } + if anime.GetMapping("shoboi/anime") != "" { shoboi["Connected with Shoboi"]++ } else { @@ -35,6 +58,8 @@ func Anime(ctx *aero.Context) string { anilist["Not connected with Anilist"]++ } + rating[arn.ToString(int(anime.Rating.Overall+0.5))]++ + status[anime.Status]++ types[anime.Type]++ } @@ -42,7 +67,10 @@ func Anime(ctx *aero.Context) string { return ctx.HTML(components.Statistics( utils.NewPieChart("Type", types), utils.NewPieChart("Status", status), + utils.NewPieChart("Rating", rating), utils.NewPieChart("Anilist", anilist), + utils.NewPieChart("Anilist Editors", anilistEdits), utils.NewPieChart("Shoboi", shoboi), + utils.NewPieChart("Shoboi Editors", shoboiEdits), )) } From 4f780a36f8128f31155266099652a226f7e283b1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 11:11:59 +0200 Subject: [PATCH 174/527] Improved stats --- pages/statistics/anime.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/statistics/anime.go b/pages/statistics/anime.go index c131b9f6..5f2ce51a 100644 --- a/pages/statistics/anime.go +++ b/pages/statistics/anime.go @@ -69,8 +69,8 @@ func Anime(ctx *aero.Context) string { utils.NewPieChart("Status", status), utils.NewPieChart("Rating", rating), utils.NewPieChart("Anilist", anilist), - utils.NewPieChart("Anilist Editors", anilistEdits), utils.NewPieChart("Shoboi", shoboi), + utils.NewPieChart("Anilist Editors", anilistEdits), utils.NewPieChart("Shoboi Editors", shoboiEdits), )) } From 60d47ead7f3badb986a9a888a1e07a1b4ea3bbce Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 12:10:27 +0200 Subject: [PATCH 175/527] Improved statistics --- jobs/statistics/statistics.go | 151 +++++++++++++++++++++++ pages/statistics/anime.go | 68 +--------- pages/statistics/statistics.go | 63 +--------- pages/statistics/statistics.pixy | 4 +- patches/import-anilist/import-anilist.go | 4 +- utils/AnalyticsItem.go | 7 -- utils/PieChart.go | 91 -------------- 7 files changed, 161 insertions(+), 227 deletions(-) create mode 100644 jobs/statistics/statistics.go delete mode 100644 utils/AnalyticsItem.go diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go new file mode 100644 index 00000000..85ddf9a8 --- /dev/null +++ b/jobs/statistics/statistics.go @@ -0,0 +1,151 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +type stats map[string]float64 + +func main() { + color.Yellow("Generating statistics") + + userStats := getUserStats() + animeStats := getAnimeStats() + + arn.PanicOnError(arn.DB.Set("Cache", "user statistics", &arn.StatisticsCategory{ + Name: "Users", + PieCharts: userStats, + })) + arn.PanicOnError(arn.DB.Set("Cache", "anime statistics", &arn.StatisticsCategory{ + Name: "Anime", + PieCharts: animeStats, + })) + + color.Green("Finished.") +} + +func getUserStats() []*arn.PieChart { + println("Generating user statistics") + + analytics, err := arn.AllAnalytics() + arn.PanicOnError(err) + + screenSize := stats{} + pixelRatio := stats{} + browser := stats{} + country := stats{} + gender := stats{} + os := stats{} + + for _, info := range analytics { + pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ + + size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) + screenSize[size]++ + } + + for user := range arn.MustStreamUsers() { + if user.Gender != "" { + gender[user.Gender]++ + } + + if user.Browser.Name != "" { + browser[user.Browser.Name]++ + } + + if user.Location.CountryName != "" { + country[user.Location.CountryName]++ + } + + if user.OS.Name != "" { + if strings.HasPrefix(user.OS.Name, "CrOS") { + user.OS.Name = "Chrome OS" + } + + os[user.OS.Name]++ + } + } + + println("Finished user statistics") + + return []*arn.PieChart{ + arn.NewPieChart("OS", os), + arn.NewPieChart("Screen size", screenSize), + arn.NewPieChart("Browser", browser), + arn.NewPieChart("Country", country), + arn.NewPieChart("Gender", gender), + arn.NewPieChart("Pixel ratio", pixelRatio), + } +} + +func getAnimeStats() []*arn.PieChart { + println("Generating anime statistics") + + allAnime, err := arn.AllAnime() + arn.PanicOnError(err) + + shoboi := stats{} + anilist := stats{} + status := stats{} + types := stats{} + shoboiEdits := stats{} + anilistEdits := stats{} + rating := stats{} + + for _, anime := range allAnime { + for _, external := range anime.Mappings { + if external.Service == "shoboi/anime" { + if external.CreatedBy == "" { + shoboiEdits["Bot"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + shoboiEdits[user.Nick]++ + } + } + + if external.Service == "anilist/anime" { + if external.CreatedBy == "" { + anilistEdits["Bot"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + anilistEdits[user.Nick]++ + } + } + } + + if anime.GetMapping("shoboi/anime") != "" { + shoboi["Connected with Shoboi"]++ + } else { + shoboi["Not connected with Shoboi"]++ + } + + if anime.GetMapping("anilist/anime") != "" { + anilist["Connected with Anilist"]++ + } else { + anilist["Not connected with Anilist"]++ + } + + rating[arn.ToString(int(anime.Rating.Overall+0.5))]++ + + status[anime.Status]++ + types[anime.Type]++ + } + + println("Finished anime statistics") + + return []*arn.PieChart{ + arn.NewPieChart("Type", types), + arn.NewPieChart("Status", status), + arn.NewPieChart("Rating", rating), + arn.NewPieChart("Anilist", anilist), + arn.NewPieChart("Shoboi", shoboi), + arn.NewPieChart("Anilist Editors", anilistEdits), + arn.NewPieChart("Shoboi Editors", shoboiEdits), + } +} diff --git a/pages/statistics/anime.go b/pages/statistics/anime.go index 5f2ce51a..87e20c28 100644 --- a/pages/statistics/anime.go +++ b/pages/statistics/anime.go @@ -1,76 +1,14 @@ package statistics import ( - "net/http" - "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" - "github.com/animenotifier/notify.moe/utils" ) // Anime ... func Anime(ctx *aero.Context) string { - allAnime, err := arn.AllAnime() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Couldn't fetch anime", err) - } - - shoboi := stats{} - anilist := stats{} - status := stats{} - types := stats{} - shoboiEdits := stats{} - anilistEdits := stats{} - rating := stats{} - - for _, anime := range allAnime { - for _, external := range anime.Mappings { - if external.Service == "shoboi/anime" { - if external.CreatedBy == "" { - shoboiEdits["Bot"]++ - } else { - user, _ := arn.GetUser(external.CreatedBy) - shoboiEdits[user.Nick]++ - } - } - - if external.Service == "anilist/anime" { - if external.CreatedBy == "" { - anilistEdits["Bot"]++ - } else { - user, _ := arn.GetUser(external.CreatedBy) - anilistEdits[user.Nick]++ - } - } - } - - if anime.GetMapping("shoboi/anime") != "" { - shoboi["Connected with Shoboi"]++ - } else { - shoboi["Not connected with Shoboi"]++ - } - - if anime.GetMapping("anilist/anime") != "" { - anilist["Connected with Anilist"]++ - } else { - anilist["Not connected with Anilist"]++ - } - - rating[arn.ToString(int(anime.Rating.Overall+0.5))]++ - - status[anime.Status]++ - types[anime.Type]++ - } - - return ctx.HTML(components.Statistics( - utils.NewPieChart("Type", types), - utils.NewPieChart("Status", status), - utils.NewPieChart("Rating", rating), - utils.NewPieChart("Anilist", anilist), - utils.NewPieChart("Shoboi", shoboi), - utils.NewPieChart("Anilist Editors", anilistEdits), - utils.NewPieChart("Shoboi Editors", shoboiEdits), - )) + statistics := arn.StatisticsCategory{} + arn.DB.GetObject("Cache", "anime statistics", &statistics) + return ctx.HTML(components.Statistics(statistics.PieCharts...)) } diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index 4343706c..04d1855d 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -1,71 +1,14 @@ package statistics import ( - "fmt" - "net/http" - "strings" - "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" - "github.com/animenotifier/notify.moe/utils" ) -type stats = map[string]float64 - // Get ... func Get(ctx *aero.Context) string { - analytics, err := arn.AllAnalytics() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Couldn't fetch analytics", err) - } - - screenSize := stats{} - // platform := stats{} - pixelRatio := stats{} - browser := stats{} - country := stats{} - gender := stats{} - os := stats{} - - for _, info := range analytics { - // platform[info.System.Platform]++ - pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ - - size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) - screenSize[size]++ - } - - for user := range arn.MustStreamUsers() { - if user.Gender != "" { - gender[user.Gender]++ - } - - if user.Browser.Name != "" { - browser[user.Browser.Name]++ - } - - if user.Location.CountryName != "" { - country[user.Location.CountryName]++ - } - - if user.OS.Name != "" { - if strings.HasPrefix(user.OS.Name, "CrOS") { - user.OS.Name = "Chrome OS" - } - - os[user.OS.Name]++ - } - } - - return ctx.HTML(components.Statistics( - utils.NewPieChart("OS", os), - // utils.NewPieChart("Platform", platform), - utils.NewPieChart("Screen size", screenSize), - utils.NewPieChart("Browser", browser), - utils.NewPieChart("Country", country), - utils.NewPieChart("Gender", gender), - utils.NewPieChart("Pixel ratio", pixelRatio), - )) + statistics := arn.StatisticsCategory{} + arn.DB.GetObject("Cache", "user statistics", &statistics) + return ctx.HTML(components.Statistics(statistics.PieCharts...)) } diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 0899237f..8cb99c08 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -1,4 +1,4 @@ -component Statistics(pieCharts ...*utils.PieChart) +component Statistics(pieCharts ...*arn.PieChart) h1 Statistics StatisticsHeader @@ -19,7 +19,7 @@ component StatisticsHeader Icon("tv") span Anime -component PieChart(slices []*utils.PieChartSlice) +component PieChart(slices []*arn.PieChartSlice) svg.pie-chart(viewBox="-1.1 -1.1 2.2 2.2") each slice in slices g diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go index da1a1276..5bfee012 100644 --- a/patches/import-anilist/import-anilist.go +++ b/patches/import-anilist/import-anilist.go @@ -20,8 +20,8 @@ func main() { for aniListAnime := range stream { anime := arn.FindAniListAnime(aniListAnime, allAnime) - if anime != nil { - fmt.Println(aniListAnime.TitleRomaji, "=>", anime.Title.Canonical) + if anime == nil { + fmt.Println(anime.ID, aniListAnime.TitleRomaji) } count++ diff --git a/utils/AnalyticsItem.go b/utils/AnalyticsItem.go deleted file mode 100644 index 82b3288f..00000000 --- a/utils/AnalyticsItem.go +++ /dev/null @@ -1,7 +0,0 @@ -package utils - -// AnalyticsItem ... -type AnalyticsItem struct { - Key string - Value float64 -} diff --git a/utils/PieChart.go b/utils/PieChart.go index 203cdad9..b349fe46 100644 --- a/utils/PieChart.go +++ b/utils/PieChart.go @@ -5,20 +5,6 @@ import ( "math" ) -// PieChart ... -type PieChart struct { - Title string - Slices []*PieChartSlice -} - -// PieChartSlice ... -type PieChartSlice struct { - From float64 - To float64 - Title string - Color string -} - // coords returns the coordinates for the given percentage. func coords(percent float64) (float64, float64) { x := math.Cos(2 * math.Pi * percent) @@ -39,80 +25,3 @@ func SVGSlicePath(from float64, to float64) string { return fmt.Sprintf("M %.3f %.3f A 1 1 0 %s 1 %.3f %.3f L 0 0", x1, y1, largeArc, x2, y2) } - -// NewPieChart ... -func NewPieChart(title string, data map[string]float64) *PieChart { - return &PieChart{ - Title: title, - Slices: ToPieChartSlices(data), - } -} - -// ToPieChartSlices ... -func ToPieChartSlices(data map[string]float64) []*PieChartSlice { - if len(data) == 0 { - return nil - } - - dataSorted := []*AnalyticsItem{} - sum := 0.0 - - for key, value := range data { - sum += value - - item := &AnalyticsItem{ - Key: key, - Value: value, - } - - if len(dataSorted) == 0 { - dataSorted = append(dataSorted, item) - continue - } - - found := false - - for i := 0; i < len(dataSorted); i++ { - if value >= dataSorted[i].Value { - // Append empty element - dataSorted = append(dataSorted, nil) - - // Move all elements after index "i" 1 position up - copy(dataSorted[i+1:], dataSorted[i:]) - - // Set value for index "i" - dataSorted[i] = item - - // Set flag - found = true - - // Leave loop - break - } - } - - if !found { - dataSorted = append(dataSorted, item) - } - } - - slices := []*PieChartSlice{} - current := 0.0 - hueOffset := 230.0 - hueScaling := -30.0 - - for i, item := range dataSorted { - percentage := float64(item.Value) / sum - - slices = append(slices, &PieChartSlice{ - From: current, - To: current + percentage, - Title: fmt.Sprintf("%s (%d%%)", item.Key, int(percentage*100+0.5)), - Color: fmt.Sprintf("hsl(%.2f, 75%%, 50%%)", float64(i)*hueScaling+hueOffset), - }) - - current += percentage - } - - return slices -} From dede7d9135c986c56d766e021c7be6fdbf52d518 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 12:13:41 +0200 Subject: [PATCH 176/527] Typo --- pages/statistics/statistics.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 8cb99c08..29faf393 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -13,7 +13,7 @@ component StatisticsHeader .buttons.tabs a.button.tab.action(href="/statistics", data-action="diff", data-trigger="click") Icon("user") - span User + span Users a.button.tab.action(href="/statistics/anime", data-action="diff", data-trigger="click") Icon("tv") From b07c75ca1a8dc8bb3b7cd258601347d54d00f577 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 12:22:43 +0200 Subject: [PATCH 177/527] Added statistics to scheduler --- jobs/jobs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/jobs/jobs.go b/jobs/jobs.go index 74e4662f..7b938c83 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -27,6 +27,7 @@ var jobs = map[string]time.Duration{ "forum-activity": 1 * time.Minute, "anime-ratings": 10 * time.Minute, "airing-anime": 10 * time.Minute, + "statistics": 15 * time.Minute, "popular-anime": 20 * time.Minute, "avatars": 30 * time.Minute, "sync-shoboi": 8 * time.Hour, From ddc149ffa03aae2dc275ad56fa3cbd4f60ed6b66 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 12:24:48 +0200 Subject: [PATCH 178/527] Limited bot search results to 3 --- jobs/discord/discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/discord/discord.go b/jobs/discord/discord.go index 6e3749cd..359a2cca 100644 --- a/jobs/discord/discord.go +++ b/jobs/discord/discord.go @@ -97,7 +97,7 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { if strings.HasPrefix(m.Content, "!s ") { term := m.Content[len("!s "):] - userResults, animeResults := arn.Search(term, 10, 10) + userResults, animeResults := arn.Search(term, 3, 3) message := "" for _, user := range userResults { From cfe61542e04aa14d9ac919c531376c46ae8bef8d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 12:52:24 +0200 Subject: [PATCH 179/527] Added statistics to main menu --- mixins/Navigation.pixy | 10 +++++++--- pages/statistics/statistics.pixy | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index a8f6f0a6..877daa2f 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -7,7 +7,7 @@ component Navigation(user *arn.User) component LoggedOutMenu nav#navigation.logged-out NavigationButton("About", "/", "question-circle") - NavigationButton("Music", "/music", "headphones") + NavigationButton("Explore", "/explore", "th") NavigationButton("Forum", "/forum", "comment") FuzzySearch @@ -15,7 +15,8 @@ component LoggedOutMenu .extra-navigation NavigationButton("Users", "/users", "globe") - NavigationButton("Explore", "/explore", "th") + NavigationButton("Music", "/music", "headphones") + NavigationButton("Login", "/login", "sign-in") component LoggedInMenu(user *arn.User) @@ -34,8 +35,11 @@ component LoggedInMenu(user *arn.User) .extra-navigation NavigationButton("Users", "/users", "globe") - + NavigationButton("Explore", "/explore", "th") + + .extra-navigation + NavigationButton("Statistics", "/statistics", "pie-chart") NavigationButton("Settings", "/settings", "cog") diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 29faf393..4418ad7e 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -1,5 +1,5 @@ component Statistics(pieCharts ...*arn.PieChart) - h1 Statistics + h1.page-title Statistics StatisticsHeader From c977651be5eb746a66bc007705db6fe3802e1598 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 13:33:31 +0200 Subject: [PATCH 180/527] Added footer --- pages/statistics/statistics.pixy | 14 +++++++++----- pages/statistics/statistics.scarlet | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 4418ad7e..c4d79390 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -3,11 +3,15 @@ component Statistics(pieCharts ...*arn.PieChart) StatisticsHeader - .widgets.statistics - each pie in pieCharts - .widget - h3.widget-title= pie.Title - PieChart(pie.Slices) + .statistics + .widgets + each pie in pieCharts + .widget + h3.widget-title= pie.Title + PieChart(pie.Slices) + + .footer + p Data is collected for statistical purposes only. We respect user privacy and we will never display or sell data to 3rd party services. component StatisticsHeader .buttons.tabs diff --git a/pages/statistics/statistics.scarlet b/pages/statistics/statistics.scarlet index 2f7878ae..f432ec2f 100644 --- a/pages/statistics/statistics.scarlet +++ b/pages/statistics/statistics.scarlet @@ -1,5 +1,5 @@ .statistics - // + text-align center .pie-chart transform rotate(-90deg) From 3b8ae05ef6c91c0ace742ace0b1557b50ff9bc2b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 13:34:43 +0200 Subject: [PATCH 181/527] Minor change --- pages/statistics/statistics.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index c4d79390..e5ac3e19 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -11,7 +11,7 @@ component Statistics(pieCharts ...*arn.PieChart) PieChart(pie.Slices) .footer - p Data is collected for statistical purposes only. We respect user privacy and we will never display or sell data to 3rd party services. + p Data is collected for statistical purposes only. We respect user privacy and we will never display or sell critical data to 3rd party services. component StatisticsHeader .buttons.tabs From 6d130bcdea6883786d00618ffaba4f2cef7d23d3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 14:40:20 +0200 Subject: [PATCH 182/527] Removed statistics from menu --- mixins/Navigation.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 877daa2f..edff724b 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -38,8 +38,8 @@ component LoggedInMenu(user *arn.User) NavigationButton("Explore", "/explore", "th") - .extra-navigation - NavigationButton("Statistics", "/statistics", "pie-chart") + //- .extra-navigation + //- NavigationButton("Statistics", "/statistics", "pie-chart") NavigationButton("Settings", "/settings", "cog") From ac8aa301c1a9badae4a7bae21744524676834953 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 15:44:06 +0200 Subject: [PATCH 183/527] Added mappings --- jobs/sync-anime/sync-anime.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/jobs/sync-anime/sync-anime.go b/jobs/sync-anime/sync-anime.go index 6fc26f87..2936758b 100644 --- a/jobs/sync-anime/sync-anime.go +++ b/jobs/sync-anime/sync-anime.go @@ -14,7 +14,7 @@ func main() { color.Yellow("Syncing Anime") // Get a stream of all anime - allAnime := kitsu.StreamAnime() + allAnime := kitsu.StreamAnimeWithMappings() // Iterate over the stream for anime := range allAnime { @@ -77,6 +77,22 @@ func sync(data *kitsu.Anime) { anime.Title.Japanese = attr.Titles.JaJp } + // Import mappings + for _, mapping := range data.Mappings { + switch mapping.Attributes.ExternalSite { + case "myanimelist/anime": + anime.AddMapping("myanimelist/anime", mapping.Attributes.ExternalID, "") + case "anidb": + anime.AddMapping("anidb/anime", mapping.Attributes.ExternalID, "") + case "thetvdb/series": + anime.AddMapping("thetvdb/anime", mapping.Attributes.ExternalID, "") + case "thetvdb/season": + // Ignore + default: + color.Yellow("Unknown mapping: %s %s", mapping.Attributes.ExternalSite, mapping.Attributes.ExternalID) + } + } + // NSFW if attr.Nsfw { anime.NSFW = 1 From b9c64a52b3d0a603572ba45a19a8ca988ce467c7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 16:06:46 +0200 Subject: [PATCH 184/527] New statistics --- jobs/statistics/statistics.go | 52 +++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index 85ddf9a8..3c304d93 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -90,17 +90,21 @@ func getAnimeStats() []*arn.PieChart { shoboi := stats{} anilist := stats{} + mal := stats{} + anidb := stats{} status := stats{} types := stats{} shoboiEdits := stats{} anilistEdits := stats{} + malEdits := stats{} + anidbEdits := stats{} rating := stats{} for _, anime := range allAnime { for _, external := range anime.Mappings { if external.Service == "shoboi/anime" { if external.CreatedBy == "" { - shoboiEdits["Bot"]++ + shoboiEdits["(auto-generated)"]++ } else { user, err := arn.GetUser(external.CreatedBy) arn.PanicOnError(err) @@ -110,13 +114,33 @@ func getAnimeStats() []*arn.PieChart { if external.Service == "anilist/anime" { if external.CreatedBy == "" { - anilistEdits["Bot"]++ + anilistEdits["(auto-generated)"]++ } else { user, err := arn.GetUser(external.CreatedBy) arn.PanicOnError(err) anilistEdits[user.Nick]++ } } + + if external.Service == "myanimelist/anime" { + if external.CreatedBy == "" { + malEdits["(auto-generated)"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + malEdits[user.Nick]++ + } + } + + if external.Service == "anidb/anime" { + if external.CreatedBy == "" { + anidbEdits["(auto-generated)"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + anidbEdits[user.Nick]++ + } + } } if anime.GetMapping("shoboi/anime") != "" { @@ -126,9 +150,21 @@ func getAnimeStats() []*arn.PieChart { } if anime.GetMapping("anilist/anime") != "" { - anilist["Connected with Anilist"]++ + anilist["Connected with AniList"]++ } else { - anilist["Not connected with Anilist"]++ + anilist["Not connected with AniList"]++ + } + + if anime.GetMapping("myanimelist/anime") != "" { + mal["Connected with MyAnimeList"]++ + } else { + mal["Not connected with MyAnimeList"]++ + } + + if anime.GetMapping("anidb/anime") != "" { + anidb["Connected with AniDB"]++ + } else { + anidb["Not connected with AniDB"]++ } rating[arn.ToString(int(anime.Rating.Overall+0.5))]++ @@ -143,9 +179,13 @@ func getAnimeStats() []*arn.PieChart { arn.NewPieChart("Type", types), arn.NewPieChart("Status", status), arn.NewPieChart("Rating", rating), - arn.NewPieChart("Anilist", anilist), + arn.NewPieChart("MyAnimeList", mal), + arn.NewPieChart("AniList", anilist), + arn.NewPieChart("AniDB", anidb), arn.NewPieChart("Shoboi", shoboi), - arn.NewPieChart("Anilist Editors", anilistEdits), + // arn.NewPieChart("MyAnimeList Editors", malEdits), + arn.NewPieChart("AniList Editors", anilistEdits), + // arn.NewPieChart("AniDB Editors", anidbEdits), arn.NewPieChart("Shoboi Editors", shoboiEdits), } } From 17c94996016d0b36c2be61b447673eec84ad32dc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 17:16:40 +0200 Subject: [PATCH 185/527] Added timestamps to forum --- mixins/Postable.pixy | 2 + pages/anime/anime.pixy | 2 +- pages/animelist/animelist.pixy | 2 +- pages/dashboard/dashboard.pixy | 2 +- scripts/AnimeNotifier.ts | 8 +++- scripts/DateView.ts | 71 ++++++++++------------------------ styles/forum.scarlet | 12 ++++++ 7 files changed, 44 insertions(+), 55 deletions(-) diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index 65da2aab..77fdf3b4 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -22,6 +22,8 @@ component Postable(post arn.Postable, user *arn.User, highlightAuthorID string) a.button.post-cancel-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID()) Icon("close") span Cancel + + .post-date.utc-date(data-date=post.Created()) .post-toolbar(id="toolbar-" + post.ID()) .spacer diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 8dc41907..6461d1e6 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -161,7 +161,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis td.episode-actions a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") RawIcon("google") - td.episode-airing-date-start.utc-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() + td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() //- h3.anime-section-name Reviews //- p Coming soon. diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 0c300b61..5e59b7e1 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -55,7 +55,7 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical td.anime-list-item-airing-date if item.Anime().UpcomingEpisode() != nil - span.utc-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) + span.utc-airing-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) td.anime-list-item-episodes .anime-list-item-episodes-watched .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save")= item.Episodes diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index c0254df9..d0f557c1 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -13,7 +13,7 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound Icon("calendar-o") .schedule-item-title= schedule[i].Anime.Title.Canonical .spacer - .schedule-item-date.utc-date(data-start-date=schedule[i].Episode.AiringDate.Start, data-end-date=schedule[i].Episode.AiringDate.End, data-episode-number=schedule[i].Episode.Number) + .schedule-item-date.utc-airing-date(data-start-date=schedule[i].Episode.AiringDate.Start, data-end-date=schedule[i].Episode.AiringDate.End, data-episode-number=schedule[i].Episode.Number) else .widget-element .widget-element-text diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 06ba641d..3693693f 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -1,6 +1,6 @@ import { Application } from "./Application" import { Diff } from "./Diff" -import { displayLocalDate } from "./DateView" +import { displayAiringDate, displayDate } from "./DateView" import { findAll, delay } from "./Utils" import { MutationQueue } from "./MutationQueue" import * as actions from "./Actions" @@ -148,8 +148,12 @@ export class AnimeNotifier { displayLocalDates() { const now = new Date() + for(let element of findAll("utc-airing-date")) { + displayAiringDate(element, now) + } + for(let element of findAll("utc-date")) { - displayLocalDate(element, now) + displayDate(element, now) } } diff --git a/scripts/DateView.ts b/scripts/DateView.ts index 2c7360df..270e76d4 100644 --- a/scripts/DateView.ts +++ b/scripts/DateView.ts @@ -59,7 +59,7 @@ function getRemainingTime(remaining: number): string { return "Just now" } -export function displayLocalDate(element: HTMLElement, now: Date) { +export function displayAiringDate(element: HTMLElement, now: Date) { let startDate = new Date(element.dataset.startDate) let endDate = new Date(element.dataset.endDate) @@ -73,9 +73,6 @@ export function displayLocalDate(element: HTMLElement, now: Date) { let airingVerb = "will be airing" - - // let dayInfo = dayNames[startDate.getDay()] + ", " + monthNames[startDate.getMonth()] + " " + startDate.getDate() - let remaining = startDate.getTime() - now.getTime() let remainingString = getRemainingTime(remaining) @@ -86,55 +83,29 @@ export function displayLocalDate(element: HTMLElement, now: Date) { element.innerText = remainingString - // let remainingString = seconds + plural(seconds, 'second') - - // let days = seconds / (60 * ) - // if(Math.abs(days) >= 1) { - // remainingString = plural(days, 'day') - // } else { - // let hours = arn.inHours(now, timeStamp) - // if(Math.abs(hours) >= 1) { - // remainingString = plural(hours, 'hour') - // } else { - // let minutes = arn.inMinutes(now, timeStamp) - // if(Math.abs(minutes) >= 1) { - // remainingString = plural(minutes, 'minute') - // } else { - // let seconds = arn.inSeconds(now, timeStamp) - // remainingString = plural(seconds, 'second') - // } - // } - // } - - // if(isNaN(oneHour)) { - // element.style.opacity = "0" - // return - // } - - // switch(Math.floor(dayDifference)) { - // case 0: - // element.innerText = "Today" - // break - // case 1: - // element.innerText = "Tomorrow" - // break - // case -1: - // element.innerText = "Yesterday" - // break - // default: - // let text = Math.abs(dayDifference) + " days" - - // if(dayDifference < 0) { - // text += " ago" - // airingVerb = "aired" - // } else { - // element.innerText = text - // } - // } - if(remaining < 0) { airingVerb = "aired" } element.title = "Episode " + element.dataset.episodeNumber + " " + airingVerb + " " + dayNames[startDate.getDay()] + " from " + startTime + " - " + endTime +} + +export function displayDate(element: HTMLElement, now: Date) { + let startDate = new Date(element.dataset.date) + + let h = startDate.getHours() + let m = startDate.getMinutes() + let startTime = (h <= 9 ? "0" + h : h) + ":" + (m <= 9 ? "0" + m : m) + + let remaining = startDate.getTime() - now.getTime() + let remainingString = getRemainingTime(remaining) + + // Add "ago" if the date is in the past + if(remainingString.startsWith("-")) { + remainingString = remainingString.substring(1) + " ago" + } + + element.innerText = remainingString + + element.title = dayNames[startDate.getDay()] + " " + startTime } \ No newline at end of file diff --git a/styles/forum.scarlet b/styles/forum.scarlet index c882eb68..7ce5e863 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -41,6 +41,9 @@ post-content-padding-y = 0.75rem :hover .post-toolbar opacity 1 + + .post-date + opacity 0.25 .thread-content horizontal @@ -119,6 +122,15 @@ post-content-padding-y = 0.75rem .post-text-input min-height 200px +.post-date + position absolute + right -1rem + top 0.25rem + white-space nowrap + opacity 0 + transform translateX(100%) + default-transition + // Old // #posts From 343dfcb75b7cf81afa52c1641cb71076e9febe13 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 18:28:25 +0200 Subject: [PATCH 186/527] Improved shell commandline for anime sync --- jobs/sync-anime/shell.go | 50 +++++++++++++++++++++++++++++++++++ jobs/sync-anime/sync-anime.go | 10 ++++++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 jobs/sync-anime/shell.go diff --git a/jobs/sync-anime/shell.go b/jobs/sync-anime/shell.go new file mode 100644 index 00000000..96a3802d --- /dev/null +++ b/jobs/sync-anime/shell.go @@ -0,0 +1,50 @@ +package main + +import ( + "errors" + "flag" + + "github.com/animenotifier/arn" + "github.com/animenotifier/kitsu" + "github.com/fatih/color" +) + +// Shell parameters +var animeID string +var verbose bool + +// Shell flags +func init() { + flag.StringVar(&animeID, "id", "", "ID of the anime that you want to refresh") + flag.BoolVar(&verbose, "v", false, "Verbose output") + flag.Parse() +} + +// InvokeShellArgs ... +func InvokeShellArgs() bool { + if animeID != "" { + kitsuAnime, err := kitsu.GetAnime(animeID) + + if err != nil { + panic(err) + } + + if kitsuAnime.ID != animeID { + panic(errors.New("Anime ID is not the same")) + } + + anime := sync(kitsuAnime) + + if verbose { + color.Cyan("Kitsu:") + arn.PrettyPrint(kitsuAnime) + + color.Cyan("ARN:") + arn.PrettyPrint(anime) + } + + return true + } + + return false +} diff --git a/jobs/sync-anime/sync-anime.go b/jobs/sync-anime/sync-anime.go index 2936758b..65002a1b 100644 --- a/jobs/sync-anime/sync-anime.go +++ b/jobs/sync-anime/sync-anime.go @@ -13,6 +13,12 @@ import ( func main() { color.Yellow("Syncing Anime") + // In case we refresh only one anime + if InvokeShellArgs() { + color.Green("Finished.") + return + } + // Get a stream of all anime allAnime := kitsu.StreamAnimeWithMappings() @@ -24,7 +30,7 @@ func main() { color.Green("Finished.") } -func sync(data *kitsu.Anime) { +func sync(data *kitsu.Anime) *arn.Anime { anime, err := arn.GetAnime(data.ID) if err != nil { @@ -136,4 +142,6 @@ func sync(data *kitsu.Anime) { // Log fmt.Println(status, anime.ID, anime.Title.Canonical) + + return anime } From 93cfb471d93035c4980543236283b7b0068dfcb6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 7 Jul 2017 23:06:39 +0200 Subject: [PATCH 187/527] Cleanup --- pages/settings/settings.pixy | 2 +- styles/typography.scarlet | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 382dc1ce..300f5f4c 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -18,8 +18,8 @@ component Settings(user *arn.User) InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co") InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net") InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") - InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh") + //- InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") .widget.mountable h3.widget-title diff --git a/styles/typography.scarlet b/styles/typography.scarlet index 044737a4..f2590215 100644 --- a/styles/typography.scarlet +++ b/styles/typography.scarlet @@ -2,10 +2,10 @@ p, h1, h2, h3, h4, h5, h6 margin typography-margin 0 :first-child - margin-top 0 + margin-top 0 !important :last-child - margin-bottom 0 + margin-bottom 0 !important h1, h2 margin-top content-padding From f90522203d4ca55371dbe1a3be8bb08d5abadfbc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 01:18:21 +0200 Subject: [PATCH 188/527] MAL import preview --- main.go | 3 + pages/listimport/listimport.pixy | 17 ++-- .../anilist.scarlet => listimport.scarlet} | 3 + pages/listimport/listimportanilist/anilist.go | 67 +++++++++------- .../listimportmyanimelist/myanimelist.go | 79 +++++++++++++++++++ .../listimportmyanimelist/myanimelist.pixy | 23 ++++++ 6 files changed, 158 insertions(+), 34 deletions(-) rename pages/listimport/{listimportanilist/anilist.scarlet => listimport.scarlet} (64%) create mode 100644 pages/listimport/listimportmyanimelist/myanimelist.go create mode 100644 pages/listimport/listimportmyanimelist/myanimelist.pixy diff --git a/main.go b/main.go index e810fd01..31b25a84 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/listimport" "github.com/animenotifier/notify.moe/pages/listimport/listimportanilist" + "github.com/animenotifier/notify.moe/pages/listimport/listimportmyanimelist" "github.com/animenotifier/notify.moe/pages/login" "github.com/animenotifier/notify.moe/pages/music" "github.com/animenotifier/notify.moe/pages/newsoundtrack" @@ -91,6 +92,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/import", listimport.Get) app.Ajax("/import/anilist/animelist", listimportanilist.Preview) app.Ajax("/import/anilist/animelist/finish", listimportanilist.Finish) + app.Ajax("/import/myanimelist/animelist", listimportmyanimelist.Preview) + app.Ajax("/import/myanimelist/animelist/finish", listimportmyanimelist.Finish) app.Ajax("/admin", admin.Get) app.Ajax("/statistics", statistics.Get) app.Ajax("/statistics/anime", statistics.Anime) diff --git a/pages/listimport/listimport.pixy b/pages/listimport/listimport.pixy index 3a8e7d57..e237e2a7 100644 --- a/pages/listimport/listimport.pixy +++ b/pages/listimport/listimport.pixy @@ -1,8 +1,13 @@ component ImportLists(user *arn.User) - .buttons + .buttons.buttons-vertical if user.Accounts.AniList.Nick != "" - a.button.mountable.ajax(href="/import/anilist/animelist") - Icon("refresh") - span Import AniList Anime List - else - p No imports available. \ No newline at end of file + .widget-input + a.button.mountable.ajax(href="/import/anilist/animelist") + Icon("download") + span Import AniList + + if user.Accounts.MyAnimeList.Nick != "" + .widget-input + a.button.mountable.ajax(href="/import/myanimelist/animelist") + Icon("download") + span Import MyAnimeList \ No newline at end of file diff --git a/pages/listimport/listimportanilist/anilist.scarlet b/pages/listimport/listimport.scarlet similarity index 64% rename from pages/listimport/listimportanilist/anilist.scarlet rename to pages/listimport/listimport.scarlet index 34b92482..63b8d829 100644 --- a/pages/listimport/listimportanilist/anilist.scarlet +++ b/pages/listimport/listimport.scarlet @@ -1,3 +1,6 @@ +.buttons-vertical + width 100% + .import-list max-width table-width-normal margin 0 auto \ No newline at end of file diff --git a/pages/listimport/listimportanilist/anilist.go b/pages/listimport/listimportanilist/anilist.go index e3adb193..12f2e8dc 100644 --- a/pages/listimport/listimportanilist/anilist.go +++ b/pages/listimport/listimportanilist/anilist.go @@ -9,39 +9,14 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -func getMatches(ctx *aero.Context) ([]*arn.AniListMatch, string) { +// Preview shows an import preview. +func Preview(ctx *aero.Context) string { user := utils.GetUser(ctx) if user == nil { - return nil, ctx.Error(http.StatusBadRequest, "Not logged in", nil) + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) } - authErr := arn.AniList.Authorize() - - if authErr != nil { - return nil, ctx.Error(http.StatusBadRequest, "Couldn't authorize the Anime Notifier app on AniList", authErr) - } - - allAnime, allErr := arn.AllAnime() - - if allErr != nil { - return nil, ctx.Error(http.StatusBadRequest, "Couldn't load notify.moe list of all anime", allErr) - } - - anilistAnimeList, err := arn.AniList.GetAnimeList(user) - - if err != nil { - return nil, ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from AniList", err) - } - - matches := findAllMatches(allAnime, anilistAnimeList) - - return matches, "" -} - -// Preview ... -func Preview(ctx *aero.Context) string { - user := utils.GetUser(ctx) matches, response := getMatches(ctx) if response != "" { @@ -54,6 +29,11 @@ func Preview(ctx *aero.Context) string { // Finish ... func Finish(ctx *aero.Context) string { user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + matches, response := getMatches(ctx) if response != "" { @@ -92,6 +72,37 @@ func Finish(ctx *aero.Context) string { return ctx.Redirect("/+" + user.Nick + "/animelist") } +// getMatches finds and returns all matches for the logged in user. +func getMatches(ctx *aero.Context) ([]*arn.AniListMatch, string) { + user := utils.GetUser(ctx) + + if user == nil { + return nil, ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + authErr := arn.AniList.Authorize() + + if authErr != nil { + return nil, ctx.Error(http.StatusBadRequest, "Couldn't authorize the Anime Notifier app on AniList", authErr) + } + + allAnime, allErr := arn.AllAnime() + + if allErr != nil { + return nil, ctx.Error(http.StatusBadRequest, "Couldn't load notify.moe list of all anime", allErr) + } + + anilistAnimeList, err := arn.AniList.GetAnimeList(user) + + if err != nil { + return nil, ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from AniList", err) + } + + matches := findAllMatches(allAnime, anilistAnimeList) + + return matches, "" +} + // findAllMatches returns all matches for the anime inside an anilist anime list. func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*arn.AniListMatch { matches := []*arn.AniListMatch{} diff --git a/pages/listimport/listimportmyanimelist/myanimelist.go b/pages/listimport/listimportmyanimelist/myanimelist.go new file mode 100644 index 00000000..20050750 --- /dev/null +++ b/pages/listimport/listimportmyanimelist/myanimelist.go @@ -0,0 +1,79 @@ +package listimportmyanimelist + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/mal" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Preview shows an import preview. +func Preview(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + matches, response := getMatches(ctx) + + if response != "" { + return response + } + + return ctx.HTML(components.ImportMyAnimeList(user, matches)) +} + +// Finish ... +func Finish(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + return ctx.Redirect("/+" + user.Nick + "/animelist") +} + +// getMatches finds and returns all matches for the logged in user. +func getMatches(ctx *aero.Context) ([]*arn.MyAnimeListMatch, string) { + user := utils.GetUser(ctx) + + if user == nil { + return nil, ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + malAnimeList, err := mal.GetAnimeList(user.Accounts.MyAnimeList.Nick) + + if err != nil { + return nil, ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from MyAnimeList", err) + } + + matches := findAllMatches(malAnimeList) + + return matches, "" +} + +// findAllMatches returns all matches for the anime inside an anilist anime list. +func findAllMatches(animeList *mal.AnimeList) []*arn.MyAnimeListMatch { + matches := []*arn.MyAnimeListMatch{} + + for _, item := range animeList.Items { + var anime *arn.Anime + connection, err := arn.GetMyAnimeListToAnime(item.AnimeID) + + if err == nil { + anime, _ = arn.GetAnime(connection.AnimeID) + } + + matches = append(matches, &arn.MyAnimeListMatch{ + MyAnimeListItem: item, + ARNAnime: anime, + }) + } + + return matches +} diff --git a/pages/listimport/listimportmyanimelist/myanimelist.pixy b/pages/listimport/listimportmyanimelist/myanimelist.pixy new file mode 100644 index 00000000..2908e126 --- /dev/null +++ b/pages/listimport/listimportmyanimelist/myanimelist.pixy @@ -0,0 +1,23 @@ +component ImportMyAnimeList(user *arn.User, matches []*arn.MyAnimeListMatch) + h1= "myanimelist.net Import (" + user.Accounts.MyAnimeList.Nick + ", " + toString(len(matches)) + " anime)" + + table.import-list + thead + tr + th myanimelist.net + th notify.moe + tbody + each match in matches + tr + td + a(href=match.MyAnimeListItem.AnimeLink(), target="_blank", rel="noopener")= match.MyAnimeListItem.AnimeTitle + td + if match.ARNAnime == nil + span.import-error Not found on notify.moe + else + a(href=match.ARNAnime.Link(), target="_blank", rel="noopener")= match.ARNAnime.Title.Canonical + + .buttons + a.button.mountable(href="/import/myanimelist/animelist/finish") + Icon("refresh") + span Import \ No newline at end of file From 2edfd91338e9abc8cbd0b3289877edf6b72db8e1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 01:50:15 +0200 Subject: [PATCH 189/527] Add MAL connections --- .../add-mal-connections.go | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 patches/add-mal-connections/add-mal-connections.go diff --git a/patches/add-mal-connections/add-mal-connections.go b/patches/add-mal-connections/add-mal-connections.go new file mode 100644 index 00000000..b9bf2596 --- /dev/null +++ b/patches/add-mal-connections/add-mal-connections.go @@ -0,0 +1,35 @@ +package main + +import ( + "strconv" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + for anime := range arn.MustStreamAnime() { + malID := anime.GetMapping("myanimelist/anime") + + if malID == "" { + continue + } + + // Assure the string represents a number + malNum, _ := strconv.Atoi(malID) + normalizedID := strconv.Itoa(malNum) + + if malID != normalizedID { + color.Red("%s does not match %d", malID, normalizedID) + continue + } + + // Save + arn.PanicOnError(arn.DB.Set("MyAnimeListToAnime", malID, &arn.MyAnimeListToAnime{ + AnimeID: anime.ID, + ServiceID: malID, + Edited: arn.DateTimeUTC(), + EditedBy: "", + })) + } +} From 0a51b64e88fa6c2f7aea43b3804eabdc5927e40d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 02:41:13 +0200 Subject: [PATCH 190/527] Finished MAL import --- .../listimportmyanimelist/myanimelist.go | 44 +++++++++++++++++++ tests.go | 28 ++++++------ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/pages/listimport/listimportmyanimelist/myanimelist.go b/pages/listimport/listimportmyanimelist/myanimelist.go index 20050750..7b816e37 100644 --- a/pages/listimport/listimportmyanimelist/myanimelist.go +++ b/pages/listimport/listimportmyanimelist/myanimelist.go @@ -2,6 +2,7 @@ package listimportmyanimelist import ( "net/http" + "strconv" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -35,6 +36,49 @@ func Finish(ctx *aero.Context) string { return ctx.Error(http.StatusBadRequest, "Not logged in", nil) } + matches, response := getMatches(ctx) + + if response != "" { + return response + } + + animeList := user.AnimeList() + + for _, match := range matches { + if match.ARNAnime == nil || match.MyAnimeListItem == nil { + continue + } + + rating, _ := strconv.ParseFloat(match.MyAnimeListItem.MyScore, 64) + episodesWatched, _ := strconv.Atoi(match.MyAnimeListItem.MyWatchedEpisodes) + rewatchCount, convErr := strconv.Atoi(match.MyAnimeListItem.MyRewatching) + + if convErr != nil { + rewatchCount = 0 + } + + item := &arn.AnimeListItem{ + AnimeID: match.ARNAnime.ID, + Status: arn.MyAnimeListStatusToARNStatus(match.MyAnimeListItem.MyStatus), + Episodes: episodesWatched, + Notes: "", + Rating: &arn.AnimeRating{ + Overall: rating, + }, + RewatchCount: rewatchCount, + Created: arn.DateTimeUTC(), + Edited: arn.DateTimeUTC(), + } + + animeList.Import(item) + } + + err := animeList.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving your anime list", err) + } + return ctx.Redirect("/+" + user.Nick + "/animelist") } diff --git a/tests.go b/tests.go index f26ca750..ae7db0ed 100644 --- a/tests.go +++ b/tests.go @@ -174,19 +174,21 @@ var routeTests = map[string][]string{ }, // Disable these tests because they require authorization - "/auth/google": nil, - "/auth/google/callback": nil, - "/auth/facebook": nil, - "/auth/facebook/callback": nil, - "/import": nil, - "/import/anilist/animelist": nil, - "/import/anilist/animelist/finish": nil, - "/anime/:id/edit": nil, - "/new/thread": nil, - "/new/soundtrack": nil, - "/user": nil, - "/settings": nil, - "/extension/embed": nil, + "/auth/google": nil, + "/auth/google/callback": nil, + "/auth/facebook": nil, + "/auth/facebook/callback": nil, + "/import": nil, + "/import/anilist/animelist": nil, + "/import/anilist/animelist/finish": nil, + "/import/myanimelist/animelist": nil, + "/import/myanimelist/animelist/finish": nil, + "/anime/:id/edit": nil, + "/new/thread": nil, + "/new/soundtrack": nil, + "/user": nil, + "/settings": nil, + "/extension/embed": nil, } // API interfaces From c729d9d3bab51bfdea62ca7214d9cc49544fcf74 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 15:40:13 +0200 Subject: [PATCH 191/527] WebP bridge is working again --- assets.go | 39 ++---------------------- jobs/active-users/active-users.go | 2 +- jobs/avatars/Avatar.go | 39 ++++++++++++++++++------ jobs/avatars/AvatarOriginalFileOutput.go | 14 +++------ jobs/avatars/Gravatar.go | 2 ++ jobs/avatars/avatars.go | 33 ++++++++++---------- mixins/Avatar.pixy | 2 +- mixins/ProfileImage.pixy | 2 +- scripts/AnimeNotifier.ts | 14 +++++++-- scripts/Utils.ts | 14 ++++++++- 10 files changed, 84 insertions(+), 77 deletions(-) diff --git a/assets.go b/assets.go index 1c312697..8f3fc1e1 100644 --- a/assets.go +++ b/assets.go @@ -1,12 +1,9 @@ package main import ( - "errors" - "net/http" "strings" "github.com/aerogo/aero" - "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components/js" ) @@ -53,44 +50,12 @@ func init() { // Avatars app.Get("/images/avatars/large/:file", func(ctx *aero.Context) string { - file := strings.TrimSuffix(ctx.Get("file"), ".webp") - - if ctx.CanUseWebP() { - return ctx.File("images/avatars/large/webp/" + file + ".webp") - } - - original := arn.FindFileWithExtension( - file, - "images/avatars/large/original/", - arn.OriginalImageExtensions, - ) - - if original == "" { - return ctx.Error(http.StatusNotFound, "Avatar not found", errors.New("Image not found: "+file)) - } - - return ctx.File(original) + return ctx.File("images/avatars/large/" + ctx.Get("file")) }) // Avatars app.Get("/images/avatars/small/:file", func(ctx *aero.Context) string { - file := strings.TrimSuffix(ctx.Get("file"), ".webp") - - if ctx.CanUseWebP() { - return ctx.File("images/avatars/small/webp/" + file + ".webp") - } - - original := arn.FindFileWithExtension( - file, - "images/avatars/small/original/", - arn.OriginalImageExtensions, - ) - - if original == "" { - return ctx.Error(http.StatusNotFound, "Avatar not found", errors.New("Image not found: "+file)) - } - - return ctx.File(original) + return ctx.File("images/avatars/large/" + ctx.Get("file")) }) // Elements diff --git a/jobs/active-users/active-users.go b/jobs/active-users/active-users.go index 8c520285..ae99e4b4 100644 --- a/jobs/active-users/active-users.go +++ b/jobs/active-users/active-users.go @@ -15,7 +15,7 @@ func main() { // Filter out active users with an avatar users, err := arn.FilterUsers(func(user *arn.User) bool { - return user.IsActive() && user.Avatar != "" + return user.IsActive() && user.AvatarExtension != "" }) if err != nil { diff --git a/jobs/avatars/Avatar.go b/jobs/avatars/Avatar.go index 25c5b21a..f1748c37 100644 --- a/jobs/avatars/Avatar.go +++ b/jobs/avatars/Avatar.go @@ -5,6 +5,7 @@ import ( "fmt" "image" "net/http" + "strings" "time" "github.com/animenotifier/arn" @@ -21,6 +22,20 @@ type Avatar struct { Format string } +// Extension ... +func (avatar *Avatar) Extension() string { + switch avatar.Format { + case "jpg", "jpeg": + return ".jpg" + case "png": + return ".png" + case "gif": + return ".gif" + default: + return "" + } +} + // String returns a text representation of the format, width and height. func (avatar *Avatar) String() string { return fmt.Sprint(avatar.Format, " | ", avatar.Image.Bounds().Dx(), "x", avatar.Image.Bounds().Dy()) @@ -29,17 +44,23 @@ func (avatar *Avatar) String() string { // AvatarFromURL downloads and decodes the image from an URL and creates an Avatar. func AvatarFromURL(url string, user *arn.User) *Avatar { // Download - response, data, networkErr := gorequest.New().Get(url).EndBytes() - - // Retry after 5 seconds if service unavailable - if response.StatusCode == http.StatusServiceUnavailable { - time.Sleep(5 * time.Second) - response, data, networkErr = gorequest.New().Get(url).EndBytes() - } + response, data, networkErrs := gorequest.New().Get(url).EndBytes() // Network errors - if networkErr != nil { - netLog.Error(user.Nick, url, networkErr) + if len(networkErrs) > 0 { + netLog.Error(user.Nick, url, networkErrs[0]) + return nil + } + + // Retry HTTP only version after 5 seconds if service unavailable + if response == nil || response.StatusCode == http.StatusServiceUnavailable { + time.Sleep(5 * time.Second) + response, data, networkErrs = gorequest.New().Get(strings.Replace(url, "https://", "http://", 1)).EndBytes() + } + + // Network errors on 2nd try + if len(networkErrs) > 0 { + netLog.Error(user.Nick, url, networkErrs[0]) return nil } diff --git a/jobs/avatars/AvatarOriginalFileOutput.go b/jobs/avatars/AvatarOriginalFileOutput.go index 8bb6544c..5232ef6b 100644 --- a/jobs/avatars/AvatarOriginalFileOutput.go +++ b/jobs/avatars/AvatarOriginalFileOutput.go @@ -20,16 +20,9 @@ type AvatarOriginalFileOutput struct { // SaveAvatar writes the original avatar to the file system. func (output *AvatarOriginalFileOutput) SaveAvatar(avatar *Avatar) error { // Determine file extension - extension := "" + extension := avatar.Extension() - switch avatar.Format { - case "jpg", "jpeg": - extension = ".jpg" - case "png": - extension = ".png" - case "gif": - extension = ".gif" - default: + if extension == "" { return errors.New("Unknown format: " + avatar.Format) } @@ -58,6 +51,9 @@ func (output *AvatarOriginalFileOutput) SaveAvatar(avatar *Avatar) error { data = buffer.Bytes() } + // Set user avatar + avatar.User.AvatarExtension = extension + // Write to file fileName := output.Directory + avatar.User.ID + extension return ioutil.WriteFile(fileName, data, 0644) diff --git a/jobs/avatars/Gravatar.go b/jobs/avatars/Gravatar.go index 2ec685ce..731d863f 100644 --- a/jobs/avatars/Gravatar.go +++ b/jobs/avatars/Gravatar.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "time" "github.com/animenotifier/arn" @@ -26,6 +27,7 @@ func (source *Gravatar) GetAvatar(user *arn.User) *Avatar { // Build URL gravatarURL := gravatar.Url(user.Email) + "?s=" + fmt.Sprint(arn.AvatarMaxSize) + "&d=404&r=" + source.Rating + gravatarURL = strings.Replace(gravatarURL, "http://", "https://", 1) // Wait for request limiter to allow us to send a request <-source.RequestLimiter.C diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index c6a76bbc..6f545413 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -58,26 +58,26 @@ func main() { avatarOutputs = []AvatarOutput{ // Original - Large &AvatarOriginalFileOutput{ - Directory: "images/avatars/large/original/", + Directory: "images/avatars/large/", Size: arn.AvatarMaxSize, }, // Original - Small &AvatarOriginalFileOutput{ - Directory: "images/avatars/small/original/", + Directory: "images/avatars/small/", Size: arn.AvatarSmallSize, }, // WebP - Large &AvatarWebPFileOutput{ - Directory: "images/avatars/large/webp/", + Directory: "images/avatars/large/", Size: arn.AvatarMaxSize, Quality: webPQuality, }, // WebP - Small &AvatarWebPFileOutput{ - Directory: "images/avatars/small/webp/", + Directory: "images/avatars/small/", Size: arn.AvatarSmallSize, Quality: webPQuality, }, @@ -87,20 +87,12 @@ func main() { return } - // Stream of all users - users, _ := arn.FilterUsers(func(user *arn.User) bool { - return true - }) - - // Log user count - println(len(users), "users") - // Worker queue - usersQueue := make(chan *arn.User) + usersQueue := make(chan *arn.User, 512) StartWorkers(usersQueue, Work) // We'll send each user to one of the worker threads - for _, user := range users { + for user := range arn.MustStreamUsers() { usersQueue <- user } @@ -120,7 +112,7 @@ func StartWorkers(queue chan *arn.User, work func(*arn.User)) { // Work handles a single user. func Work(user *arn.User) { - user.Avatar = "" + user.AvatarExtension = "" for _, source := range avatarSources { avatar := source.GetAvatar(user) @@ -139,10 +131,19 @@ func Work(user *arn.User) { } fmt.Println(color.GreenString("✔"), reflect.TypeOf(source).Elem().Name(), "|", user.Nick, "|", avatar) - user.Avatar = "/+" + user.Nick + "/avatar" break } + // Since this a very long running job, refresh user data before saving it. + avatarExt := user.AvatarExtension + user, err := arn.GetUser(user.ID) + + if err != nil { + avatarLog.Error("Can't refresh user info:", user.ID, user.Nick) + return + } + // Save avatar data + user.AvatarExtension = avatarExt user.Save() } diff --git a/mixins/Avatar.pixy b/mixins/Avatar.pixy index b3f9a103..74bfd4d4 100644 --- a/mixins/Avatar.pixy +++ b/mixins/Avatar.pixy @@ -4,7 +4,7 @@ component Avatar(user *arn.User) component AvatarNoLink(user *arn.User) if user.HasAvatar() - img.user-image.lazy(data-src=user.SmallAvatar(), alt=user.Nick) + img.user-image.lazy(data-src=user.SmallAvatar(), data-webp="true", alt=user.Nick) else SVGAvatar diff --git a/mixins/ProfileImage.pixy b/mixins/ProfileImage.pixy index dc06f415..9b7a7735 100644 --- a/mixins/ProfileImage.pixy +++ b/mixins/ProfileImage.pixy @@ -1,6 +1,6 @@ component ProfileImage(user *arn.User) if user.HasAvatar() - img.profile-image(src=user.LargeAvatar(), alt="Profile image") + img.profile-image.lazy(data-src=user.LargeAvatar(), data-webp="true", alt="Profile image") else svg.profile-image(viewBox="0 0 50 50", alt="Profile image") circle.head(cx="25", cy="19", r="10") diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 3693693f..c8e32f73 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -1,7 +1,7 @@ import { Application } from "./Application" import { Diff } from "./Diff" import { displayAiringDate, displayDate } from "./DateView" -import { findAll, delay } from "./Utils" +import { findAll, delay, canUseWebP } from "./Utils" import { MutationQueue } from "./MutationQueue" import * as actions from "./Actions" @@ -9,6 +9,7 @@ export class AnimeNotifier { app: Application user: HTMLElement title: string + webpEnabled: boolean visibilityObserver: IntersectionObserver imageFound: MutationQueue @@ -80,6 +81,9 @@ export class AnimeNotifier { document.documentElement.classList.add("osx") } + // Check for WebP support + this.webpEnabled = canUseWebP() + // Initiate the elements we need this.user = this.app.find("user") this.app.content = this.app.find("content") @@ -207,7 +211,13 @@ export class AnimeNotifier { lazyLoadImage(img: HTMLImageElement) { // Once the image becomes visible, load it img["became visible"] = () => { - img.src = img.dataset.src + // Replace URL with WebP if supported + if(this.webpEnabled && img.dataset.webp) { + let dot = img.dataset.src.lastIndexOf(".") + img.src = img.dataset.src.substring(0, dot) + ".webp" + } else { + img.src = img.dataset.src + } if(img.naturalWidth === 0) { img.onload = () => { diff --git a/scripts/Utils.ts b/scripts/Utils.ts index b349dc84..17ad8d01 100644 --- a/scripts/Utils.ts +++ b/scripts/Utils.ts @@ -11,5 +11,17 @@ export function delay(millis: number, value?: T): Promise { } export function plural(count: number, singular: string): string { - return (count === 1 || count === -1) ? (count + ' ' + singular) : (count + ' ' + singular + 's') + return (count === 1 || count === -1) ? (count + " " + singular) : (count + " " + singular + "s") +} + +export function canUseWebP(): boolean { + let canvas = document.createElement("canvas") + + if(!!(canvas.getContext && canvas.getContext("2d"))) { + // WebP representation possible + return canvas.toDataURL("image/webp").indexOf("data:image/webp") === 0 + } else { + // In very old browsers (IE 8) canvas is not supported + return false + } } \ No newline at end of file From 3eaf1c714e1257f2191183fb2825940daae97b47 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 16:10:22 +0200 Subject: [PATCH 192/527] Fixed avatar downloader --- jobs/avatars/avatars.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index 6f545413..58865834 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -6,6 +6,7 @@ import ( "path" "reflect" "runtime" + "sync" "time" _ "image/gif" @@ -24,6 +25,7 @@ const ( var avatarSources []AvatarSource var avatarOutputs []AvatarOutput var avatarLog = log.New() +var wg sync.WaitGroup // Main func main() { @@ -88,14 +90,17 @@ func main() { } // Worker queue - usersQueue := make(chan *arn.User, 512) + usersQueue := make(chan *arn.User, runtime.NumCPU()) StartWorkers(usersQueue, Work) // We'll send each user to one of the worker threads for user := range arn.MustStreamUsers() { + wg.Add(1) usersQueue <- user } + wg.Wait() + color.Green("Finished.") } @@ -112,6 +117,7 @@ func StartWorkers(queue chan *arn.User, work func(*arn.User)) { // Work handles a single user. func Work(user *arn.User) { + fmt.Println(user.ID, "|", user.Nick) user.AvatarExtension = "" for _, source := range avatarSources { From 220cfb5750ec922340910b1b5f7e4b97e7741789 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 16:19:31 +0200 Subject: [PATCH 193/527] Avatar downloader update --- jobs/avatars/avatars.go | 1 + 1 file changed, 1 insertion(+) diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index 58865834..c22c9377 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -110,6 +110,7 @@ func StartWorkers(queue chan *arn.User, work func(*arn.User)) { go func() { for user := range queue { work(user) + wg.Done() } }() } From 567d146b0e1ebcd420ded54e3654083ffba9006f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 16:32:17 +0200 Subject: [PATCH 194/527] Added avatar check --- .../delete-invalid-avatars.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 patches/delete-invalid-avatars/delete-invalid-avatars.go diff --git a/patches/delete-invalid-avatars/delete-invalid-avatars.go b/patches/delete-invalid-avatars/delete-invalid-avatars.go new file mode 100644 index 00000000..58360437 --- /dev/null +++ b/patches/delete-invalid-avatars/delete-invalid-avatars.go @@ -0,0 +1,17 @@ +package main + +import ( + "strings" + + "github.com/animenotifier/arn" +) + +func main() { + for user := range arn.MustStreamUsers() { + if !strings.HasPrefix(user.AvatarExtension, ".") { + user.AvatarExtension = "" + } + + user.Save() + } +} From c910e82a0aaa0886d06a6c6cb6e9568a6504a6a6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 17:31:04 +0200 Subject: [PATCH 195/527] Improved avatar generator --- jobs/avatars/FileSystem.go | 48 ++++++++++++++++++++++++++++++++++++++ jobs/avatars/avatars.go | 13 +++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 jobs/avatars/FileSystem.go diff --git a/jobs/avatars/FileSystem.go b/jobs/avatars/FileSystem.go new file mode 100644 index 00000000..b97b1363 --- /dev/null +++ b/jobs/avatars/FileSystem.go @@ -0,0 +1,48 @@ +package main + +import ( + "bytes" + "image" + "io/ioutil" + + "github.com/animenotifier/arn" +) + +var fileSystemLog = avatarLog.NewChannel("SSD") + +// FileSystem loads avatar from the local filesystem. +type FileSystem struct { + Directory string +} + +// GetAvatar returns the local image for the user. +func (source *FileSystem) GetAvatar(user *arn.User) *Avatar { + fullPath := arn.FindFileWithExtension(user.ID, source.Directory, arn.OriginalImageExtensions) + + if fullPath == "" { + fileSystemLog.Error(user.Nick, "Not found on file system") + return nil + } + + data, err := ioutil.ReadFile(fullPath) + + if err != nil { + fileSystemLog.Error(user.Nick, err) + return nil + } + + // Decode + img, format, decodeErr := image.Decode(bytes.NewReader(data)) + + if decodeErr != nil { + fileSystemLog.Error(user.Nick, decodeErr) + return nil + } + + return &Avatar{ + User: user, + Image: img, + Data: data, + Format: format, + } +} diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index c22c9377..277106e7 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -54,6 +54,9 @@ func main() { &MyAnimeList{ RequestLimiter: time.NewTicker(250 * time.Millisecond), }, + &FileSystem{ + Directory: "images/avatars/large/", + }, } // Define the avatar outputs @@ -118,7 +121,6 @@ func StartWorkers(queue chan *arn.User, work func(*arn.User)) { // Work handles a single user. func Work(user *arn.User) { - fmt.Println(user.ID, "|", user.Nick) user.AvatarExtension = "" for _, source := range avatarSources { @@ -129,6 +131,13 @@ func Work(user *arn.User) { continue } + sourceType := reflect.TypeOf(source).Elem().Name() + + // Avoid quality loss (if it's on the file system, we don't need to write it again) + if sourceType == "FileSystem" { + continue + } + for _, writer := range avatarOutputs { err := writer.SaveAvatar(avatar) @@ -137,7 +146,7 @@ func Work(user *arn.User) { } } - fmt.Println(color.GreenString("✔"), reflect.TypeOf(source).Elem().Name(), "|", user.Nick, "|", avatar) + fmt.Println(color.GreenString("✔"), sourceType, "|", user.Nick, "|", avatar) break } From 0be7eca8cd95fa333e1b858eb23b51bc6e50dc87 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 17:34:21 +0200 Subject: [PATCH 196/527] Improved avatar generator --- jobs/avatars/avatars.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index 277106e7..f7516162 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -131,11 +131,16 @@ func Work(user *arn.User) { continue } + // Name of source sourceType := reflect.TypeOf(source).Elem().Name() + // Log + fmt.Println(color.GreenString("✔"), sourceType, "|", user.Nick, "|", avatar) + // Avoid quality loss (if it's on the file system, we don't need to write it again) if sourceType == "FileSystem" { - continue + user.AvatarExtension = avatar.Extension() + break } for _, writer := range avatarOutputs { @@ -146,7 +151,6 @@ func Work(user *arn.User) { } } - fmt.Println(color.GreenString("✔"), sourceType, "|", user.Nick, "|", avatar) break } From 7579c52188811c1f8c5a1ffc482ed45efaab1b7d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 21:11:03 +0200 Subject: [PATCH 197/527] Editor page --- main.go | 40 +++++++++++++++++++---------- pages/anime/anime.pixy | 4 +-- pages/anime/episode.scarlet | 2 +- pages/editor/editor.go | 27 +++++++++++++++++++ pages/editor/editor.pixy | 14 ++++++++++ pages/listimport/listimport.scarlet | 6 +---- styles/table.scarlet | 2 ++ 7 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 pages/editor/editor.go create mode 100644 pages/editor/editor.pixy diff --git a/main.go b/main.go index 31b25a84..a52e76dd 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/animenotifier/notify.moe/pages/best" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" + "github.com/animenotifier/notify.moe/pages/editor" "github.com/animenotifier/notify.moe/pages/embed" "github.com/animenotifier/notify.moe/pages/explore" "github.com/animenotifier/notify.moe/pages/forum" @@ -73,6 +74,14 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/threads/:id", threads.Get) app.Ajax("/posts/:id", posts.Get) app.Ajax("/tracks/:id", tracks.Get) + app.Ajax("/new/thread", newthread.Get) + app.Ajax("/new/soundtrack", newsoundtrack.Get) + app.Ajax("/settings", settings.Get) + app.Ajax("/music", music.Get) + app.Ajax("/users", users.Get) + app.Ajax("/login", login.Get) + + // User profiles app.Ajax("/user", user.Get) app.Ajax("/user/:nick", profile.Get) app.Ajax("/user/:nick/threads", profile.GetThreadsByUser) @@ -85,27 +94,32 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/animelist/hold", animelist.FilterByStatus(arn.AnimeListStatusHold)) app.Ajax("/user/:nick/animelist/dropped", animelist.FilterByStatus(arn.AnimeListStatusDropped)) app.Ajax("/user/:nick/animelist/anime/:id", animelistitem.Get) - app.Ajax("/new/thread", newthread.Get) - app.Ajax("/new/soundtrack", newsoundtrack.Get) - app.Ajax("/settings", settings.Get) - app.Ajax("/music", music.Get) + + // Search + app.Ajax("/search", search.Get) + app.Ajax("/search/:term", search.Get) + + // Admin + app.Ajax("/admin", admin.Get) + app.Ajax("/editor", editor.Get) + app.Ajax("/statistics", statistics.Get) + app.Ajax("/statistics/anime", statistics.Anime) + app.Ajax("/webdev", webdev.Get) + + // Import app.Ajax("/import", listimport.Get) app.Ajax("/import/anilist/animelist", listimportanilist.Preview) app.Ajax("/import/anilist/animelist/finish", listimportanilist.Finish) app.Ajax("/import/myanimelist/animelist", listimportmyanimelist.Preview) app.Ajax("/import/myanimelist/animelist/finish", listimportmyanimelist.Finish) - app.Ajax("/admin", admin.Get) - app.Ajax("/statistics", statistics.Get) - app.Ajax("/statistics/anime", statistics.Anime) - app.Ajax("/search", search.Get) - app.Ajax("/search/:term", search.Get) - app.Ajax("/users", users.Get) - app.Ajax("/login", login.Get) - app.Ajax("/webdev", webdev.Get) - app.Ajax("/extension/embed", embed.Get) + + // Genres // app.Ajax("/genres", genres.Get) // app.Ajax("/genres/:name", genre.Get) + // Browser extension + app.Ajax("/extension/embed", embed.Get) + // Middleware app.Use(middleware.Log()) app.Use(middleware.Session()) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 6461d1e6..d2f31656 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -152,8 +152,8 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis h3.anime-section-name Latest episodes else h3.anime-section-name Episodes - table - tbody.episodes + table.episodes + tbody each episode in anime.Episodes tr.episode td.episode-number= episode.Number diff --git a/pages/anime/episode.scarlet b/pages/anime/episode.scarlet index f8353360..2e06b9d9 100644 --- a/pages/anime/episode.scarlet +++ b/pages/anime/episode.scarlet @@ -1,5 +1,5 @@ .episodes - // + max-width 100% .episode horizontal diff --git a/pages/editor/editor.go b/pages/editor/editor.go new file mode 100644 index 00000000..73de1f37 --- /dev/null +++ b/pages/editor/editor.go @@ -0,0 +1,27 @@ +package editor + +import ( + "net/http" + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +// Get ... +func Get(ctx *aero.Context) string { + missing, err := arn.FilterAnime(func(anime *arn.Anime) bool { + return anime.GetMapping("anilist/anime") == "" + }) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Couldn't filter anime", err) + } + + sort.Slice(missing, func(i, j int) bool { + return missing[i].StartDate > missing[j].StartDate + }) + + return ctx.HTML(components.AniListMissingMapping(missing)) +} diff --git a/pages/editor/editor.pixy b/pages/editor/editor.pixy new file mode 100644 index 00000000..f9a1cd52 --- /dev/null +++ b/pages/editor/editor.pixy @@ -0,0 +1,14 @@ +component AniListMissingMapping(missing []*arn.Anime) + h1 Anime without Anilist links + + table + tbody + each anime in missing + tr + td + if len(anime.StartDate) >= 4 + span= anime.StartDate[:4] + td + a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical + td + a(href="https://anilist.co/search?type=anime&q=" + anime.Title.Canonical, target="_blank", rel="noopener") Search diff --git a/pages/listimport/listimport.scarlet b/pages/listimport/listimport.scarlet index 63b8d829..5f42a32f 100644 --- a/pages/listimport/listimport.scarlet +++ b/pages/listimport/listimport.scarlet @@ -1,6 +1,2 @@ .buttons-vertical - width 100% - -.import-list - max-width table-width-normal - margin 0 auto \ No newline at end of file + width 100% \ No newline at end of file diff --git a/styles/table.scarlet b/styles/table.scarlet index 95095801..713df6bc 100644 --- a/styles/table.scarlet +++ b/styles/table.scarlet @@ -1,5 +1,7 @@ table width 100% + max-width table-width-normal + margin 0 auto tr border-bottom-width 1px From 1f4dc0a05ded595eb798a5df4b163613b68add04 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 23:27:24 +0200 Subject: [PATCH 198/527] Implemented forum likes --- mixins/Postable.pixy | 15 +++++++-------- scripts/Actions.ts | 18 ++++++++++++++++++ scripts/AnimeNotifier.ts | 27 ++++++++++++++++++++------- scripts/Diff.ts | 4 ++-- tests.go | 4 ++++ 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index 77fdf3b4..6df39fff 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -30,14 +30,13 @@ component Postable(post arn.Postable, user *arn.User, highlightAuthorID string) .post-likes(id="likes-" + post.ID(), title="Likes")= len(post.Likes()) if user != nil - //- if user.ID !== post.authorId - //- - var liked = post.likes && post.likes.indexOf(user.ID) !== -1 - - //- a.post-tool.post-like(id="like-" + post.ID, onclick=`$.like("${type.toLowerCase()}", "${post.ID}")`, title="Like", class=liked ? "hidden" : ") - //- i.fa.fa-thumbs-up.fa-fw - - //- a.post-tool.post-unlike(id="unlike-" + post.ID, onclick=`$.unlike("${type.toLowerCase()}", "${post.ID}")`, title="Unlike", class=!liked ? "hidden" : ") - //- i.fa.fa-thumbs-down.fa-fw + if user.ID != post.Author().ID + if post.LikedBy(user.ID) + a.post-tool.post-unlike.action(id="unlike-" + post.ID(), title="Unlike", data-action="unlike", data-trigger="click") + RawIcon("thumbs-down") + else + a.post-tool.post-like.action(id="like-" + post.ID(), title="Like", data-action="like", data-trigger="click") + RawIcon("thumbs-up") if user.ID == post.Author().ID a.post-tool.post-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID(), title="Edit") diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 6e437977..917ebde7 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -115,6 +115,24 @@ export function savePost(arn: AnimeNotifier, element: HTMLElement) { .catch(console.error) } +// like +export function like(arn: AnimeNotifier, element: HTMLElement) { + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint + "/like", null) + .then(() => arn.reloadContent()) + .catch(console.error) +} + +// unlike +export function unlike(arn: AnimeNotifier, element: HTMLElement) { + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint + "/unlike", null) + .then(() => arn.reloadContent()) + .catch(console.error) +} + // Forum reply export function forumReply(arn: AnimeNotifier) { let textarea = arn.app.find("new-reply") as HTMLTextAreaElement diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index c8e32f73..3e3cc16a 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -182,23 +182,36 @@ export class AnimeNotifier { assignActions() { for(let element of findAll("action")) { - if(element["action assigned"]) { - continue - } - + let actionTrigger = element.dataset.trigger let actionName = element.dataset.action - element.addEventListener(element.dataset.trigger, e => { + let oldAction = element["action assigned"] + + if(oldAction) { + if(oldAction.trigger === actionTrigger && oldAction.action === actionName) { + continue + } + + element.removeEventListener(oldAction.trigger, oldAction.handler) + } + + let actionHandler = e => { actions[actionName](this, element, e) e.stopPropagation() e.preventDefault() - }) + } + + element.addEventListener(actionTrigger, actionHandler) // Use "action assigned" flag instead of removing the class. // This will make sure that DOM diffs which restore the class name // will not assign the action multiple times to the same element. - element["action assigned"] = true + element["action assigned"] = { + trigger: actionTrigger, + action: actionName, + handler: actionHandler + } } } diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 2326bf50..c2b7793d 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -47,8 +47,8 @@ export class Diff { let elemA = a as HTMLElement let elemB = b as HTMLElement - // Skip iframes - if(elemA.tagName === "IFRAME") { + // Skip iframes and lazy loaded images + if(elemA.tagName === "IFRAME" || elemA.classList.contains("lazy")) { continue } diff --git a/tests.go b/tests.go index ae7db0ed..8b872208 100644 --- a/tests.go +++ b/tests.go @@ -186,6 +186,7 @@ var routeTests = map[string][]string{ "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, + "/editor": nil, "/user": nil, "/settings": nil, "/extension/embed": nil, @@ -194,6 +195,7 @@ var routeTests = map[string][]string{ // API interfaces var creatable = reflect.TypeOf((*api.Creatable)(nil)).Elem() var updatable = reflect.TypeOf((*api.Updatable)(nil)).Elem() +var actionable = reflect.TypeOf((*api.Actionable)(nil)).Elem() var collection = reflect.TypeOf((*api.Collection)(nil)).Elem() // Required interface implementations @@ -204,10 +206,12 @@ var interfaceImplementations = map[string][]reflect.Type{ "Thread": []reflect.Type{ creatable, updatable, + actionable, }, "Post": []reflect.Type{ creatable, updatable, + actionable, }, "SoundTrack": []reflect.Type{ creatable, From e929451c88d8c01efd5a846d811fb320c076165c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 8 Jul 2017 23:57:40 +0200 Subject: [PATCH 199/527] Fixed diff bug --- scripts/Diff.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index c2b7793d..620b7797 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -47,8 +47,13 @@ export class Diff { let elemA = a as HTMLElement let elemB = b as HTMLElement - // Skip iframes and lazy loaded images - if(elemA.tagName === "IFRAME" || elemA.classList.contains("lazy")) { + // Skip iframes + if(elemA.tagName === "IFRAME") { + continue + } + + // Ignore lazy images if they have the same source + if(elemA.classList.contains("lazy") && elemA.dataset.src === elemB.dataset.src) { continue } From 34d85e5bd18657b79b1387cf71b968b873552a37 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 04:22:14 +0200 Subject: [PATCH 200/527] New dashboard layout --- pages/animelistitem/animelistitem.pixy | 2 +- pages/dashboard/dashboard.pixy | 24 +++++++++++++-- pages/listimport/listimport.pixy | 26 ++++++++-------- pages/newsoundtrack/newsoundtrack.pixy | 2 +- pages/newthread/newthread.pixy | 9 +++--- pages/profile/profile.scarlet | 3 +- pages/search/search.pixy | 26 ++++++++++++++-- pages/settings/settings.pixy | 5 +-- pages/settings/settings.scarlet | 6 ++-- styles/include/config.scarlet | 2 +- styles/widgets.scarlet | 42 ++++++++++++++++---------- 11 files changed, 101 insertions(+), 46 deletions(-) diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index b5052d63..d0dfe9aa 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -1,5 +1,5 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.Anime) - .widgets.mountable + .widget-form.mountable .widget.anime-list-item-view(data-api="/api/animelist/" + viewUser.ID + "/update/" + anime.ID) h1= anime.Title.Canonical diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index d0f557c1..2d0638ad 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -28,14 +28,14 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound .widget-element-text Icon(arn.GetForumIcon(post.Thread().Tags[0])) span= post.Thread().Title - + .widget.mountable - h3.widget-title Groups + h3.widget-title Artworks for i := 1; i <= 5; i++ .widget-element .widget-element-text - Icon("group") + Icon("paint-brush") span ... .widget.mountable @@ -52,7 +52,25 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound .widget-element-text Icon("music") span ... + + .widget.mountable + h3.widget-title AMVs + for i := 1; i <= 5; i++ + .widget-element + .widget-element-text + Icon("video-camera") + span ... + + .widget.mountable + h3.widget-title Groups + + for i := 1; i <= 5; i++ + .widget-element + .widget-element-text + Icon("group") + span ... + .widget.mountable h3.widget-title Contacts diff --git a/pages/listimport/listimport.pixy b/pages/listimport/listimport.pixy index e237e2a7..eaac0fbe 100644 --- a/pages/listimport/listimport.pixy +++ b/pages/listimport/listimport.pixy @@ -1,13 +1,15 @@ component ImportLists(user *arn.User) - .buttons.buttons-vertical - if user.Accounts.AniList.Nick != "" - .widget-input - a.button.mountable.ajax(href="/import/anilist/animelist") - Icon("download") - span Import AniList - - if user.Accounts.MyAnimeList.Nick != "" - .widget-input - a.button.mountable.ajax(href="/import/myanimelist/animelist") - Icon("download") - span Import MyAnimeList \ No newline at end of file + if user.Accounts.AniList.Nick != "" + label AniList: + + .widget-input + a.button.mountable.ajax(href="/import/anilist/animelist") + Icon("download") + span Import AniList + + if user.Accounts.MyAnimeList.Nick != "" + label MyAnimeList: + .widget-input + a.button.mountable.ajax(href="/import/myanimelist/animelist") + Icon("download") + span Import MyAnimeList \ No newline at end of file diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy index e1a85517..9d29aea0 100644 --- a/pages/newsoundtrack/newsoundtrack.pixy +++ b/pages/newsoundtrack/newsoundtrack.pixy @@ -1,5 +1,5 @@ component NewSoundTrack(user *arn.User) - .widgets + .widget-form .widget h1 New soundtrack diff --git a/pages/newthread/newthread.pixy b/pages/newthread/newthread.pixy index 08bfb5ef..479864bf 100644 --- a/pages/newthread/newthread.pixy +++ b/pages/newthread/newthread.pixy @@ -1,5 +1,5 @@ component NewThread(user *arn.User) - .widgets + .widget-form .widget input#title.widget-element(type="text", placeholder="Title") @@ -15,6 +15,7 @@ component NewThread(user *arn.User) if user.Role == "admin" option(value="update") Update - button.action(data-action="createThread", data-trigger="click") - Icon("check") - span Create thread \ No newline at end of file + .buttons + button.action(data-action="createThread", data-trigger="click") + Icon("check") + span Create thread \ No newline at end of file diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 7ec638c4..d232cf1f 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -87,7 +87,8 @@ profile-boot-duration = 2s margin-bottom 1rem .no-data - text-align center + width 100% + text-align left // Categories diff --git a/pages/search/search.pixy b/pages/search/search.pixy index b6ea2c5a..8fd64e6b 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -3,7 +3,10 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim .widgets .widget - h3 Users + h3.widget-title + Icon("user") + span Users + .user-avatars.user-search if len(users) == 0 p.no-data.mountable No users found. @@ -14,11 +17,28 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim //- a.ajax(href=user.Link())= user.Nick .widget - h3 Anime + h3.widget-title + Icon("tv") + span Anime + .profile-watching-list.anime-search if len(animeResults) == 0 p.no-data.mountable No anime found. else each anime in animeResults a.profile-watching-list-item.mountable.ajax(href=anime.Link(), title=anime.Title.Canonical, data-mountable-type="anime") - img.anime-cover-image.anime-search-result(src=anime.Image.Tiny, alt=anime.Title.Canonical) \ No newline at end of file + img.anime-cover-image.anime-search-result(src=anime.Image.Tiny, alt=anime.Title.Canonical) + + .widget + h3.widget-title + Icon("comment") + span Forums + + p.no-data.mountable Forums search coming soon. + + .widget + h3.widget-title + Icon("music") + span Soundtracks + + p.no-data.mountable Soundtrack search coming soon. \ No newline at end of file diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 300f5f4c..f3e2fa01 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -51,7 +51,7 @@ component Settings(user *arn.User) .widget.mountable h3.widget-title - Icon("refresh") + Icon("download") span Import ImportLists(user) @@ -61,7 +61,8 @@ component Settings(user *arn.User) Icon("puzzle-piece") span Extensions - .buttons + .widget-input + label Chrome Extension: button.action(data-action="installExtension", data-trigger="click") Icon("chrome") span Get the Chrome Extension diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet index 301a0549..a773b757 100644 --- a/pages/settings/settings.scarlet +++ b/pages/settings/settings.scarlet @@ -1,2 +1,4 @@ -.social-account-button - margin-bottom 1rem \ No newline at end of file +.widget-input + button, + .button + margin-bottom 1rem \ No newline at end of file diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 7ee83f3e..e108721a 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -67,4 +67,4 @@ typography-margin = 0.4rem // Timings fade-speed = 200ms transition-speed = 250ms -mountable-transition-speed = 400ms +mountable-transition-speed = 300ms diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index 33940d6b..e60792a6 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -1,21 +1,23 @@ .widgets - horizontal-wrap - justify-content space-around + display grid + grid-template-columns 1fr + +> 810px + .widgets + grid-template-columns repeat(2, 1fr) + grid-gap content-padding + +> 1240px + .widgets + grid-template-columns repeat(3, 1fr) + +> 1640px + .widgets + grid-template-columns repeat(4, 1fr) .widget vertical - align-items center - width 100% - padding 0.25rem - max-width 400px - -> 1240px - .widget - max-width 30vw - -< 810px - .widget - max-width 600px + margin-bottom content-padding .widget-element vertical-wrap @@ -24,7 +26,7 @@ margin-bottom 1rem padding 0.5rem 1rem width 100% - max-width 700px + // max-width 700px .widget-element-text horizontal @@ -38,5 +40,13 @@ width 100% .widget-title + text-align left + padding-bottom 0.5rem + border-bottom 1px solid rgba(0, 0, 0, 0.1) // We need !important here to overwrite the h3:first-child rule - margin 1rem 0 !important \ No newline at end of file + margin 1rem 0 !important + +.widget-form + width 100% + max-width 650px + margin 0 auto \ No newline at end of file From 7e65920be97d78b6946ce9a95fd4ecdac27bc5ac Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 04:31:41 +0200 Subject: [PATCH 201/527] Reduced number of anime search results --- pages/search/search.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/search/search.go b/pages/search/search.go index 0044ec82..c86995f4 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -6,8 +6,8 @@ import ( "github.com/animenotifier/notify.moe/components" ) -const maxUsers = 9 * 4 -const maxAnime = 9 * 4 +const maxUsers = 6 * 6 +const maxAnime = 5 * 6 // Get search page. func Get(ctx *aero.Context) string { From 840911c91b42467ead9649ddfb1a96f7a7d8c97c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 04:52:13 +0200 Subject: [PATCH 202/527] Fixed mobile layout --- styles/widgets.scarlet | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index e60792a6..6091e200 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -18,6 +18,7 @@ .widget vertical margin-bottom content-padding + overflow hidden .widget-element vertical-wrap From 839918e20e3f56b7757bf4f6fc890aff0ee1fc2a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 13:11:54 +0200 Subject: [PATCH 203/527] Fixed edit anime page --- pages/editanime/editanime.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/editanime/editanime.pixy b/pages/editanime/editanime.pixy index c22a8753..b40e541c 100644 --- a/pages/editanime/editanime.pixy +++ b/pages/editanime/editanime.pixy @@ -1,9 +1,9 @@ component EditAnime(anime *arn.Anime) h1= anime.Title.Canonical - .widgets + .widget-form.mountable .widget(data-api="/api/anime/" + anime.ID) - h3.anime-section-name Mappings + h3.widget-title Mappings InputText("Custom:ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on cal.syoboi.jp") InputText("Custom:AniListID", anime.GetMapping("anilist/anime"), "AniList ID", "ID on anilist.co") From 27b828578f3b2eee9be34476b1e9489e60912000 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 15:56:40 +0200 Subject: [PATCH 204/527] Fixed anilist changes --- pages/listimport/listimportanilist/anilist.go | 13 +++++++------ patches/import-anilist-user/import-anilist-user.go | 9 +++++---- patches/import-anilist/import-anilist.go | 7 ++++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pages/listimport/listimportanilist/anilist.go b/pages/listimport/listimportanilist/anilist.go index 12f2e8dc..6f1b3c95 100644 --- a/pages/listimport/listimportanilist/anilist.go +++ b/pages/listimport/listimportanilist/anilist.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/aerogo/aero" + "github.com/animenotifier/anilist" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" @@ -49,7 +50,7 @@ func Finish(ctx *aero.Context) string { item := &arn.AnimeListItem{ AnimeID: match.ARNAnime.ID, - Status: match.AniListItem.AnimeListStatus(), + Status: arn.AniListAnimeListStatus(match.AniListItem), Episodes: match.AniListItem.EpisodesWatched, Notes: match.AniListItem.Notes, Rating: &arn.AnimeRating{ @@ -80,7 +81,7 @@ func getMatches(ctx *aero.Context) ([]*arn.AniListMatch, string) { return nil, ctx.Error(http.StatusBadRequest, "Not logged in", nil) } - authErr := arn.AniList.Authorize() + authErr := anilist.Authorize() if authErr != nil { return nil, ctx.Error(http.StatusBadRequest, "Couldn't authorize the Anime Notifier app on AniList", authErr) @@ -92,7 +93,7 @@ func getMatches(ctx *aero.Context) ([]*arn.AniListMatch, string) { return nil, ctx.Error(http.StatusBadRequest, "Couldn't load notify.moe list of all anime", allErr) } - anilistAnimeList, err := arn.AniList.GetAnimeList(user) + anilistAnimeList, err := anilist.GetAnimeList(user.Accounts.AniList.Nick) if err != nil { return nil, ctx.Error(http.StatusBadRequest, "Couldn't load your anime list from AniList", err) @@ -104,7 +105,7 @@ func getMatches(ctx *aero.Context) ([]*arn.AniListMatch, string) { } // findAllMatches returns all matches for the anime inside an anilist anime list. -func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*arn.AniListMatch { +func findAllMatches(allAnime []*arn.Anime, animeList *anilist.AnimeList) []*arn.AniListMatch { matches := []*arn.AniListMatch{} matches = importList(matches, allAnime, animeList.Lists.Watching) @@ -113,7 +114,7 @@ func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*a matches = importList(matches, allAnime, animeList.Lists.OnHold) matches = importList(matches, allAnime, animeList.Lists.Dropped) - custom, ok := animeList.CustomLists.(map[string][]*arn.AniListAnimeListItem) + custom, ok := animeList.CustomLists.(map[string][]*anilist.AnimeListItem) if !ok { return matches @@ -127,7 +128,7 @@ func findAllMatches(allAnime []*arn.Anime, animeList *arn.AniListAnimeList) []*a } // importList imports a single list inside an anilist anime list collection. -func importList(matches []*arn.AniListMatch, allAnime []*arn.Anime, animeListItems []*arn.AniListAnimeListItem) []*arn.AniListMatch { +func importList(matches []*arn.AniListMatch, allAnime []*arn.Anime, animeListItems []*anilist.AnimeListItem) []*arn.AniListMatch { for _, item := range animeListItems { matches = append(matches, &arn.AniListMatch{ AniListItem: item, diff --git a/patches/import-anilist-user/import-anilist-user.go b/patches/import-anilist-user/import-anilist-user.go index 80a13763..0c63685c 100644 --- a/patches/import-anilist-user/import-anilist-user.go +++ b/patches/import-anilist-user/import-anilist-user.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/animenotifier/anilist" "github.com/animenotifier/arn" "github.com/fatih/color" ) @@ -15,18 +16,18 @@ func init() { } func main() { - arn.PanicOnError(arn.AniList.Authorize()) - println(arn.AniList.AccessToken) + arn.PanicOnError(anilist.Authorize()) + println(anilist.AccessToken) user, _ := arn.GetUserByNick(userName) - animeList, err := arn.AniList.GetAnimeList(user) + animeList, err := anilist.GetAnimeList(user.Accounts.AniList.Nick) arn.PanicOnError(err) importList(animeList.Lists.Watching) importList(animeList.Lists.Completed) } -func importList(animeListItems []*arn.AniListAnimeListItem) { +func importList(animeListItems []*anilist.AnimeListItem) { imported := []*arn.Anime{} for _, item := range animeListItems { diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go index 5bfee012..7711fb4b 100644 --- a/patches/import-anilist/import-anilist.go +++ b/patches/import-anilist/import-anilist.go @@ -3,19 +3,20 @@ package main import ( "fmt" + "github.com/animenotifier/anilist" "github.com/animenotifier/arn" "github.com/fatih/color" ) func main() { - arn.PanicOnError(arn.AniList.Authorize()) - color.Green(arn.AniList.AccessToken) + arn.PanicOnError(anilist.Authorize()) + color.Green(anilist.AccessToken) allAnime, err := arn.AllAnime() arn.PanicOnError(err) count := 0 - stream := arn.AniList.StreamAnime() + stream := anilist.StreamAnime() for aniListAnime := range stream { anime := arn.FindAniListAnime(aniListAnime, allAnime) From 37fb1895fe1b81308c26d268a8b92eef8f719e42 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 17:02:29 +0200 Subject: [PATCH 205/527] Fixed wrong redirect --- pages/animelistitem/animelistitem.pixy | 2 +- scripts/Actions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index d0dfe9aa..d63fa80b 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -25,7 +25,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. InputTextArea("Notes", item.Notes, "Notes", "Your notes") .buttons.mountable - a.ajax.button(href="/+" + viewUser.Nick + "/animelist/watching") + a.ajax.button(href="/+" + viewUser.Nick + "/animelist") Icon("list") span View collection a.ajax.button(href=anime.Link()) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 917ebde7..119512c3 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -272,7 +272,7 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen throw body } - return arn.app.load("/+" + userNick + "/animelist/watching") + return arn.app.load("/+" + userNick + "/animelist") }) .catch(console.error) .then(() => arn.loading(false)) From 0ab9433eb24b69943c61d593514164764e4133df Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 17:43:13 +0200 Subject: [PATCH 206/527] Fixed alignment --- pages/profile/profile.scarlet | 2 +- pages/search/search.pixy | 8 ++++---- pages/search/search.scarlet | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index d232cf1f..29cec37b 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -88,7 +88,7 @@ profile-boot-duration = 2s .no-data width 100% - text-align left + text-align center // Categories diff --git a/pages/search/search.pixy b/pages/search/search.pixy index 8fd64e6b..f7269231 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -9,7 +9,7 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim .user-avatars.user-search if len(users) == 0 - p.no-data.mountable No users found. + p.no-search-results.mountable No users found. else each user in users .mountable(data-mountable-type="user") @@ -23,7 +23,7 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim .profile-watching-list.anime-search if len(animeResults) == 0 - p.no-data.mountable No anime found. + p.no-search-results.mountable No anime found. else each anime in animeResults a.profile-watching-list-item.mountable.ajax(href=anime.Link(), title=anime.Title.Canonical, data-mountable-type="anime") @@ -34,11 +34,11 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim Icon("comment") span Forums - p.no-data.mountable Forums search coming soon. + p.no-search-results.mountable Forums search coming soon. .widget h3.widget-title Icon("music") span Soundtracks - p.no-data.mountable Soundtrack search coming soon. \ No newline at end of file + p.no-search-results.mountable Soundtrack search coming soon. \ No newline at end of file diff --git a/pages/search/search.scarlet b/pages/search/search.scarlet index ec095322..2f0d315d 100644 --- a/pages/search/search.scarlet +++ b/pages/search/search.scarlet @@ -1,3 +1,6 @@ .anime-search-result width 55px !important - height 78px !important \ No newline at end of file + height 78px !important + +.no-search-results + text-align left \ No newline at end of file From ced1790c35594afce4330ceabddc22ab561d5a46 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 9 Jul 2017 23:22:31 +0200 Subject: [PATCH 207/527] Added basics for twist.moe background job --- jobs/twist/twist.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 jobs/twist/twist.go diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go new file mode 100644 index 00000000..f364ff03 --- /dev/null +++ b/jobs/twist/twist.go @@ -0,0 +1,38 @@ +package main + +import ( + "sort" + "strconv" + + "github.com/animenotifier/arn" + "github.com/animenotifier/twist" + "github.com/fatih/color" +) + +func main() { + // Replace this with ID list from twist.moe later + animeIDs := []string{ + "13274", + "10902", + } + + for _, animeID := range animeIDs { + feed, err := twist.GetFeedByKitsuID(animeID) + + if err != nil { + color.Red("Error querying ID %s: %v", animeID, err) + continue + } + + episodes := feed.Episodes + + // Sort by episode number + sort.Slice(episodes, func(a, b int) bool { + epsA, _ := strconv.Atoi(episodes[a].Number) + epsB, _ := strconv.Atoi(episodes[b].Number) + return epsA < epsB + }) + + arn.PrettyPrint(episodes) + } +} From 29842b3cccfc160d71ffae9a02c28e40d24d77c1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 10 Jul 2017 00:21:53 +0200 Subject: [PATCH 208/527] Minor changes --- jobs/twist/twist.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index f364ff03..b9369e3a 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -2,7 +2,6 @@ package main import ( "sort" - "strconv" "github.com/animenotifier/arn" "github.com/animenotifier/twist" @@ -28,9 +27,7 @@ func main() { // Sort by episode number sort.Slice(episodes, func(a, b int) bool { - epsA, _ := strconv.Atoi(episodes[a].Number) - epsB, _ := strconv.Atoi(episodes[b].Number) - return epsA < epsB + return episodes[a].Number < episodes[b].Number }) arn.PrettyPrint(episodes) From 8037edcb6797178f58dedb5df4d4315fff7cb834 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 10 Jul 2017 16:58:34 +0200 Subject: [PATCH 209/527] Anime episodes stored under a different table --- jobs/twist/twist.go | 51 ++++++++++++++++--- main.go | 4 +- mixins/ThreadLink.pixy | 2 +- pages/anime/anime.go | 8 +-- pages/anime/anime.pixy | 11 ++-- pages/animelist/animelist.pixy | 35 ++++++------- pages/animelist/animelist.scarlet | 32 ++++-------- pages/profile/profile.pixy | 2 +- pages/profile/watching.scarlet | 5 ++ .../move-anime-episodes.go | 17 +++++++ tests.go | 8 +-- 11 files changed, 112 insertions(+), 63 deletions(-) create mode 100644 patches/move-anime-episodes/move-anime-episodes.go diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index b9369e3a..8f56977e 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -1,21 +1,44 @@ package main import ( + "fmt" + "os" "sort" + "strings" + "time" "github.com/animenotifier/arn" "github.com/animenotifier/twist" "github.com/fatih/color" ) +var rateLimiter = time.NewTicker(500 * time.Millisecond) + func main() { // Replace this with ID list from twist.moe later - animeIDs := []string{ - "13274", - "10902", - } + currentAnime, err := arn.FilterAnime(func(anime *arn.Anime) bool { + return anime.Status == "current" + }) + arn.PanicOnError(err) - for _, animeID := range animeIDs { + color.Yellow("Refreshing twist.moe links for %d anime", len(currentAnime)) + + for count, anime := range currentAnime { + // Wait for rate limiter + <-rateLimiter.C + + // anime, animeErr := arn.GetAnime(animeID) + + // if animeErr != nil { + // color.Red("Error fetching anime from the database with ID %s: %v", animeID, animeErr) + // continue + // } + animeID := anime.ID + + // Log + fmt.Fprintf(os.Stdout, "[%d / %d] ", count+1, len(currentAnime)) + + // Get twist.moe feed feed, err := twist.GetFeedByKitsuID(animeID) if err != nil { @@ -30,6 +53,22 @@ func main() { return episodes[a].Number < episodes[b].Number }) - arn.PrettyPrint(episodes) + for _, episode := range episodes { + arnEpisode := anime.EpisodeByNumber(episode.Number) + + if arnEpisode == nil { + color.Red("Anime %s Episode %d not found", anime.ID, episode.Number) + continue + } + + if arnEpisode.Links == nil { + arnEpisode.Links = map[string]string{} + } + + arnEpisode.Links["twist.moe"] = strings.Replace(episode.Link, "https://test.twist.moe/", "https://twist.moe/", 1) + } + + arn.PanicOnError(anime.Episodes().Save()) + color.Green("Found %d episodes for anime %s", len(episodes), animeID) } } diff --git a/main.go b/main.go index a52e76dd..23f79b31 100644 --- a/main.go +++ b/main.go @@ -71,8 +71,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/explore", explore.Get) app.Ajax("/forum", forums.Get) app.Ajax("/forum/:tag", forum.Get) - app.Ajax("/threads/:id", threads.Get) - app.Ajax("/posts/:id", posts.Get) + app.Ajax("/thread/:id", threads.Get) + app.Ajax("/post/:id", posts.Get) app.Ajax("/tracks/:id", tracks.Get) app.Ajax("/new/thread", newthread.Get) app.Ajax("/new/soundtrack", newsoundtrack.Get) diff --git a/mixins/ThreadLink.pixy b/mixins/ThreadLink.pixy index 6d6bbb87..564f7787 100644 --- a/mixins/ThreadLink.pixy +++ b/mixins/ThreadLink.pixy @@ -6,7 +6,7 @@ component ThreadLink(thread *arn.Thread) .thread-content if thread.Sticky != 0 Icon("thumb-tack") - a.thread-link-title.ajax(href="/threads/" + thread.ID)= thread.Title + a.thread-link-title.ajax(href="/thread/" + thread.ID)= thread.Title .spacer .thread-reply-count= len(thread.Posts) .thread-icons diff --git a/pages/anime/anime.go b/pages/anime/anime.go index a3ad71e9..9981e74a 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -31,12 +31,12 @@ func Get(ctx *aero.Context) string { episodesReversed := false - if len(anime.Episodes) > maxEpisodes { + if len(anime.Episodes().Items) > maxEpisodes { episodesReversed = true - anime.Episodes = anime.Episodes[len(anime.Episodes)-maxEpisodesLongSeries:] + anime.Episodes().Items = anime.Episodes().Items[len(anime.Episodes().Items)-maxEpisodesLongSeries:] - for i, j := 0, len(anime.Episodes)-1; i < j; i, j = i+1, j-1 { - anime.Episodes[i], anime.Episodes[j] = anime.Episodes[j], anime.Episodes[i] + for i, j := 0, len(anime.Episodes().Items)-1; i < j; i, j = i+1, j-1 { + anime.Episodes().Items[i], anime.Episodes().Items[j] = anime.Episodes().Items[j], anime.Episodes().Items[i] } } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index d2f31656..d32f2b88 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -147,20 +147,23 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis each track in tracks SoundTrack(track) - if len(anime.Episodes) > 0 + if len(anime.Episodes().Items) > 0 if episodesReversed h3.anime-section-name Latest episodes else h3.anime-section-name Episodes table.episodes tbody - each episode in anime.Episodes + each episode in anime.Episodes().Items tr.episode td.episode-number= episode.Number td.episode-title= episode.Title.Japanese td.episode-actions - a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") - RawIcon("google") + for name, link := range episode.Links + a(href=link, target="_blank", rel="noopener", title="Watch episode " + toString(episode.Number) + " on " + name) + RawIcon("eye") + //- a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") + //- RawIcon("google") td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() //- h3.anime-section-name Reviews diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 5e59b7e1..d99052fe 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -36,26 +36,25 @@ component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, u //- AnimeList(animeList, user) component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User) - table.anime-list - thead - tr - th.anime-list-item-name Anime - th.anime-list-item-airing-date Airing - th.anime-list-item-episodes Episodes - th.anime-list-item-rating Overall - //- th.anime-list-item-rating Story - //- th.anime-list-item-rating Visuals - //- th.anime-list-item-rating Soundtrack - if user != nil - th.anime-list-item-actions Actions - tbody + table + tbody.anime-list each item in animeList.Items - tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) + tr.anime-list-item(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical + + if user != nil && item.Status == arn.AnimeListStatusWatching && item.Anime().EpisodeByNumber(item.Episodes + 1) != nil + td.anime-list-item-actions + for _, link := range item.Anime().EpisodeByNumber(item.Episodes + 1).Links + a(href=link, title="Watch episode " + toString(item.Episodes + 1) + " on twist.moe", target="_blank", rel="noopener") + RawIcon("eye") + //- a(href=arn.Nyaa.GetLink(item.Anime()), title="Search on Nyaa", target="_blank", rel="noopener") + //- RawIcon("download") + td.anime-list-item-airing-date - if item.Anime().UpcomingEpisode() != nil + if item.Status == arn.AnimeListStatusWatching && item.Anime().UpcomingEpisode() != nil span.utc-airing-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) + td.anime-list-item-episodes .anime-list-item-episodes-watched .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save")= item.Episodes @@ -64,6 +63,7 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User //- .anime-list-item-episodes-edit //- a.ajax(href=, title="Edit anime") //- RawIcon("pencil") + td.anime-list-item-rating(title="Overall rating") .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Overall) //- td.anime-list-item-rating(title="Story rating") @@ -72,8 +72,3 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Visuals", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Visuals) //- td.anime-list-item-rating(title="Soundtrack rating") //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Soundtrack", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Soundtrack) - - //- 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 e47f72aa..6ac35260 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -8,11 +8,8 @@ .anime-list vertical - tr - horizontal - - thead - display none +.anime-list-item + horizontal .anime-list-item-name flex 1 @@ -38,25 +35,16 @@ flex 0.4 opacity 0.5 -// .anime-list-item-episodes-edit -// flex 0.5 +.anime-list-item-rating + text-align right + +.anime-list-item-actions + display none + flex-basis 30px // // Beautify icon alignment // .raw-icon -// margin-bottom -2px - -.anime-list-item-rating - flex-basis 50px - text-align center - -.anime-list-item-actions - flex-basis 40px - text-align right - display none - - // Beautify icon alignment - .raw-icon - margin-bottom -4px +// margin-bottom -4px > 740px .anime-list-item-actions @@ -68,6 +56,8 @@ > 700px .anime-list-item-airing-date display block + text-align right + flex-basis 100px < 1100px .anime-list-item-rating diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 87b952fc..d5586a36 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -102,7 +102,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .profile-watching-list.mountable each item in animeList.Items a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") - img.anime-cover-image.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + img.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) //- .profile-category.mountable //- h3 diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 009c9d0e..307b5b1d 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -9,6 +9,11 @@ width 55px !important height 78px !important border-radius 2px + filter none + transition filter transition-speed ease + + :hover + filter saturate(1.3) // .status-tabs // position fixed diff --git a/patches/move-anime-episodes/move-anime-episodes.go b/patches/move-anime-episodes/move-anime-episodes.go new file mode 100644 index 00000000..ca2dc59e --- /dev/null +++ b/patches/move-anime-episodes/move-anime-episodes.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + for anime := range arn.MustStreamAnime() { + arn.PanicOnError(arn.DB.Set("AnimeEpisodes", anime.ID, &arn.AnimeEpisodes{ + AnimeID: anime.ID, + Items: anime.Episodes, + })) + + anime.Episodes = anime.Episodes[:0] + anime.MustSave() + } +} diff --git a/tests.go b/tests.go index 8b872208..7fe2a5ed 100644 --- a/tests.go +++ b/tests.go @@ -59,12 +59,12 @@ var routeTests = map[string][]string{ "/anime/1", }, - "/threads/:id": []string{ - "/threads/HJgS7c2K", + "/thread/:id": []string{ + "/thread/HJgS7c2K", }, - "/posts/:id": []string{ - "/posts/B1RzshnK", + "/post/:id": []string{ + "/post/B1RzshnK", }, "/forum/:tag": []string{ From 1da3e3f2f842d4f286f988fc72ab7e56c6217584 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 10 Jul 2017 20:35:45 +0200 Subject: [PATCH 210/527] Episode extrapolation --- jobs/refresh-episodes/refresh-episodes.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 662652ee..3c000c48 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -13,6 +13,7 @@ func main() { color.Yellow("Refreshing episode information for each anime.") highPriority := []*arn.Anime{} + mediumPriority := []*arn.Anime{} lowPriority := []*arn.Anime{} for anime := range arn.MustStreamAnime() { @@ -20,9 +21,12 @@ func main() { continue } - if anime.Status == "current" || anime.Status == "upcoming" { + switch anime.Status { + case "current": highPriority = append(highPriority, anime) - } else { + case "upcoming": + mediumPriority = append(mediumPriority, anime) + default: lowPriority = append(lowPriority, anime) } } @@ -30,6 +34,9 @@ func main() { color.Cyan("High priority queue:") refresh(highPriority) + color.Cyan("Medium priority queue:") + refresh(mediumPriority) + color.Cyan("Low priority queue:") refresh(lowPriority) @@ -38,7 +45,8 @@ func main() { func refresh(queue []*arn.Anime) { for _, anime := range queue { - episodeCount := len(anime.Episodes) + episodeCount := len(anime.Episodes().Items) + availableEpisodeCount := anime.Episodes().AvailableCount() err := anime.RefreshEpisodes() @@ -49,7 +57,7 @@ func refresh(queue []*arn.Anime) { color.Red(err.Error()) } else { - fmt.Println(anime.ID, "|", anime.Title.Canonical, "+"+strconv.Itoa(len(anime.Episodes)-episodeCount)) + fmt.Println(anime.ID, "|", anime.Title.Canonical, "|", "+"+strconv.Itoa(len(anime.Episodes().Items)-episodeCount)+" airing", "|", "+"+strconv.Itoa(anime.Episodes().AvailableCount()-availableEpisodeCount)+" available") } } } From b6fb1c1f08335333ed5b464c67ff3d32a7f6c188 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 10 Jul 2017 20:39:50 +0200 Subject: [PATCH 211/527] Fixed old stuff --- jobs/sync-anime/sync-anime.go | 4 --- jobs/twist/twist.go | 9 +++--- pages/anime/anime.pixy | 10 ++++-- pages/animeepisode/animeepisode.go | 7 ++++ pages/apiview/api.pixy | 2 +- pages/newthread/newthread.pixy | 2 ++ .../add-empty-episodes/add-empty-episodes.go | 32 ------------------- .../move-anime-episodes.go | 17 ---------- scripts/DateView.ts | 10 ++++++ 9 files changed, 32 insertions(+), 61 deletions(-) create mode 100644 pages/animeepisode/animeepisode.go delete mode 100644 patches/add-empty-episodes/add-empty-episodes.go delete mode 100644 patches/move-anime-episodes/move-anime-episodes.go diff --git a/jobs/sync-anime/sync-anime.go b/jobs/sync-anime/sync-anime.go index 65002a1b..2a45a036 100644 --- a/jobs/sync-anime/sync-anime.go +++ b/jobs/sync-anime/sync-anime.go @@ -68,10 +68,6 @@ func sync(data *kitsu.Anime) *arn.Anime { anime.Mappings = []*arn.Mapping{} } - if anime.Episodes == nil { - anime.Episodes = []*arn.AnimeEpisode{} - } - // Prefer Shoboi Japanese titles over Kitsu JP titles if anime.GetMapping("shoboi/anime") != "" { // Only take Kitsu title when our JP title is empty diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index 8f56977e..43c4c42b 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "sort" "strings" "time" @@ -48,10 +47,10 @@ func main() { episodes := feed.Episodes - // Sort by episode number - sort.Slice(episodes, func(a, b int) bool { - return episodes[a].Number < episodes[b].Number - }) + // // Sort by episode number + // sort.Slice(episodes, func(a, b int) bool { + // return episodes[a].Number < episodes[b].Number + // }) for _, episode := range episodes { arnEpisode := anime.EpisodeByNumber(episode.Number) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index d32f2b88..c67c4ac5 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -156,8 +156,14 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis tbody each episode in anime.Episodes().Items tr.episode - td.episode-number= episode.Number - td.episode-title= episode.Title.Japanese + td.episode-number + if episode.Number != -1 + span= episode.Number + td.episode-title + if episode.Title.Japanese != "" + span= episode.Title.Japanese + else + span - td.episode-actions for name, link := range episode.Links a(href=link, target="_blank", rel="noopener", title="Watch episode " + toString(episode.Number) + " on " + name) diff --git a/pages/animeepisode/animeepisode.go b/pages/animeepisode/animeepisode.go new file mode 100644 index 00000000..736ac5e4 --- /dev/null +++ b/pages/animeepisode/animeepisode.go @@ -0,0 +1,7 @@ +package animeepisode + +import "github.com/aerogo/aero" + +func Get(ctx *aero.Context) string { + return ctx.HTML("") +} diff --git a/pages/apiview/api.pixy b/pages/apiview/api.pixy index 1a3e0df3..4ff6fc7d 100644 --- a/pages/apiview/api.pixy +++ b/pages/apiview/api.pixy @@ -1,5 +1,5 @@ component API(types []string) - h3 API + h1 API table //- thead diff --git a/pages/newthread/newthread.pixy b/pages/newthread/newthread.pixy index 479864bf..9ddebcbe 100644 --- a/pages/newthread/newthread.pixy +++ b/pages/newthread/newthread.pixy @@ -1,4 +1,6 @@ component NewThread(user *arn.User) + h1 New thread + .widget-form .widget input#title.widget-element(type="text", placeholder="Title") diff --git a/patches/add-empty-episodes/add-empty-episodes.go b/patches/add-empty-episodes/add-empty-episodes.go deleted file mode 100644 index 47b32a6c..00000000 --- a/patches/add-empty-episodes/add-empty-episodes.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - // Get a stream of all anime - allAnime, err := arn.AllAnime() - - if err != nil { - panic(err) - } - - // Iterate over the stream - for _, anime := range allAnime { - if anime.Mappings == nil { - anime.Mappings = []*arn.Mapping{} - } - - if anime.Episodes == nil { - anime.Episodes = []*arn.AnimeEpisode{} - } - - err := anime.Save() - - if err != nil { - color.Red("Error saving anime: %v", err) - } - } -} diff --git a/patches/move-anime-episodes/move-anime-episodes.go b/patches/move-anime-episodes/move-anime-episodes.go deleted file mode 100644 index ca2dc59e..00000000 --- a/patches/move-anime-episodes/move-anime-episodes.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" -) - -func main() { - for anime := range arn.MustStreamAnime() { - arn.PanicOnError(arn.DB.Set("AnimeEpisodes", anime.ID, &arn.AnimeEpisodes{ - AnimeID: anime.ID, - Items: anime.Episodes, - })) - - anime.Episodes = anime.Episodes[:0] - anime.MustSave() - } -} diff --git a/scripts/DateView.ts b/scripts/DateView.ts index 270e76d4..21abfe11 100644 --- a/scripts/DateView.ts +++ b/scripts/DateView.ts @@ -60,6 +60,11 @@ function getRemainingTime(remaining: number): string { } export function displayAiringDate(element: HTMLElement, now: Date) { + if(element.dataset.startDate === "") { + element.innerText = "" + return + } + let startDate = new Date(element.dataset.startDate) let endDate = new Date(element.dataset.endDate) @@ -91,6 +96,11 @@ export function displayAiringDate(element: HTMLElement, now: Date) { } export function displayDate(element: HTMLElement, now: Date) { + if(element.dataset.date === "") { + element.innerText = "" + return + } + let startDate = new Date(element.dataset.date) let h = startDate.getHours() From 96beca0885291365babeff7c3d558bf7960e8361 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 10 Jul 2017 20:51:06 +0200 Subject: [PATCH 212/527] Fixed schedule --- pages/dashboard/dashboard.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 3050e74a..61d06347 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -50,10 +50,6 @@ func dashboard(ctx *aero.Context) string { animeList.PrefetchAnime() for _, item := range animeList.Items { - if len(upcomingEpisodes) >= maxScheduleItems { - break - } - futureEpisodes := item.Anime().UpcomingEpisodes() if len(futureEpisodes) == 0 { @@ -66,6 +62,10 @@ func dashboard(ctx *aero.Context) string { sort.Slice(upcomingEpisodes, func(i, j int) bool { return upcomingEpisodes[i].Episode.AiringDate.Start < upcomingEpisodes[j].Episode.AiringDate.Start }) + + if len(upcomingEpisodes) >= maxScheduleItems { + upcomingEpisodes = upcomingEpisodes[:maxScheduleItems] + } }, func() { var err error soundTracks, err = arn.AllSoundTracks() From bc83e93970f7f1cd4b839f3f360bb47051bca206 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 10 Jul 2017 22:35:48 +0200 Subject: [PATCH 213/527] Added twist.moe statistics --- jobs/statistics/statistics.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index 3c304d93..74a4d81d 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -99,6 +99,7 @@ func getAnimeStats() []*arn.PieChart { malEdits := stats{} anidbEdits := stats{} rating := stats{} + twist := stats{} for _, anime := range allAnime { for _, external := range anime.Mappings { @@ -169,6 +170,20 @@ func getAnimeStats() []*arn.PieChart { rating[arn.ToString(int(anime.Rating.Overall+0.5))]++ + found := false + for _, episode := range anime.Episodes().Items { + if episode.Links != nil && episode.Links["twist.moe"] != "" { + found = true + break + } + } + + if found { + twist["Connected with AnimeTwist"]++ + } else { + twist["Not connected with AnimeTwist"]++ + } + status[anime.Status]++ types[anime.Type]++ } @@ -183,6 +198,7 @@ func getAnimeStats() []*arn.PieChart { arn.NewPieChart("AniList", anilist), arn.NewPieChart("AniDB", anidb), arn.NewPieChart("Shoboi", shoboi), + arn.NewPieChart("AnimeTwist", twist), // arn.NewPieChart("MyAnimeList Editors", malEdits), arn.NewPieChart("AniList Editors", anilistEdits), // arn.NewPieChart("AniDB Editors", anidbEdits), From 65ca0466e01180574dfb6807be585511e36508fc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 10 Jul 2017 23:37:57 +0200 Subject: [PATCH 214/527] Added export option --- pages/settings/settings.pixy | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index f3e2fa01..34d69748 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -49,13 +49,6 @@ component Settings(user *arn.User) Icon("circle-o") span Not connected - .widget.mountable - h3.widget-title - Icon("download") - span Import - - ImportLists(user) - .widget.mountable h3.widget-title Icon("puzzle-piece") @@ -67,6 +60,24 @@ component Settings(user *arn.User) Icon("chrome") span Get the Chrome Extension + .widget.mountable + h3.widget-title + Icon("download") + span Import + + ImportLists(user) + + .widget.mountable + h3.widget-title + Icon("upload") + span Export + + .widget-input + label JSON: + a.button(href="/api/animelist/" + user.ID) + Icon("upload") + span Export anime list as JSON + //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title //- Icon("cogs") From 0de59c5b4f0c16691f06528bb3b300b33233ab46 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 00:56:18 +0200 Subject: [PATCH 215/527] Updated sync --- jobs/sync-anime/sync-anime.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jobs/sync-anime/sync-anime.go b/jobs/sync-anime/sync-anime.go index 2a45a036..b531802e 100644 --- a/jobs/sync-anime/sync-anime.go +++ b/jobs/sync-anime/sync-anime.go @@ -136,6 +136,13 @@ func sync(data *kitsu.Anime) *arn.Anime { status = color.RedString("✘") } + // Episodes + episodes, err := arn.GetAnimeEpisodes(anime.ID) + + if err != nil || episodes == nil { + anime.RefreshEpisodes() + } + // Log fmt.Println(status, anime.ID, anime.Title.Canonical) From b4ba7c3b4df302dfcb198724868f0892ddd1d666 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 01:12:59 +0200 Subject: [PATCH 216/527] Added transition --- pages/profile/watching.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 307b5b1d..dec436a5 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -10,7 +10,7 @@ height 78px !important border-radius 2px filter none - transition filter transition-speed ease + transition filter transition-speed ease, opacity transition-speed ease :hover filter saturate(1.3) From 06fddad8bd0af3a46489b3b8d9f7c6f0dc9d627f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 13:12:10 +0200 Subject: [PATCH 217/527] Improved twist.moe job --- jobs/twist/twist.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index 43c4c42b..88b14325 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -15,27 +15,25 @@ var rateLimiter = time.NewTicker(500 * time.Millisecond) func main() { // Replace this with ID list from twist.moe later - currentAnime, err := arn.FilterAnime(func(anime *arn.Anime) bool { - return anime.Status == "current" - }) + twistAnime, err := twist.GetAnimeIndex() arn.PanicOnError(err) + idList := twistAnime.KitsuIDs() - color.Yellow("Refreshing twist.moe links for %d anime", len(currentAnime)) + color.Yellow("Refreshing twist.moe links for %d anime", len(idList)) - for count, anime := range currentAnime { + for count, animeID := range idList { // Wait for rate limiter <-rateLimiter.C - // anime, animeErr := arn.GetAnime(animeID) + anime, animeErr := arn.GetAnime(animeID) - // if animeErr != nil { - // color.Red("Error fetching anime from the database with ID %s: %v", animeID, animeErr) - // continue - // } - animeID := anime.ID + if animeErr != nil { + color.Red("Error fetching anime from the database with ID %s: %v", animeID, animeErr) + continue + } // Log - fmt.Fprintf(os.Stdout, "[%d / %d] ", count+1, len(currentAnime)) + fmt.Fprintf(os.Stdout, "[%d / %d] ", count+1, len(idList)) // Get twist.moe feed feed, err := twist.GetFeedByKitsuID(animeID) From 112bec862eabec10a6814689073d8da627dbf6f0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 13:26:53 +0200 Subject: [PATCH 218/527] Improved twist.moe updater --- jobs/twist/twist.go | 40 ++++++---------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index 88b14325..0182a389 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "strings" "time" "github.com/animenotifier/arn" @@ -22,9 +21,6 @@ func main() { color.Yellow("Refreshing twist.moe links for %d anime", len(idList)) for count, animeID := range idList { - // Wait for rate limiter - <-rateLimiter.C - anime, animeErr := arn.GetAnime(animeID) if animeErr != nil { @@ -35,37 +31,13 @@ func main() { // Log fmt.Fprintf(os.Stdout, "[%d / %d] ", count+1, len(idList)) - // Get twist.moe feed - feed, err := twist.GetFeedByKitsuID(animeID) + // Refresh + anime.RefreshEpisodes() - if err != nil { - color.Red("Error querying ID %s: %v", animeID, err) - continue - } + // Ok + color.Green("Found %d episodes for anime %s", len(anime.Episodes().Items), animeID) - episodes := feed.Episodes - - // // Sort by episode number - // sort.Slice(episodes, func(a, b int) bool { - // return episodes[a].Number < episodes[b].Number - // }) - - for _, episode := range episodes { - arnEpisode := anime.EpisodeByNumber(episode.Number) - - if arnEpisode == nil { - color.Red("Anime %s Episode %d not found", anime.ID, episode.Number) - continue - } - - if arnEpisode.Links == nil { - arnEpisode.Links = map[string]string{} - } - - arnEpisode.Links["twist.moe"] = strings.Replace(episode.Link, "https://test.twist.moe/", "https://twist.moe/", 1) - } - - arn.PanicOnError(anime.Episodes().Save()) - color.Green("Found %d episodes for anime %s", len(episodes), animeID) + // Wait for rate limiter + <-rateLimiter.C } } From 474310359507fb66fa7ca242c954373073b922a8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 15:13:42 +0200 Subject: [PATCH 219/527] Fixed airing date style --- pages/animelist/animelist.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 6ac35260..d339373f 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -57,7 +57,7 @@ .anime-list-item-airing-date display block text-align right - flex-basis 100px + flex-basis 120px < 1100px .anime-list-item-rating From 61c0f2a6d2d1e6494322371331cde8c2917c1e04 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 16:54:17 +0200 Subject: [PATCH 220/527] Japanese title tokenizer --- mixins/Japanese.pixy | 11 +++++++++++ pages/anime/anime.pixy | 4 ++-- pages/anime/anime.scarlet | 3 ++- styles/typography.scarlet | 14 +++++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 mixins/Japanese.pixy diff --git a/mixins/Japanese.pixy b/mixins/Japanese.pixy new file mode 100644 index 00000000..9f8388df --- /dev/null +++ b/mixins/Japanese.pixy @@ -0,0 +1,11 @@ +component Japanese(text string) + if arn.ContainsUnicodeLetters(text) + for _, token := range arn.TokenizeJapanese(text) + if token.NeedsFurigana() + a.japanese(href="http://jisho.org/search/" + token.Original, target="_blank", rel="noopener") + ruby(title=token.Romaji)= token.Original + rt.furigana= token.Hiragana + else + ruby.japanese(title=token.Romaji)= token.Original + else + span.japanese= text \ No newline at end of file diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index c67c4ac5..10b31742 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -14,7 +14,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis //- else if anime.Title.Japanese != anime.Title.Canonical h2.anime-alternative-title - a(href="http://jisho.org/search/" + anime.Title.Japanese, target="_blank", title="Look up reading on jisho.org", rel="nofollow")= anime.Title.Japanese + Japanese(anime.Title.Japanese) //- h3.anime-section-name.anime-summary-header Summary p.anime-summary= anime.Summary @@ -161,7 +161,7 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis span= episode.Number td.episode-title if episode.Title.Japanese != "" - span= episode.Title.Japanese + Japanese(episode.Title.Japanese) else span - td.episode-actions diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index c02f0ade..00de8545 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -43,7 +43,8 @@ text-align left font-weight normal line-height content-line-height - a + + .japanese color rgba(60, 60, 60, 0.5) !important .anime-actions diff --git a/styles/typography.scarlet b/styles/typography.scarlet index f2590215..2b546a2d 100644 --- a/styles/typography.scarlet +++ b/styles/typography.scarlet @@ -27,4 +27,16 @@ p > img max-width 100% border-radius 3px display inherit - margin 0 auto \ No newline at end of file + margin 0 auto + +.furigana + opacity 0.25 + transition opacity transition-speed ease, transform transition-speed ease + transform translateY(0) + +.japanese + color text-color + :hover + .furigana + opacity 1 + transform translateY(-2px) \ No newline at end of file From 3917586bd50df54792f8a120ced2d9c0db6813e5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 17:15:45 +0200 Subject: [PATCH 221/527] Fixed HTML5 errors --- mixins/Japanese.pixy | 1 + 1 file changed, 1 insertion(+) diff --git a/mixins/Japanese.pixy b/mixins/Japanese.pixy index 9f8388df..2f660537 100644 --- a/mixins/Japanese.pixy +++ b/mixins/Japanese.pixy @@ -7,5 +7,6 @@ component Japanese(text string) rt.furigana= token.Hiragana else ruby.japanese(title=token.Romaji)= token.Original + rt.furigana else span.japanese= text \ No newline at end of file From 1da3e52c558ffd71732209a4436a3c087524dc1b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 11 Jul 2017 17:18:19 +0200 Subject: [PATCH 222/527] Fixed HTML5 errors --- mixins/AnimeGrid.pixy | 2 +- mixins/Avatar.pixy | 2 +- mixins/ProfileImage.pixy | 2 +- mixins/SoundTrack.pixy | 4 ++-- pages/profile/profile.pixy | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mixins/AnimeGrid.pixy b/mixins/AnimeGrid.pixy index 43537328..71f62dd6 100644 --- a/mixins/AnimeGrid.pixy +++ b/mixins/AnimeGrid.pixy @@ -2,4 +2,4 @@ component AnimeGrid(animeList []*arn.Anime) .anime-grid each anime in animeList a.anime-grid-cell.ajax(href="/anime/" + toString(anime.ID)) - img.anime-grid-image.lazy(data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")") \ No newline at end of file + img.anime-grid-image.lazy(src="", data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")") \ No newline at end of file diff --git a/mixins/Avatar.pixy b/mixins/Avatar.pixy index 74bfd4d4..526ef8e1 100644 --- a/mixins/Avatar.pixy +++ b/mixins/Avatar.pixy @@ -4,7 +4,7 @@ component Avatar(user *arn.User) component AvatarNoLink(user *arn.User) if user.HasAvatar() - img.user-image.lazy(data-src=user.SmallAvatar(), data-webp="true", alt=user.Nick) + img.user-image.lazy(src="", data-src=user.SmallAvatar(), data-webp="true", alt=user.Nick) else SVGAvatar diff --git a/mixins/ProfileImage.pixy b/mixins/ProfileImage.pixy index 9b7a7735..958d2bce 100644 --- a/mixins/ProfileImage.pixy +++ b/mixins/ProfileImage.pixy @@ -1,6 +1,6 @@ component ProfileImage(user *arn.User) if user.HasAvatar() - img.profile-image.lazy(data-src=user.LargeAvatar(), data-webp="true", alt="Profile image") + img.profile-image.lazy(src="", data-src=user.LargeAvatar(), data-webp="true", alt="Profile image") else svg.profile-image(viewBox="0 0 50 50", alt="Profile image") circle.head(cx="25", cy="19", r="10") diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index 41931775..970180fc 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -9,9 +9,9 @@ component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia) .sound-track.mountable(id=track.ID) .sound-track-content a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) - img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) + img.sound-track-anime-image.lazy(src="", data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - iframe.lazy(data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") + iframe.lazy(src="", data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") .sound-track-footer a.ajax(href=track.Link()) Icon("music") diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index d5586a36..cc34463e 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -102,7 +102,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .profile-watching-list.mountable each item in animeList.Items a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") - img.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + img.profile-watching-list-item-image.lazy(src="", data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) //- .profile-category.mountable //- h3 From ed940c6543cf1de1957dfff33333babbdfb211dc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 12 Jul 2017 00:55:24 +0200 Subject: [PATCH 223/527] Japanese tokenizer in external library now --- mixins/Japanese.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/Japanese.pixy b/mixins/Japanese.pixy index 2f660537..24c979e1 100644 --- a/mixins/Japanese.pixy +++ b/mixins/Japanese.pixy @@ -1,6 +1,6 @@ component Japanese(text string) if arn.ContainsUnicodeLetters(text) - for _, token := range arn.TokenizeJapanese(text) + for _, token := range japanese.Tokenize(text) if token.NeedsFurigana() a.japanese(href="http://jisho.org/search/" + token.Original, target="_blank", rel="noopener") ruby(title=token.Romaji)= token.Original From a74e314dcb467e2788b36e2169c7c861b1da6b51 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 12 Jul 2017 02:57:53 +0200 Subject: [PATCH 224/527] Added anime twist cache --- jobs/jobs.go | 1 + jobs/twist/twist.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/jobs/jobs.go b/jobs/jobs.go index 7b938c83..f3fd08ad 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -30,6 +30,7 @@ var jobs = map[string]time.Duration{ "statistics": 15 * time.Minute, "popular-anime": 20 * time.Minute, "avatars": 30 * time.Minute, + "twist": 8 * time.Hour, "sync-shoboi": 8 * time.Hour, "refresh-episodes": 10 * time.Hour, "refresh-track-titles": 10 * time.Hour, diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index 0182a389..04f0219a 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -18,6 +18,11 @@ func main() { arn.PanicOnError(err) idList := twistAnime.KitsuIDs() + // Save index in cache + arn.PanicOnError(arn.DB.Set("Cache", "animetwist index", &arn.ListOfIDs{ + IDList: idList, + })) + color.Yellow("Refreshing twist.moe links for %d anime", len(idList)) for count, animeID := range idList { From ab938e804c554964bbb57f1c748c39d9516814b1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 12 Jul 2017 08:24:26 +0200 Subject: [PATCH 225/527] Added Kitsu importer --- main.go | 3 + pages/listimport/listimport.pixy | 7 ++ pages/listimport/listimportkitsu/kitsu.go | 133 ++++++++++++++++++++ pages/listimport/listimportkitsu/kitsu.pixy | 23 ++++ tests.go | 2 + 5 files changed, 168 insertions(+) create mode 100644 pages/listimport/listimportkitsu/kitsu.go create mode 100644 pages/listimport/listimportkitsu/kitsu.pixy diff --git a/main.go b/main.go index 23f79b31..2d47d72e 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/listimport" "github.com/animenotifier/notify.moe/pages/listimport/listimportanilist" + "github.com/animenotifier/notify.moe/pages/listimport/listimportkitsu" "github.com/animenotifier/notify.moe/pages/listimport/listimportmyanimelist" "github.com/animenotifier/notify.moe/pages/login" "github.com/animenotifier/notify.moe/pages/music" @@ -112,6 +113,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/import/anilist/animelist/finish", listimportanilist.Finish) app.Ajax("/import/myanimelist/animelist", listimportmyanimelist.Preview) app.Ajax("/import/myanimelist/animelist/finish", listimportmyanimelist.Finish) + app.Ajax("/import/kitsu/animelist", listimportkitsu.Preview) + app.Ajax("/import/kitsu/animelist/finish", listimportkitsu.Finish) // Genres // app.Ajax("/genres", genres.Get) diff --git a/pages/listimport/listimport.pixy b/pages/listimport/listimport.pixy index eaac0fbe..17a65ab6 100644 --- a/pages/listimport/listimport.pixy +++ b/pages/listimport/listimport.pixy @@ -6,6 +6,13 @@ component ImportLists(user *arn.User) a.button.mountable.ajax(href="/import/anilist/animelist") Icon("download") span Import AniList + + if user.Accounts.Kitsu.Nick != "" + label Kitsu: + .widget-input + a.button.mountable.ajax(href="/import/kitsu/animelist") + Icon("download") + span Import Kitsu if user.Accounts.MyAnimeList.Nick != "" label MyAnimeList: diff --git a/pages/listimport/listimportkitsu/kitsu.go b/pages/listimport/listimportkitsu/kitsu.go new file mode 100644 index 00000000..ebdcd7a3 --- /dev/null +++ b/pages/listimport/listimportkitsu/kitsu.go @@ -0,0 +1,133 @@ +package listimportkitsu + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/kitsu" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Preview shows an import preview. +func Preview(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + matches, response := getMatches(ctx) + + if response != "" { + return response + } + + return ctx.HTML(components.ImportKitsu(user, matches)) +} + +// Finish ... +func Finish(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + matches, response := getMatches(ctx) + + if response != "" { + return response + } + + animeList := user.AnimeList() + + for _, match := range matches { + if match.ARNAnime == nil || match.KitsuItem == nil { + continue + } + + rating := match.KitsuItem.Attributes.RatingTwenty + + if rating < 2 { + rating = 2 + } + + if rating > 20 { + rating = 20 + } + + // Convert rating + convertedRating := (float64(rating-2) / 18.0) * 10.0 + + item := &arn.AnimeListItem{ + AnimeID: match.ARNAnime.ID, + Status: arn.KitsuStatusToARNStatus(match.KitsuItem.Attributes.Status), + Episodes: match.KitsuItem.Attributes.Progress, + Notes: match.KitsuItem.Attributes.Notes, + Rating: &arn.AnimeRating{ + Overall: convertedRating, + }, + RewatchCount: match.KitsuItem.Attributes.ReconsumeCount, + Created: arn.DateTimeUTC(), + Edited: arn.DateTimeUTC(), + } + + animeList.Import(item) + } + + err := animeList.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving your anime list", err) + } + + return ctx.Redirect("/+" + user.Nick + "/animelist") +} + +// getMatches finds and returns all matches for the logged in user. +func getMatches(ctx *aero.Context) ([]*arn.KitsuMatch, string) { + user := utils.GetUser(ctx) + + if user == nil { + return nil, ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + kitsuUser, err := kitsu.GetUser(user.Accounts.Kitsu.Nick) + + if err != nil { + return nil, ctx.Error(http.StatusBadRequest, "Couldn't load your user info from Kitsu", err) + } + + library := kitsuUser.StreamLibraryEntries() + matches := findAllMatches(library) + + return matches, "" +} + +// findAllMatches returns all matches for the anime inside an anilist anime list. +func findAllMatches(library chan *kitsu.LibraryEntry) []*arn.KitsuMatch { + matches := []*arn.KitsuMatch{} + + for item := range library { + // Ignore non-anime entries + if item.Anime == nil { + continue + } + + var anime *arn.Anime + connection, err := arn.GetKitsuToAnime(item.Anime.ID) + + if err == nil { + anime, _ = arn.GetAnime(connection.AnimeID) + } + + matches = append(matches, &arn.KitsuMatch{ + KitsuItem: item, + ARNAnime: anime, + }) + } + + return matches +} diff --git a/pages/listimport/listimportkitsu/kitsu.pixy b/pages/listimport/listimportkitsu/kitsu.pixy new file mode 100644 index 00000000..1a39b7a8 --- /dev/null +++ b/pages/listimport/listimportkitsu/kitsu.pixy @@ -0,0 +1,23 @@ +component ImportKitsu(user *arn.User, matches []*arn.KitsuMatch) + h1= "kitsu.io Import (" + user.Accounts.Kitsu.Nick + ", " + toString(len(matches)) + " anime)" + + table.import-list + thead + tr + th kitsu.io + th notify.moe + tbody + each match in matches + tr + td + a(href=match.KitsuItem.Anime.Link(), target="_blank", rel="noopener")= match.KitsuItem.Anime.Attributes.CanonicalTitle + td + if match.ARNAnime == nil + span.import-error Not found on notify.moe + else + a(href=match.ARNAnime.Link(), target="_blank", rel="noopener")= match.ARNAnime.Title.Canonical + + .buttons + a.button.mountable(href="/import/kitsu/animelist/finish") + Icon("refresh") + span Import \ No newline at end of file diff --git a/tests.go b/tests.go index 7fe2a5ed..fdc299a8 100644 --- a/tests.go +++ b/tests.go @@ -183,6 +183,8 @@ var routeTests = map[string][]string{ "/import/anilist/animelist/finish": nil, "/import/myanimelist/animelist": nil, "/import/myanimelist/animelist/finish": nil, + "/import/kitsu/animelist": nil, + "/import/kitsu/animelist/finish": nil, "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, From 9d309b2e8c5b35401d39efef8fda9d7deafea466 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 12 Jul 2017 20:37:34 +0200 Subject: [PATCH 226/527] Added status messages --- auth/google.go | 4 ++ layout/layout.pixy | 7 +++ mixins/Postable.pixy | 8 +-- .../delete-private-data.go | 59 ++++++++++--------- scripts/Actions.ts | 29 +++++---- scripts/AnimeNotifier.ts | 8 +++ scripts/StatusMessage.ts | 37 ++++++++++++ styles/status-message.scarlet | 18 ++++++ utils/Icon.go | 2 +- 9 files changed, 124 insertions(+), 48 deletions(-) create mode 100644 scripts/StatusMessage.ts create mode 100644 styles/status-message.scarlet diff --git a/auth/google.go b/auth/google.go index 7839beb9..8355305a 100644 --- a/auth/google.go +++ b/auth/google.go @@ -90,6 +90,10 @@ func InstallGoogleAuth(app *aero.Application) { return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err) } + if googleUser.Sub == "" { + return ctx.Error(http.StatusBadRequest, "Failed retrieving Google data", errors.New("Empty ID")) + } + // Change googlemail.com to gmail.com googleUser.Email = strings.Replace(googleUser.Email, "googlemail.com", "gmail.com", 1) diff --git a/layout/layout.pixy b/layout/layout.pixy index 3ab5cb01..c6cac28d 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -25,10 +25,17 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openG #content-container main#content.fade!= content LoadingAnimation + StatusMessage if user != nil #user(data-id=user.ID) script(src="/scripts") +component StatusMessage + #status-message.fade.fade-out + #status-message-text + a.status-message-action.action(href="#", data-trigger="click", data-action="closeStatusMessage") + RawIcon("close") + component LoadingAnimation #loading.sk-cube-grid.fade .sk-cube.hide diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index 6df39fff..c0c8e366 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -33,18 +33,18 @@ component Postable(post arn.Postable, user *arn.User, highlightAuthorID string) if user.ID != post.Author().ID if post.LikedBy(user.ID) a.post-tool.post-unlike.action(id="unlike-" + post.ID(), title="Unlike", data-action="unlike", data-trigger="click") - RawIcon("thumbs-down") + Icon("thumbs-down") else a.post-tool.post-like.action(id="like-" + post.ID(), title="Like", data-action="like", data-trigger="click") - RawIcon("thumbs-up") + Icon("thumbs-up") if user.ID == post.Author().ID a.post-tool.post-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID(), title="Edit") - RawIcon("pencil") + Icon("pencil") if post.Type() != "Thread" a.post-tool.post-permalink.ajax(href=post.Link(), title="Permalink") - RawIcon("link") + Icon("link") //- if type === "Messages" && user && (user.ID === post.authorId || user.ID === post.recipientId) //- a.post-tool.post-delete(onclick=`if(confirm("Do you really want to delete this ${typeSingular.toLowerCase()} from ${post.author.nick}?")) $.delete${typeSingular}("${post.ID}")`, title="Delete") diff --git a/patches/delete-private-data/delete-private-data.go b/patches/delete-private-data/delete-private-data.go index 94df6456..6b3d4ad2 100644 --- a/patches/delete-private-data/delete-private-data.go +++ b/patches/delete-private-data/delete-private-data.go @@ -1,40 +1,43 @@ package main -// This patch is disabled because it would be dangerous to run it accidentally. +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) func main() { - // color.Yellow("Deleting private user data") + color.Yellow("Deleting private user data") - // // Get a stream of all users - // allUsers, err := arn.StreamUsers() + // Get a stream of all users + allUsers, err := arn.StreamUsers() - // if err != nil { - // panic(err) - // } + if err != nil { + panic(err) + } - // arn.DB.DeleteTable("EmailToUser") - // arn.DB.DeleteTable("GoogleToUser") + arn.DB.DeleteTable("EmailToUser") + arn.DB.DeleteTable("GoogleToUser") - // // Iterate over the stream - // count := 0 - // for user := range allUsers { - // count++ - // println(count, user.Nick) + // Iterate over the stream + count := 0 + for user := range allUsers { + count++ + println(count, user.Nick) - // // Delete private data - // user.Email = "" - // user.Gender = "" - // user.FirstName = "" - // user.LastName = "" - // user.IP = "" - // user.Accounts.Facebook.ID = "" - // user.Accounts.Google.ID = "" - // user.AgeRange = arn.UserAgeRange{} - // user.Location = arn.UserLocation{} + // Delete private data + user.Email = "" + user.Gender = "" + user.FirstName = "" + user.LastName = "" + user.IP = "" + user.Accounts.Facebook.ID = "" + user.Accounts.Google.ID = "" + user.AgeRange = arn.UserAgeRange{} + user.Location = arn.UserLocation{} - // // Save in DB - // user.Save() - // } + // Save in DB + user.Save() + } - // color.Green("Finished.") + color.Green("Finished.") } diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 119512c3..d4aafbd4 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -40,7 +40,7 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE throw body } }) - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) .then(() => { arn.loading(false) @@ -54,6 +54,11 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE }) } +// Close status message +export function closeStatusMessage(arn: AnimeNotifier) { + arn.statusMessage.close() +} + // Load export function load(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") @@ -112,7 +117,7 @@ export function savePost(arn: AnimeNotifier, element: HTMLElement) { arn.post(apiEndpoint, updates) .then(() => arn.reloadContent()) - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) } // like @@ -121,7 +126,7 @@ export function like(arn: AnimeNotifier, element: HTMLElement) { arn.post(apiEndpoint + "/like", null) .then(() => arn.reloadContent()) - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) } // unlike @@ -130,7 +135,7 @@ export function unlike(arn: AnimeNotifier, element: HTMLElement) { arn.post(apiEndpoint + "/unlike", null) .then(() => arn.reloadContent()) - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) } // Forum reply @@ -147,7 +152,7 @@ export function forumReply(arn: AnimeNotifier) { arn.post("/api/new/post", post) .then(() => arn.reloadContent()) .then(() => textarea.value = "") - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) } // Create thread @@ -164,7 +169,7 @@ export function createThread(arn: AnimeNotifier) { arn.post("/api/new/thread", thread) .then(() => arn.app.load("/forum/" + thread.tags[0])) - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) } // Create soundtrack @@ -180,15 +185,9 @@ export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) tags: [anime.value, osu.value], } - button.innerText = "Adding..." - button.disabled = true - arn.post("/api/new/soundtrack", soundtrack) .then(() => arn.app.load("/music")) - .catch(err => { - console.error(err) - arn.reloadContent() - }) + .catch(err => arn.statusMessage.showError(err)) } // Search @@ -250,7 +249,7 @@ export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) { return arn.reloadContent() }) - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) .then(() => arn.loading(false)) } @@ -274,7 +273,7 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen return arn.app.load("/+" + userNick + "/animelist") }) - .catch(console.error) + .catch(err => arn.statusMessage.showError(err)) .then(() => arn.loading(false)) } diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 3e3cc16a..12501b26 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -3,6 +3,7 @@ import { Diff } from "./Diff" import { displayAiringDate, displayDate } from "./DateView" import { findAll, delay, canUseWebP } from "./Utils" import { MutationQueue } from "./MutationQueue" +import { StatusMessage } from "./StatusMessage" import * as actions from "./Actions" export class AnimeNotifier { @@ -10,6 +11,7 @@ export class AnimeNotifier { user: HTMLElement title: string webpEnabled: boolean + statusMessage: StatusMessage visibilityObserver: IntersectionObserver imageFound: MutationQueue @@ -89,6 +91,12 @@ export class AnimeNotifier { this.app.content = this.app.find("content") this.app.loading = this.app.find("loading") + // Status message + this.statusMessage = new StatusMessage( + this.app.find("status-message"), + this.app.find("status-message-text") + ) + // Let's start this.app.run() } diff --git a/scripts/StatusMessage.ts b/scripts/StatusMessage.ts new file mode 100644 index 00000000..0820b90e --- /dev/null +++ b/scripts/StatusMessage.ts @@ -0,0 +1,37 @@ +import { delay } from "./Utils" + +export class StatusMessage { + container: HTMLElement + text: HTMLElement + + constructor(container: HTMLElement, text: HTMLElement) { + this.container = container + this.text = text + } + + show(message: string, duration?: number) { + let messageId = String(Date.now()) + + this.text.innerText = message + + this.container.classList.remove("fade-out") + this.container.dataset.messageId = messageId + + delay(duration || 4000).then(() => { + if(this.container.dataset.messageId !== messageId) { + return + } + + this.close() + }) + } + + showError(message: string, duration?: number) { + this.show(message, duration) + this.container.classList.add("error-message") + } + + close() { + this.container.classList.add("fade-out") + } +} \ No newline at end of file diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet new file mode 100644 index 00000000..e0c916a7 --- /dev/null +++ b/styles/status-message.scarlet @@ -0,0 +1,18 @@ +#status-message + horizontal + position fixed + bottom 0 + left 0 + width 100% + padding calc(content-padding / 2) content-padding + +#status-message-text + flex 1 + text-align center + +.status-message-action + color white !important + +.error-message + color white + background-color hsl(0, 75%, 50%) \ No newline at end of file diff --git a/utils/Icon.go b/utils/Icon.go index 87886781..dfc6ea25 100644 --- a/utils/Icon.go +++ b/utils/Icon.go @@ -24,5 +24,5 @@ func Icon(name string) string { // RawIcon ... func RawIcon(name string) string { - return strings.Replace(svgIcons[name], "class='icon'", "class='raw-icon'", 1) + return strings.Replace(svgIcons[name], "class='icon", "class='raw-icon", 1) } From da57b00ff21f353dd1f55157aa637cacc8873bd9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 12 Jul 2017 22:45:33 +0200 Subject: [PATCH 227/527] New tokenizer --- mixins/Japanese.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixins/Japanese.pixy b/mixins/Japanese.pixy index 24c979e1..9ebe2bbd 100644 --- a/mixins/Japanese.pixy +++ b/mixins/Japanese.pixy @@ -1,7 +1,7 @@ component Japanese(text string) if arn.ContainsUnicodeLetters(text) - for _, token := range japanese.Tokenize(text) - if token.NeedsFurigana() + for _, token := range arn.JapaneseTokenizer.Tokenize(text) + if token.Furigana a.japanese(href="http://jisho.org/search/" + token.Original, target="_blank", rel="noopener") ruby(title=token.Romaji)= token.Original rt.furigana= token.Hiragana From df4b367c61e2c463e53c9eef9c7e713cc7be004e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 00:34:33 +0200 Subject: [PATCH 228/527] Improved large anime lists --- pages/animelist/animelist.pixy | 8 +++++--- pages/animelist/animelist.scarlet | 14 ++++++++++++++ pages/animelistitem/animelistitem.pixy | 2 +- pages/profile/profile.pixy | 22 ++++++++++++---------- pages/profile/watching.scarlet | 3 ++- scripts/Actions.ts | 20 ++++++++++++++------ utils/FormatRating.go | 8 ++++++++ 7 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 utils/FormatRating.go diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index d99052fe..f4e96a3b 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -36,8 +36,8 @@ component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, u //- AnimeList(animeList, user) component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User) - table - tbody.anime-list + table.anime-list + tbody each item in animeList.Items tr.anime-list-item(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) td.anime-list-item-name @@ -58,6 +58,8 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User td.anime-list-item-episodes .anime-list-item-episodes-watched .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save")= item.Episodes + if item.Status == arn.AnimeListStatusWatching + .plus-episode.action(data-action="increaseEpisode", data-trigger="click") + .anime-list-item-episodes-separator / .anime-list-item-episodes-max= item.Anime().EpisodeCountString() //- .anime-list-item-episodes-edit @@ -65,7 +67,7 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User //- RawIcon("pencil") td.anime-list-item-rating(title="Overall rating") - .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Overall) + .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save")= utils.FormatRating(item.Rating.Overall) //- td.anime-list-item-rating(title="Story rating") //- .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Story", data-type="number", data-trigger="focusout", data-action="save")= fmt.Sprintf("%.1f", item.Rating.Story) //- td.anime-list-item-rating(title="Visuals rating") diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index d339373f..faa7d726 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -24,8 +24,21 @@ white-space nowrap flex-basis 120px + :hover + .plus-episode + opacity 1 + .anime-list-item-episodes-watched flex 0.4 + horizontal + justify-content flex-end + +.plus-episode + display inline-block + cursor pointer + opacity 0 + margin-left 1px + transition opacity transition-speed ease .anime-list-item-episodes-separator flex 0.2 @@ -37,6 +50,7 @@ .anime-list-item-rating text-align right + flex-basis 70px .anime-list-item-actions display none diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index d63fa80b..fd7653b9 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -25,7 +25,7 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. InputTextArea("Notes", item.Notes, "Notes", "Your notes") .buttons.mountable - a.ajax.button(href="/+" + viewUser.Nick + "/animelist") + a.ajax.button(href="/+" + viewUser.Nick + "/animelist/" + item.Status) Icon("list") span View collection a.ajax.button(href=anime.Link()) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index cc34463e..d16aa2c3 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -52,7 +52,7 @@ component ProfileNavigation(viewUser *arn.User, uri string) Icon("th") span.tab-text Anime - a.button.tab.action(href="/+" + viewUser.Nick + "/animelist", data-action="diff", data-trigger="click") + a.button.tab.action(href="/+" + viewUser.Nick + "/animelist/watching", data-action="diff", data-trigger="click") Icon("list") span.tab-text Collection @@ -68,28 +68,29 @@ component ProfileNavigation(viewUser *arn.User, uri string) Icon("music") span.tab-text Tracks - //- if strings.Contains(uri, "/animelist") - //- StatusTabs("/+" + viewUser.Nick + "/animelist") + if strings.Contains(uri, "/animelist") + hr + StatusTabs("/+" + viewUser.Nick + "/animelist") component StatusTabs(urlPrefix string) .buttons.tabs.status-tabs - a.button.status-tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") + a.button.tab.status-tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") Icon("play") span.tab-text Watching - a.button.status-tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") + a.button.tab.status-tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") Icon("check") span.tab-text Completed - a.button.status-tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") + a.button.tab.status-tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") Icon("forward") span.tab-text Planned - a.button.status-tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") + a.button.tab.status-tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") Icon("pause") span.tab-text On Hold - a.button.status-tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") + a.button.tab.status-tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") Icon("stop") span.tab-text Dropped @@ -101,8 +102,9 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, else .profile-watching-list.mountable each item in animeList.Items - a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") - img.profile-watching-list-item-image.lazy(src="", data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + if item.Status == arn.AnimeListStatusWatching || item.Status == arn.AnimeListStatusCompleted + a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") + img.profile-watching-list-item-image.lazy(src="", data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) //- .profile-category.mountable //- h3 diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index dec436a5..128749b9 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -15,7 +15,8 @@ :hover filter saturate(1.3) -// .status-tabs +.status-tabs + // margin-top 2px // position fixed // top 4.6rem // right 1.6rem diff --git a/scripts/Actions.ts b/scripts/Actions.ts index d4aafbd4..9feecdc9 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -4,14 +4,14 @@ import { Diff } from "./Diff" import { findAll } from "./Utils" // Save new data from an input field -export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) { +export function save(arn: AnimeNotifier, input: HTMLElement) { arn.loading(true) let isContentEditable = input.isContentEditable let obj = {} - let value = isContentEditable ? input.innerText : input.value + let value = isContentEditable ? input.innerText : (input as HTMLInputElement).value - if(input.type === "number" || input.dataset.type === "number") { + if((input as HTMLInputElement).type === "number" || input.dataset.type === "number") { if(input.getAttribute("step") === "1" || input.dataset.step === "1") { obj[input.dataset.field] = parseInt(value) } else { @@ -24,7 +24,7 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE if(isContentEditable) { input.contentEditable = "false" } else { - input.disabled = true + (input as HTMLInputElement).disabled = true } let apiEndpoint = arn.findAPIEndpoint(input) @@ -47,7 +47,7 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE if(isContentEditable) { input.contentEditable = "true" } else { - input.disabled = false + (input as HTMLInputElement).disabled = false } return arn.reloadContent() @@ -59,6 +59,14 @@ export function closeStatusMessage(arn: AnimeNotifier) { arn.statusMessage.close() } +// Increase episode +export function increaseEpisode(arn: AnimeNotifier, element: HTMLElement) { + let prev = element.previousSibling as HTMLElement + let episodes = parseInt(prev.innerText) + prev.innerText = String(episodes + 1) + save(arn, prev) +} + // Load export function load(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") @@ -271,7 +279,7 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen throw body } - return arn.app.load("/+" + userNick + "/animelist") + return arn.app.load("/+" + userNick + "/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value) }) .catch(err => arn.statusMessage.showError(err)) .then(() => arn.loading(false)) diff --git a/utils/FormatRating.go b/utils/FormatRating.go new file mode 100644 index 00000000..8b5651d2 --- /dev/null +++ b/utils/FormatRating.go @@ -0,0 +1,8 @@ +package utils + +import "fmt" + +// FormatRating formats the rating number. +func FormatRating(rating float64) string { + return fmt.Sprintf("%.1f", rating) +} From 33d62020410538be1e984504ceab530cb02fff39 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 02:32:36 +0200 Subject: [PATCH 229/527] Added statistics --- main.go | 1 + pages/profile/profile.pixy | 4 +++ pages/profile/stats.go | 64 ++++++++++++++++++++++++++++++++++++++ pages/profile/stats.pixy | 15 +++++++++ styles/base.scarlet | 3 ++ utils/UserStats.go | 10 ++++++ 6 files changed, 97 insertions(+) create mode 100644 pages/profile/stats.go create mode 100644 pages/profile/stats.pixy create mode 100644 utils/UserStats.go diff --git a/main.go b/main.go index 2d47d72e..80d7d10c 100644 --- a/main.go +++ b/main.go @@ -88,6 +88,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/threads", profile.GetThreadsByUser) app.Ajax("/user/:nick/posts", profile.GetPostsByUser) app.Ajax("/user/:nick/tracks", profile.GetSoundTracksByUser) + app.Ajax("/user/:nick/stats", profile.GetStatsByUser) app.Ajax("/user/:nick/animelist", animelist.Get) app.Ajax("/user/:nick/animelist/watching", animelist.FilterByStatus(arn.AnimeListStatusWatching)) app.Ajax("/user/:nick/animelist/completed", animelist.FilterByStatus(arn.AnimeListStatusCompleted)) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index d16aa2c3..90da6855 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -67,6 +67,10 @@ component ProfileNavigation(viewUser *arn.User, uri string) a.button.tab.action(href="/+" + viewUser.Nick + "/tracks", data-action="diff", data-trigger="click") Icon("music") span.tab-text Tracks + + a.button.tab.action(href="/+" + viewUser.Nick + "/stats", data-action="diff", data-trigger="click") + Icon("area-chart") + span.tab-text Stats if strings.Contains(uri, "/animelist") hr diff --git a/pages/profile/stats.go b/pages/profile/stats.go new file mode 100644 index 00000000..05b79b81 --- /dev/null +++ b/pages/profile/stats.go @@ -0,0 +1,64 @@ +package profile + +import ( + "net/http" + "strconv" + "time" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +type stats map[string]float64 + +// GetStatsByUser shows statistics for a given user. +func GetStatsByUser(ctx *aero.Context) string { + nick := ctx.Get("nick") + viewUser, err := arn.GetUserByNick(nick) + userStats := utils.UserStats{} + ratings := stats{} + status := stats{} + types := stats{} + years := stats{} + + if err != nil { + return ctx.Error(http.StatusNotFound, "User not found", err) + } + + animeList, err := arn.GetAnimeList(viewUser) + animeList.PrefetchAnime() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Anime list not found", err) + } + + for _, item := range animeList.Items { + duration := time.Duration(item.Episodes * item.Anime().EpisodeLength) + userStats.AnimeWatchingTime += duration * time.Minute + + ratings[strconv.Itoa(int(item.Rating.Overall+0.5))]++ + status[item.Status]++ + types[item.Anime().Type]++ + + if item.Anime().StartDate != "" { + year := item.Anime().StartDate[:4] + + if year < "2000" { + year = "Before 2000" + } + + years[year]++ + } + } + + userStats.PieCharts = []*arn.PieChart{ + arn.NewPieChart("Ratings", ratings), + arn.NewPieChart("Status", status), + arn.NewPieChart("Types", types), + arn.NewPieChart("Years", years), + } + + return ctx.HTML(components.ProfileStats(&userStats, viewUser, utils.GetUser(ctx), ctx.URI())) +} diff --git a/pages/profile/stats.pixy b/pages/profile/stats.pixy new file mode 100644 index 00000000..199fbf3c --- /dev/null +++ b/pages/profile/stats.pixy @@ -0,0 +1,15 @@ +component ProfileStats(stats *utils.UserStats, viewUser *arn.User, user *arn.User, uri string) + ProfileHeader(viewUser, user, uri) + + .widgets + each pie in stats.PieCharts + .widget + h3.widget-title + Icon("pie-chart") + span= pie.Title + PieChart(pie.Slices) + + .footer.text-center + span You spent + span= int(stats.AnimeWatchingTime / time.Hour / 24) + span days watching anime. \ No newline at end of file diff --git a/styles/base.scarlet b/styles/base.scarlet index 6892df79..c2eb31d3 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -35,5 +35,8 @@ img .hidden display none !important +.text-center + text-align center + .spacer flex 1 \ No newline at end of file diff --git a/utils/UserStats.go b/utils/UserStats.go new file mode 100644 index 00000000..d4e5c2d7 --- /dev/null +++ b/utils/UserStats.go @@ -0,0 +1,10 @@ +package utils + +import "time" +import "github.com/animenotifier/arn" + +// UserStats ... +type UserStats struct { + AnimeWatchingTime time.Duration + PieCharts []*arn.PieChart +} From 37c77c1f04c25e6f8a917d5ac8749a863973c7a4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 02:53:04 +0200 Subject: [PATCH 230/527] Restored effects --- pages/animelist/animelist.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index f4e96a3b..95ccb1c3 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -39,7 +39,7 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User table.anime-list tbody each item in animeList.Items - tr.anime-list-item(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) + tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical From df98beacb4ed3eea29f2b754c0e9e35a5f442d14 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 03:02:58 +0200 Subject: [PATCH 231/527] Improved mobile version --- pages/anime/episode.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/anime/episode.scarlet b/pages/anime/episode.scarlet index 2e06b9d9..e84bdd8e 100644 --- a/pages/anime/episode.scarlet +++ b/pages/anime/episode.scarlet @@ -19,6 +19,6 @@ .episode-airing-date-start display none -< 500px +< 320px .episode-actions display none \ No newline at end of file From 84e246d31fa1a3c20f4ea83e484956d8ebe94049 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 05:47:17 +0200 Subject: [PATCH 232/527] Better stats --- pages/profile/stats.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/profile/stats.pixy b/pages/profile/stats.pixy index 199fbf3c..82e1366f 100644 --- a/pages/profile/stats.pixy +++ b/pages/profile/stats.pixy @@ -3,13 +3,13 @@ component ProfileStats(stats *utils.UserStats, viewUser *arn.User, user *arn.Use .widgets each pie in stats.PieCharts - .widget + .widget.mountable h3.widget-title Icon("pie-chart") span= pie.Title PieChart(pie.Slices) .footer.text-center - span You spent + span= viewUser.Nick + " spent " span= int(stats.AnimeWatchingTime / time.Hour / 24) span days watching anime. \ No newline at end of file From a539fa0f8350449be37f5ffb93753c9256995d54 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 07:53:36 +0200 Subject: [PATCH 233/527] Implemented anime characters --- jobs/anime-characters/anime-characters.go | 14 +++++++++++ jobs/sync-characters/sync-characters.go | 30 +++++++++++++++++++++++ mixins/Character.pixy | 4 +++ pages/anime/anime.pixy | 7 ++++++ pages/anime/character.scarlet | 25 +++++++++++++++++++ pages/animelistitem/animelistitem.pixy | 22 +++++++++-------- pages/animelistitem/animelistitem.scarlet | 12 +++++++++ 7 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 jobs/anime-characters/anime-characters.go create mode 100644 jobs/sync-characters/sync-characters.go create mode 100644 mixins/Character.pixy create mode 100644 pages/anime/character.scarlet diff --git a/jobs/anime-characters/anime-characters.go b/jobs/anime-characters/anime-characters.go new file mode 100644 index 00000000..6f84a1f1 --- /dev/null +++ b/jobs/anime-characters/anime-characters.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + anime, _ := arn.GetAnime("6887") + err := anime.RefreshAnimeCharacters() + arn.PanicOnError(err) + + color.Green("Finished.") +} diff --git a/jobs/sync-characters/sync-characters.go b/jobs/sync-characters/sync-characters.go new file mode 100644 index 00000000..11ed97ed --- /dev/null +++ b/jobs/sync-characters/sync-characters.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" + "github.com/animenotifier/kitsu" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Syncing characters with Kitsu DB") + + kitsuCharacters := kitsu.StreamCharacters() + + for kitsuCharacter := range kitsuCharacters { + character := &arn.Character{ + ID: kitsuCharacter.ID, + Name: kitsuCharacter.Attributes.Name, + Image: kitsu.FixImageURL(kitsuCharacter.Attributes.Image.Original), + Description: kitsuCharacter.Attributes.Description, + } + + fmt.Printf("%s %s\n", character.ID, character.Name) + + arn.PanicOnError(character.Save()) + } + + color.Green("Finished.") +} diff --git a/mixins/Character.pixy b/mixins/Character.pixy new file mode 100644 index 00000000..a697f00d --- /dev/null +++ b/mixins/Character.pixy @@ -0,0 +1,4 @@ +component Character(character *arn.Character) + a.character(href="#") + img.character-image.lazy(src="", data-src=character.Image, alt=character.Name, title=character.Name) + span.character-name= character.Name \ No newline at end of file diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 10b31742..63f68651 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -147,6 +147,13 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis each track in tracks SoundTrack(track) + if anime.Characters() != nil && len(anime.Characters().Items) > 0 + h3.anime-section-name Characters + .characters + each character in anime.Characters().Items + if character.Character() != nil + Character(character.Character()) + if len(anime.Episodes().Items) > 0 if episodesReversed h3.anime-section-name Latest episodes diff --git a/pages/anime/character.scarlet b/pages/anime/character.scarlet new file mode 100644 index 00000000..a8b18d08 --- /dev/null +++ b/pages/anime/character.scarlet @@ -0,0 +1,25 @@ +.characters + horizontal-wrap + +.character + vertical + align-items center + margin 0.5rem + + :hover + .character-name + opacity 1 + +.character-image + border-radius 3px + object-fit cover + +.character-name + font-size 0.8rem + color text-color + opacity 0.5 + default-transition + +.character-image + width 112px + height 175px \ No newline at end of file diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index fd7653b9..3b9ced93 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -2,17 +2,19 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. .widget-form.mountable .widget.anime-list-item-view(data-api="/api/animelist/" + viewUser.ID + "/update/" + anime.ID) h1= anime.Title.Canonical - - InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") - .widget-input - label(for="Status") Status: - select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") - option(value=arn.AnimeListStatusWatching) Watching - option(value=arn.AnimeListStatusCompleted) Completed - option(value=arn.AnimeListStatusPlanned) Plan to watch - option(value=arn.AnimeListStatusHold) On hold - option(value=arn.AnimeListStatusDropped) Dropped + .anime-list-item-progress-edit + .anime-list-item-episodes-edit + InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") + + .widget-input.anime-list-item-status-edit + label(for="Status") Status: + select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") + option(value=arn.AnimeListStatusWatching) Watching + option(value=arn.AnimeListStatusCompleted) Completed + option(value=arn.AnimeListStatusPlanned) Plan to watch + option(value=arn.AnimeListStatusHold) On hold + option(value=arn.AnimeListStatusDropped) Dropped .anime-list-item-rating-edit InputNumber("Rating.Overall", item.Rating.Overall, arn.OverallRatingName(item.Episodes), "Overall rating on a scale of 0 to 10", "0", "10", "0.1") diff --git a/pages/animelistitem/animelistitem.scarlet b/pages/animelistitem/animelistitem.scarlet index e1e7cc3e..a515568c 100644 --- a/pages/animelistitem/animelistitem.scarlet +++ b/pages/animelistitem/animelistitem.scarlet @@ -1,3 +1,15 @@ +// .anime-list-item-progress-edit +// horizontal-wrap +// justify-content space-between +// width 100% + +// .anime-list-item-episodes-edit +// flex 1 +// margin-right content-padding + +// .anime-list-item-status-edit +// flex-basis 50% + .anime-list-item-rating-edit horizontal-wrap justify-content space-between From db4d362fb3057090e0e334a0ebcea7eb6136d2e2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 08:02:41 +0200 Subject: [PATCH 234/527] Minor improvements --- pages/anime/character.scarlet | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pages/anime/character.scarlet b/pages/anime/character.scarlet index a8b18d08..adcce315 100644 --- a/pages/anime/character.scarlet +++ b/pages/anime/character.scarlet @@ -5,8 +5,12 @@ vertical align-items center margin 0.5rem + default-transition + transform scale(1) :hover + transform scale(1.05) + .character-name opacity 1 @@ -15,10 +19,10 @@ object-fit cover .character-name - font-size 0.8rem + font-size 0.75rem color text-color opacity 0.5 - default-transition + transition opacity transition-speed ease .character-image width 112px From d6155bf3a1fa3a29cf5d35b7995dd7de83190fd2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 08:23:20 +0200 Subject: [PATCH 235/527] Anime character refresh --- jobs/anime-characters/anime-characters.go | 22 +++++++++++++++++++--- jobs/sync-characters/sync-characters.go | 2 +- main.go | 4 +++- mixins/Character.pixy | 2 +- pages/anime/character.scarlet | 1 + pages/character/character.go | 21 +++++++++++++++++++++ pages/character/character.pixy | 5 +++++ pages/tracks/tracks.go | 2 +- tests.go | 4 ++-- 9 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 pages/character/character.go create mode 100644 pages/character/character.pixy diff --git a/jobs/anime-characters/anime-characters.go b/jobs/anime-characters/anime-characters.go index 6f84a1f1..7273d221 100644 --- a/jobs/anime-characters/anime-characters.go +++ b/jobs/anime-characters/anime-characters.go @@ -1,14 +1,30 @@ package main import ( + "fmt" + "time" + "github.com/animenotifier/arn" "github.com/fatih/color" ) func main() { - anime, _ := arn.GetAnime("6887") - err := anime.RefreshAnimeCharacters() - arn.PanicOnError(err) + color.Yellow("Refreshing anime characters...") + + rateLimiter := time.NewTicker(500 * time.Millisecond) + + for anime := range arn.MustStreamAnime() { + <-rateLimiter.C + + chars, err := anime.RefreshAnimeCharacters() + + if err != nil { + color.Red(err.Error()) + continue + } + + fmt.Printf("%s %s (%d characters)\n", anime.ID, anime.Title.Canonical, len(chars.Items)) + } color.Green("Finished.") } diff --git a/jobs/sync-characters/sync-characters.go b/jobs/sync-characters/sync-characters.go index 11ed97ed..3668d3fa 100644 --- a/jobs/sync-characters/sync-characters.go +++ b/jobs/sync-characters/sync-characters.go @@ -18,7 +18,7 @@ func main() { ID: kitsuCharacter.ID, Name: kitsuCharacter.Attributes.Name, Image: kitsu.FixImageURL(kitsuCharacter.Attributes.Image.Original), - Description: kitsuCharacter.Attributes.Description, + Description: arn.FixAnimeDescription(kitsuCharacter.Attributes.Description), } fmt.Printf("%s %s\n", character.ID, character.Name) diff --git a/main.go b/main.go index 80d7d10c..10e62049 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/animenotifier/notify.moe/pages/animelistitem" "github.com/animenotifier/notify.moe/pages/apiview" "github.com/animenotifier/notify.moe/pages/best" + "github.com/animenotifier/notify.moe/pages/character" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/editor" @@ -74,7 +75,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/forum/:tag", forum.Get) app.Ajax("/thread/:id", threads.Get) app.Ajax("/post/:id", posts.Get) - app.Ajax("/tracks/:id", tracks.Get) + app.Ajax("/track/:id", tracks.Get) + app.Ajax("/character/:id", character.Get) app.Ajax("/new/thread", newthread.Get) app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) diff --git a/mixins/Character.pixy b/mixins/Character.pixy index a697f00d..cf068890 100644 --- a/mixins/Character.pixy +++ b/mixins/Character.pixy @@ -1,4 +1,4 @@ component Character(character *arn.Character) - a.character(href="#") + a.character(href="/character/" + character.ID) img.character-image.lazy(src="", data-src=character.Image, alt=character.Name, title=character.Name) span.character-name= character.Name \ No newline at end of file diff --git a/pages/anime/character.scarlet b/pages/anime/character.scarlet index adcce315..9767743c 100644 --- a/pages/anime/character.scarlet +++ b/pages/anime/character.scarlet @@ -16,6 +16,7 @@ .character-image border-radius 3px + box-shadow shadow-medium object-fit cover .character-name diff --git a/pages/character/character.go b/pages/character/character.go new file mode 100644 index 00000000..38876149 --- /dev/null +++ b/pages/character/character.go @@ -0,0 +1,21 @@ +package character + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +// Get character. +func Get(ctx *aero.Context) string { + id := ctx.Get("id") + character, err := arn.GetCharacter(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Character not found", err) + } + + return ctx.HTML(components.CharacterDetails(character)) +} diff --git a/pages/character/character.pixy b/pages/character/character.pixy new file mode 100644 index 00000000..0963d600 --- /dev/null +++ b/pages/character/character.pixy @@ -0,0 +1,5 @@ +component CharacterDetails(character *arn.Character) + h1= character.Name + p + img(src=character.Image, alt=character.Name) + p= character.Description \ No newline at end of file diff --git a/pages/tracks/tracks.go b/pages/tracks/tracks.go index deb9efdb..dd698342 100644 --- a/pages/tracks/tracks.go +++ b/pages/tracks/tracks.go @@ -8,7 +8,7 @@ import ( "github.com/animenotifier/notify.moe/components" ) -// Get post. +// Get track. func Get(ctx *aero.Context) string { id := ctx.Get("id") track, err := arn.GetSoundTrack(id) diff --git a/tests.go b/tests.go index fdc299a8..aae85607 100644 --- a/tests.go +++ b/tests.go @@ -75,8 +75,8 @@ var routeTests = map[string][]string{ "/search/Dragon Ball", }, - "/tracks/:id": []string{ - "/tracks/h0ac8sKkg", + "/track/:id": []string{ + "/track/h0ac8sKkg", }, // API From 86b2eea13a78721632f54e1ee272d06fa838c780 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 09:20:12 +0200 Subject: [PATCH 236/527] Fix timeout issues --- jobs/anime-characters/anime-characters.go | 3 ++- jobs/avatars/avatars.go | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jobs/anime-characters/anime-characters.go b/jobs/anime-characters/anime-characters.go index 7273d221..8a54bfed 100644 --- a/jobs/anime-characters/anime-characters.go +++ b/jobs/anime-characters/anime-characters.go @@ -11,9 +11,10 @@ import ( func main() { color.Yellow("Refreshing anime characters...") + allAnime, _ := arn.AllAnime() rateLimiter := time.NewTicker(500 * time.Millisecond) - for anime := range arn.MustStreamAnime() { + for _, anime := range allAnime { <-rateLimiter.C chars, err := anime.RefreshAnimeCharacters() diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index f7516162..3a6a5bd5 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -96,8 +96,10 @@ func main() { usersQueue := make(chan *arn.User, runtime.NumCPU()) StartWorkers(usersQueue, Work) + allUsers, _ := arn.AllUsers() + // We'll send each user to one of the worker threads - for user := range arn.MustStreamUsers() { + for _, user := range allUsers { wg.Add(1) usersQueue <- user } From da0af0cd9a290412393d0d2559cd5d43263a7778 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 09:22:32 +0200 Subject: [PATCH 237/527] Improved style --- mixins/Character.pixy | 2 +- pages/anime/character.scarlet | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mixins/Character.pixy b/mixins/Character.pixy index cf068890..d401facb 100644 --- a/mixins/Character.pixy +++ b/mixins/Character.pixy @@ -1,4 +1,4 @@ component Character(character *arn.Character) a.character(href="/character/" + character.ID) img.character-image.lazy(src="", data-src=character.Image, alt=character.Name, title=character.Name) - span.character-name= character.Name \ No newline at end of file + //- span.character-name= character.Name \ No newline at end of file diff --git a/pages/anime/character.scarlet b/pages/anime/character.scarlet index 9767743c..340e2108 100644 --- a/pages/anime/character.scarlet +++ b/pages/anime/character.scarlet @@ -11,19 +11,19 @@ :hover transform scale(1.05) - .character-name - opacity 1 + // .character-name + // opacity 1 .character-image border-radius 3px box-shadow shadow-medium object-fit cover -.character-name - font-size 0.75rem - color text-color - opacity 0.5 - transition opacity transition-speed ease +// .character-name +// font-size 0.75rem +// color text-color +// opacity 0.5 +// transition opacity transition-speed ease .character-image width 112px From c76c783ed90a7b0a9f370f60551881cf99ede905 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 09:30:00 +0200 Subject: [PATCH 238/527] AJAX link --- mixins/Character.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/Character.pixy b/mixins/Character.pixy index d401facb..9e54317b 100644 --- a/mixins/Character.pixy +++ b/mixins/Character.pixy @@ -1,4 +1,4 @@ component Character(character *arn.Character) - a.character(href="/character/" + character.ID) + a.character.ajax(href="/character/" + character.ID) img.character-image.lazy(src="", data-src=character.Image, alt=character.Name, title=character.Name) //- span.character-name= character.Name \ No newline at end of file From 72d8a94b175cb93d2e973f17665a7b221f4277ab Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 10:50:17 +0200 Subject: [PATCH 239/527] Fixed nav bar --- styles/status-message.scarlet | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet index e0c916a7..9d7ae0eb 100644 --- a/styles/status-message.scarlet +++ b/styles/status-message.scarlet @@ -5,6 +5,7 @@ left 0 width 100% padding calc(content-padding / 2) content-padding + pointer-events none #status-message-text flex 1 From 0f59969f0db082d2ab3c17cd6f0421f3a31d1c44 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 16:37:51 +0200 Subject: [PATCH 240/527] Increased session duration --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 10e62049..cf644c5e 100644 --- a/main.go +++ b/main.go @@ -58,7 +58,7 @@ func configure(app *aero.Application) *aero.Application { app.SetStyle(css.Bundle()) // Sessions - app.Sessions.Duration = 3600 * 24 * 7 + app.Sessions.Duration = 3600 * 24 * 30 app.Sessions.Store = aerospikestore.New(arn.DB, "Session", app.Sessions.Duration) // Layout From 4f7c2e95f08dd86bce30bbb2c5fe4325f0da09d6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 17:56:14 +0200 Subject: [PATCH 241/527] Started working on service worker --- assets.go | 12 ++++++++++++ layout/layout.pixy | 2 +- scripts/AnimeNotifier.ts | 17 ++++++++++++++++- sw/service-worker.ts | 5 +++++ 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 sw/service-worker.ts diff --git a/assets.go b/assets.go index 8f3fc1e1..48f3b1eb 100644 --- a/assets.go +++ b/assets.go @@ -1,6 +1,7 @@ package main import ( + "io/ioutil" "strings" "github.com/aerogo/aero" @@ -10,6 +11,12 @@ import ( func init() { // Scripts scripts := js.Bundle() + serviceWorkerBytes, err := ioutil.ReadFile("sw/service-worker.js") + serviceWorker := string(serviceWorkerBytes) + + if err != nil { + panic("Couldn't load service worker") + } app.Get("/scripts", func(ctx *aero.Context) string { ctx.SetResponseHeader("Content-Type", "application/javascript") @@ -21,6 +28,11 @@ func init() { return scripts }) + app.Get("/service-worker", func(ctx *aero.Context) string { + ctx.SetResponseHeader("Content-Type", "application/javascript") + return serviceWorker + }) + // Web manifest app.Get("/manifest.json", func(ctx *aero.Context) string { return ctx.JSON(app.Config.Manifest) diff --git a/layout/layout.pixy b/layout/layout.pixy index c6cac28d..313da78e 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -33,7 +33,7 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openG component StatusMessage #status-message.fade.fade-out #status-message-text - a.status-message-action.action(href="#", data-trigger="click", data-action="closeStatusMessage") + a.status-message-action.action(href="#", data-trigger="click", data-action="closeStatusMessage", aria-label="Close status message") RawIcon("close") component LoadingAnimation diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 12501b26..4301a1e2 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -97,7 +97,7 @@ export class AnimeNotifier { this.app.find("status-message-text") ) - // Let's start + // Let"s start this.app.run() } @@ -123,6 +123,21 @@ export class AnimeNotifier { } onIdle() { + this.registerServiceWorker() + this.pushAnalytics() + } + + registerServiceWorker() { + navigator.serviceWorker.register("service-worker", { + scope: "/" + }) + + navigator.serviceWorker.ready.then(() => { + console.log("Service worker registered.") + }) + } + + pushAnalytics() { if(!this.user) { return } diff --git a/sw/service-worker.ts b/sw/service-worker.ts new file mode 100644 index 00000000..7deffa06 --- /dev/null +++ b/sw/service-worker.ts @@ -0,0 +1,5 @@ +// pack:ignore + +self.addEventListener("install", evt => { + console.log("The service worker is being installed.") +}) \ No newline at end of file From 3740ec61d676505a536c53b1074715f61d3435d5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Jul 2017 20:45:16 +0200 Subject: [PATCH 242/527] Added caching to service worker --- pages/frontpage/frontpage.scarlet | 24 +++++++++++ pages/login/login.pixy | 10 +++-- scripts/AnimeNotifier.ts | 8 ++-- sw/service-worker.ts | 72 ++++++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/pages/frontpage/frontpage.scarlet b/pages/frontpage/frontpage.scarlet index f13e5a70..1d8a3285 100644 --- a/pages/frontpage/frontpage.scarlet +++ b/pages/frontpage/frontpage.scarlet @@ -39,6 +39,30 @@ width auto height auto z-index -100 + background rgb(32, 32, 32) + +.login-button + horizontal + align-items center + border-radius 3px + padding 0.75rem 1.25rem + margin 0.5rem + font-size 1.2rem + text-shadow none !important + box-shadow shadow-medium + default-transition + +.login-button-google + background hsl(8, 75%, 43%) + + :hover + background hsl(8, 75%, 48%) + +.login-button-facebook + background hsl(222, 67%, 42%) + + :hover + background hsl(222, 67%, 47%) .screenshot max-width 100% diff --git a/pages/login/login.pixy b/pages/login/login.pixy index 883cd798..23396163 100644 --- a/pages/login/login.pixy +++ b/pages/login/login.pixy @@ -1,7 +1,9 @@ component Login .login-buttons - a.login-button(href="/auth/google") - img.login-button-image(src="/images/login/google", alt="Google Login", title="Login with your Google account") + a.login-button.login-button-google(href="/auth/google") + Icon("google") + span Sign in via Google - a.login-button(href="/auth/facebook") - img.login-button-image(src="/images/login/facebook", alt="Facebook Login", title="Login with your Facebook account") \ No newline at end of file + a.login-button.login-button-facebook(href="/auth/facebook") + Icon("facebook") + span Sign in via Facebook \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 4301a1e2..13f4d0a0 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -129,12 +129,12 @@ export class AnimeNotifier { registerServiceWorker() { navigator.serviceWorker.register("service-worker", { - scope: "/" + scope: "./" }) - navigator.serviceWorker.ready.then(() => { - console.log("Service worker registered.") - }) + // navigator.serviceWorker.ready.then(() => { + // console.log("Service worker registered.") + // }) } pushAnalytics() { diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 7deffa06..0c93229d 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -1,5 +1,73 @@ // pack:ignore -self.addEventListener("install", evt => { +var CACHE = "v-alpha" + +self.addEventListener("install", (evt: any) => { console.log("The service worker is being installed.") -}) \ No newline at end of file + + evt.waitUntil(installCache()) +}) + +self.addEventListener("activate", (evt: any) => { + evt.waitUntil((self as any).clients.claim()) +}) + +self.addEventListener("fetch", async (evt: any) => { + let request = evt.request + + console.log("Serving:", request.url, request, request.method) + + // Do not use cache in some cases + if(request.method !== "GET" || request.url.includes("/auth/") || request.url.includes("chrome-extension")) { + return evt.waitUntil(evt.respondWith(fetch(request))) + } + + // Start fetching the request + let refresh = fetch(request).then(response => { + let clone = response.clone() + + // Save the new version of the resource in the cache + caches.open(CACHE).then(cache => { + cache.put(request, clone) + }) + + return response + }) + + // Try to serve cache first and fall back to network response + let networkOrCache = fromCache(request).catch(error => { + console.log("Cache MISS:", request.url) + return refresh + }) + + return evt.waitUntil(evt.respondWith(networkOrCache)) +}) + +function installCache() { + return caches.open(CACHE).then(cache => { + return cache.addAll([ + "./", + "./scripts", + "https://fonts.gstatic.com/s/ubuntu/v10/2Q-AW1e_taO6pHwMXcXW5w.ttf" + ]) + }) +} + +function fromCache(request) { + return caches.open(CACHE).then(cache => { + return cache.match(request).then(matching => { + if(matching) { + console.log("Cache HIT:", request.url) + return Promise.resolve(matching) + } + + return Promise.reject("no-match") + }) + }) +} + +function updateCache(request, response) { + return caches.open(CACHE).then(cache => { + cache.put(request, response) + }) +} From 1a5cd8494e2f6b8a1003757cc50b8d3a5d1937e1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 14 Jul 2017 00:11:25 +0200 Subject: [PATCH 243/527] Added service worker --- pages/login/login.scarlet | 6 ++-- scripts/AnimeNotifier.ts | 70 ++++++++++++++++++++++++++++++++++++--- scripts/Application.ts | 3 ++ sw/service-worker.ts | 64 +++++++++++++++++++++++++++-------- 4 files changed, 121 insertions(+), 22 deletions(-) diff --git a/pages/login/login.scarlet b/pages/login/login.scarlet index 36972b8f..c9c80033 100644 --- a/pages/login/login.scarlet +++ b/pages/login/login.scarlet @@ -5,7 +5,7 @@ .login-button padding 0.5rem + color white -.login-button-image - max-width 236px - max-height 44px \ No newline at end of file + :hover + color white \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 13f4d0a0..1bab02e4 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -99,6 +99,9 @@ export class AnimeNotifier { // Let"s start this.app.run() + + // Service worker + this.registerServiceWorker() } onContentLoaded() { @@ -123,18 +126,50 @@ export class AnimeNotifier { } onIdle() { - this.registerServiceWorker() this.pushAnalytics() } registerServiceWorker() { + if(!("serviceWorker" in navigator)) { + return + } + navigator.serviceWorker.register("service-worker", { scope: "./" + }).then(registration => { + registration.update() }) - // navigator.serviceWorker.ready.then(() => { - // console.log("Service worker registered.") - // }) + navigator.serviceWorker.onmessage = evt => { + this.onServiceWorkerMessage(evt) + } + } + + onServiceWorkerMessage(evt: ServiceWorkerMessageEvent) { + let message = JSON.parse(evt.data) + console.log(message.url, this.app.eTag, message.eTag) + + switch(message.type) { + case "content changed": + // If we don't have an etag it means it was a full page refresh. + // In this case we don't need to reload anything. + if(!this.app.eTag) { + this.app.eTag = message.eTag + return + } + + if(this.app.eTag !== message.eTag) { + if(message.url.includes("/_/")) { + // Content reload + this.reloadContent() + } else { + // Full page reload + this.reloadPage() + } + } + + break + } } pushAnalytics() { @@ -185,14 +220,39 @@ export class AnimeNotifier { } reloadContent() { + let headers = new Headers() + headers.append("X-Reload", "true") + return fetch("/_" + this.app.currentPath, { - credentials: "same-origin" + credentials: "same-origin", + headers + }) + .then(response => { + this.app.eTag = response.headers.get("ETag") + return response }) .then(response => response.text()) .then(html => Diff.innerHTML(this.app.content, html)) .then(() => this.app.emit("DOMContentLoaded")) } + reloadPage() { + let headers = new Headers() + headers.append("X-Reload", "true") + + return fetch(this.app.currentPath, { + credentials: "same-origin", + headers + }) + .then(response => { + this.app.eTag = response.headers.get("ETag") + return response + }) + .then(response => response.text()) + .then(html => Diff.innerHTML(document.body, html)) + .then(() => this.app.emit("DOMContentLoaded")) + } + loading(isLoading: boolean) { if(isLoading) { document.body.style.cursor = "progress" diff --git a/scripts/Application.ts b/scripts/Application.ts index 04c0d13d..d1d30bd5 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -13,6 +13,7 @@ export class Application { loading: HTMLElement currentPath: string originalPath: string + eTag: string lastRequest: XMLHttpRequest constructor() { @@ -45,6 +46,8 @@ export class Application { request.onerror = () => reject(new Error("You are either offline or the requested page doesn't exist.")) request.ontimeout = () => reject(new Error("The page took too much time to respond.")) request.onload = () => { + this.eTag = request.getResponseHeader("ETag") + if(request.status < 200 || request.status >= 400) reject(request.responseText) else diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 0c93229d..1b44f0b5 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -1,26 +1,36 @@ // pack:ignore -var CACHE = "v-alpha" +var CACHE = "v-1" self.addEventListener("install", (evt: any) => { - console.log("The service worker is being installed.") - - evt.waitUntil(installCache()) + evt.waitUntil( + (self as any).skipWaiting().then(() => { + return installCache() + }) + ) }) self.addEventListener("activate", (evt: any) => { - evt.waitUntil((self as any).clients.claim()) + evt.waitUntil( + (self as any).clients.claim() + ) }) self.addEventListener("fetch", async (evt: any) => { let request = evt.request + let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") - console.log("Serving:", request.url, request, request.method) + // Delete existing cache on authentication + if(isAuth) { + caches.delete(CACHE) + } // Do not use cache in some cases - if(request.method !== "GET" || request.url.includes("/auth/") || request.url.includes("chrome-extension")) { + if(request.method !== "GET" || isAuth || request.url.includes("chrome-extension")) { return evt.waitUntil(evt.respondWith(fetch(request))) } + + let servedCachedResponse = false // Start fetching the request let refresh = fetch(request).then(response => { @@ -28,15 +38,33 @@ self.addEventListener("fetch", async (evt: any) => { // Save the new version of the resource in the cache caches.open(CACHE).then(cache => { - cache.put(request, clone) + return cache.put(request, clone) + }).then(() => { + if(!servedCachedResponse) { + return + } + + let contentType = clone.headers.get("Content-Type") + + if(contentType && contentType.startsWith("text/html") && clone.headers.get("ETag") && request.headers.get("X-Reload") !== "true") { + reloadContent(clone) + } }) return response }) + // Forced reload + if(request.headers.get("X-Reload") === "true") { + return evt.waitUntil(refresh) + } + // Try to serve cache first and fall back to network response - let networkOrCache = fromCache(request).catch(error => { - console.log("Cache MISS:", request.url) + let networkOrCache = fromCache(request).then(response => { + servedCachedResponse = true + return response + }).catch(error => { + // console.log("Cache MISS:", request.url) return refresh }) @@ -57,7 +85,7 @@ function fromCache(request) { return caches.open(CACHE).then(cache => { return cache.match(request).then(matching => { if(matching) { - console.log("Cache HIT:", request.url) + // console.log("Cache HIT:", request.url) return Promise.resolve(matching) } @@ -66,8 +94,16 @@ function fromCache(request) { }) } -function updateCache(request, response) { - return caches.open(CACHE).then(cache => { - cache.put(request, response) +function reloadContent(response) { + return (self as any).clients.matchAll().then(clients => { + clients.forEach(client => { + var message = { + type: 'content changed', + url: response.url, + eTag: response.headers.get('ETag') + } + + client.postMessage(JSON.stringify(message)) + }) }) } From b165e117bb8d2d87390f7e6a7b2c74e97cf69e3a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 14 Jul 2017 01:00:51 +0200 Subject: [PATCH 244/527] Fixed search --- scripts/Actions.ts | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 9feecdc9..d08f4eb4 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -206,10 +206,12 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard let term = search.value + arn.app.currentPath = "/search/" + term + if(window.location.pathname.startsWith("/search/")) { - history.replaceState("search", null, "/search/" + term) + history.replaceState("search", null, arn.app.currentPath) } else { - history.pushState("search", null, "/search/" + term) + history.pushState("search", null, arn.app.currentPath) } if(!term || term.length < 2) { @@ -217,24 +219,7 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard return } - var results = arn.app.find("results") - - if(!results) { - results = document.createElement("div") - results.id = "results" - arn.app.content.innerHTML = "" - arn.app.content.appendChild(results) - } - - arn.app.get("/_/search/" + term) - .then(html => { - if(!search.value) { - return - } - - Diff.innerHTML(results, html) - arn.app.emit("DOMContentLoaded") - }) + arn.reloadContent() } // Add anime to collection From f2386ee9c70a604eec56c0c3af4814449b3d0294 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 14 Jul 2017 01:07:49 +0200 Subject: [PATCH 245/527] Improved search --- scripts/AnimeNotifier.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 1bab02e4..54fe10eb 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -223,13 +223,19 @@ export class AnimeNotifier { let headers = new Headers() headers.append("X-Reload", "true") - return fetch("/_" + this.app.currentPath, { + let path = this.app.currentPath + + return fetch("/_" + path, { credentials: "same-origin", headers }) .then(response => { + if(this.app.currentPath !== path) { + return Promise.reject("old request") + } + this.app.eTag = response.headers.get("ETag") - return response + return Promise.resolve(response) }) .then(response => response.text()) .then(html => Diff.innerHTML(this.app.content, html)) From 815f99ce9cb900d432469477873898a35a1e2a89 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 14 Jul 2017 01:50:10 +0200 Subject: [PATCH 246/527] More fixes --- scripts/Actions.ts | 10 +--------- scripts/AnimeNotifier.ts | 6 ++---- sw/service-worker.ts | 2 +- utils/AllowEmbed.go | 2 +- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index d08f4eb4..bd5672d2 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -206,20 +206,12 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard let term = search.value - arn.app.currentPath = "/search/" + term - - if(window.location.pathname.startsWith("/search/")) { - history.replaceState("search", null, arn.app.currentPath) - } else { - history.pushState("search", null, arn.app.currentPath) - } - if(!term || term.length < 2) { arn.app.content.innerHTML = "Please enter at least 2 characters to start searching." return } - arn.reloadContent() + arn.diff("/search/" + term) } // Add anime to collection diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 54fe10eb..dedd5302 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -134,9 +134,7 @@ export class AnimeNotifier { return } - navigator.serviceWorker.register("service-worker", { - scope: "./" - }).then(registration => { + navigator.serviceWorker.register("/service-worker").then(registration => { registration.update() }) @@ -409,7 +407,7 @@ export class AnimeNotifier { } diff(url: string) { - if(url == this.app.currentPath) { + if(url === this.app.currentPath) { return Promise.reject(null) } diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 1b44f0b5..5378672f 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -26,7 +26,7 @@ self.addEventListener("fetch", async (evt: any) => { } // Do not use cache in some cases - if(request.method !== "GET" || isAuth || request.url.includes("chrome-extension")) { + if(request.method !== "GET" || isAuth) { return evt.waitUntil(evt.respondWith(fetch(request))) } diff --git a/utils/AllowEmbed.go b/utils/AllowEmbed.go index 16761b8c..0beac966 100644 --- a/utils/AllowEmbed.go +++ b/utils/AllowEmbed.go @@ -5,6 +5,6 @@ import "github.com/aerogo/aero" // AllowEmbed allows the page to be called by the browser extension. func AllowEmbed(ctx *aero.Context, response string) string { // This is a bit of a hack. - ctx.SetResponseHeader("X-Frame-Options", "ALLOW-FROM chrome-extension://hjfcooigdelogjmniiahfiilcefdlpha/options.html") + // ctx.SetResponseHeader("X-Frame-Options", "ALLOW-FROM chrome-extension://hjfcooigdelogjmniiahfiilcefdlpha/options.html") return response } From 2e504548c4ae037c68748b9d05224e5b1e9ae4d2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 14 Jul 2017 04:58:21 +0200 Subject: [PATCH 247/527] Improved service worker --- main.go | 2 +- scripts/AnimeNotifier.ts | 81 ++++++++++++++++++++++------------------ sw/service-worker.ts | 80 +++++++++++++++++++++++++-------------- 3 files changed, 97 insertions(+), 66 deletions(-) diff --git a/main.go b/main.go index cf644c5e..b70b1e1f 100644 --- a/main.go +++ b/main.go @@ -58,7 +58,7 @@ func configure(app *aero.Application) *aero.Application { app.SetStyle(css.Bundle()) // Sessions - app.Sessions.Duration = 3600 * 24 * 30 + app.Sessions.Duration = 3600 * 24 * 30 * 6 app.Sessions.Store = aerospikestore.New(arn.DB, "Session", app.Sessions.Duration) // Layout diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index dedd5302..f3964900 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -11,6 +11,7 @@ export class AnimeNotifier { user: HTMLElement title: string webpEnabled: boolean + contentLoadedActions: Promise statusMessage: StatusMessage visibilityObserver: IntersectionObserver @@ -58,6 +59,7 @@ export class AnimeNotifier { document.addEventListener("keydown", this.onKeyDown.bind(this), false) window.addEventListener("popstate", this.onPopState.bind(this)) + // Idle this.requestIdleCallback(this.onIdle.bind(this)) } @@ -108,11 +110,13 @@ export class AnimeNotifier { // Stop watching all the objects from the previous page. this.visibilityObserver.disconnect() - Promise.resolve().then(() => this.mountMountables()), - Promise.resolve().then(() => this.lazyLoadImages()), - Promise.resolve().then(() => this.displayLocalDates()), - Promise.resolve().then(() => this.setSelectBoxValue()), - Promise.resolve().then(() => this.assignActions()) + this.contentLoadedActions = Promise.all([ + Promise.resolve().then(() => this.mountMountables()), + Promise.resolve().then(() => this.lazyLoadImages()), + Promise.resolve().then(() => this.displayLocalDates()), + Promise.resolve().then(() => this.setSelectBoxValue()), + Promise.resolve().then(() => this.assignActions()) + ]) let headers = document.getElementsByTagName("h1") @@ -138,32 +142,45 @@ export class AnimeNotifier { registration.update() }) - navigator.serviceWorker.onmessage = evt => { + navigator.serviceWorker.addEventListener("message", evt => { this.onServiceWorkerMessage(evt) - } + }) + + document.addEventListener("DOMContentLoaded", () => { + if(!navigator.serviceWorker.controller) { + return + } + + let message = { + type: "loaded", + url: "" + } + + if(this.app.lastRequest) { + message.url = this.app.lastRequest.responseURL + } else { + message.url = window.location.href + } + + navigator.serviceWorker.controller.postMessage(JSON.stringify(message)) + }) } onServiceWorkerMessage(evt: ServiceWorkerMessageEvent) { let message = JSON.parse(evt.data) - console.log(message.url, this.app.eTag, message.eTag) switch(message.type) { - case "content changed": - // If we don't have an etag it means it was a full page refresh. - // In this case we don't need to reload anything. - if(!this.app.eTag) { - this.app.eTag = message.eTag - return - } - - if(this.app.eTag !== message.eTag) { - if(message.url.includes("/_/")) { - // Content reload + case "new content": + if(message.url.includes("/_/")) { + // Content reload + this.contentLoadedActions.then(() => { this.reloadContent() - } else { - // Full page reload + }) + } else { + // Full page reload + this.contentLoadedActions.then(() => { this.reloadPage() - } + }) } break @@ -241,20 +258,7 @@ export class AnimeNotifier { } reloadPage() { - let headers = new Headers() - headers.append("X-Reload", "true") - - return fetch(this.app.currentPath, { - credentials: "same-origin", - headers - }) - .then(response => { - this.app.eTag = response.headers.get("ETag") - return response - }) - .then(response => response.text()) - .then(html => Diff.innerHTML(document.body, html)) - .then(() => this.app.emit("DOMContentLoaded")) + location.reload() } loading(isLoading: boolean) { @@ -414,7 +418,10 @@ export class AnimeNotifier { let request = fetch("/_" + url, { credentials: "same-origin" }) - .then(response => response.text()) + .then(response => { + this.app.eTag = response.headers.get("ETag") + return response.text() + }) history.pushState(url, null, url) this.app.currentPath = url diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 5378672f..0a918432 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -1,8 +1,12 @@ // pack:ignore -var CACHE = "v-1" +const CACHE = "v-1" +const RELOADS = new Map>() +const ETAGS = new Map() self.addEventListener("install", (evt: any) => { + console.log("Service worker install") + evt.waitUntil( (self as any).skipWaiting().then(() => { return installCache() @@ -11,11 +15,51 @@ self.addEventListener("install", (evt: any) => { }) self.addEventListener("activate", (evt: any) => { + console.log("Service worker activate") + evt.waitUntil( (self as any).clients.claim() ) }) +// controlling service worker +self.addEventListener("message", (evt: any) => { + let message = JSON.parse(evt.data) + + let url = message.url + let refresh = RELOADS.get(url) + let servedETag = ETAGS.get(url) + + if(!refresh || !servedETag) { + return + } + + evt.waitUntil( + refresh.then((response: Response) => { + // If the fresh copy was used to serve the request instead of the cache, + // we don't need to tell the client to do a refresh. + if(response.bodyUsed) { + return + } + + let eTag = response.headers.get("ETag") + + if(eTag === servedETag) { + return + } + + ETAGS.set(url, eTag) + + let message = { + type: "new content", + url + } + + return evt.source.postMessage(JSON.stringify(message)) + }) + ) +}) + self.addEventListener("fetch", async (evt: any) => { let request = evt.request let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") @@ -26,11 +70,11 @@ self.addEventListener("fetch", async (evt: any) => { } // Do not use cache in some cases - if(request.method !== "GET" || isAuth) { + if(request.method !== "GET" || isAuth || request.url.includes("chrome-extension")) { return evt.waitUntil(evt.respondWith(fetch(request))) } - let servedCachedResponse = false + let servedETag = undefined // Start fetching the request let refresh = fetch(request).then(response => { @@ -39,21 +83,14 @@ self.addEventListener("fetch", async (evt: any) => { // Save the new version of the resource in the cache caches.open(CACHE).then(cache => { return cache.put(request, clone) - }).then(() => { - if(!servedCachedResponse) { - return - } - - let contentType = clone.headers.get("Content-Type") - - if(contentType && contentType.startsWith("text/html") && clone.headers.get("ETag") && request.headers.get("X-Reload") !== "true") { - reloadContent(clone) - } }) return response }) + // Save in map + RELOADS.set(request.url, refresh) + // Forced reload if(request.headers.get("X-Reload") === "true") { return evt.waitUntil(refresh) @@ -61,7 +98,8 @@ self.addEventListener("fetch", async (evt: any) => { // Try to serve cache first and fall back to network response let networkOrCache = fromCache(request).then(response => { - servedCachedResponse = true + servedETag = response.headers.get("ETag") + ETAGS.set(request.url, servedETag) return response }).catch(error => { // console.log("Cache MISS:", request.url) @@ -93,17 +131,3 @@ function fromCache(request) { }) }) } - -function reloadContent(response) { - return (self as any).clients.matchAll().then(clients => { - clients.forEach(client => { - var message = { - type: 'content changed', - url: response.url, - eTag: response.headers.get('ETag') - } - - client.postMessage(JSON.stringify(message)) - }) - }) -} From 92a540e024de8b74ceab79c74d8d2849756ea2cc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 14 Jul 2017 23:50:34 +0200 Subject: [PATCH 248/527] Push notifications --- main.go | 4 + pages/notifications/notifications.go | 28 +++++++ pages/settings/settings.pixy | 45 ++++++++--- patches/add-push-subs/add-push-subs.go | 39 +++++++++ scripts/Actions.ts | 17 ++++ scripts/AnimeNotifier.ts | 11 ++- scripts/PushManager.ts | 105 +++++++++++++++++++++++++ sw/index.d.ts | 4 + sw/service-worker.ts | 41 +++++++++- 9 files changed, 276 insertions(+), 18 deletions(-) create mode 100644 pages/notifications/notifications.go create mode 100644 patches/add-push-subs/add-push-subs.go create mode 100644 scripts/PushManager.ts create mode 100644 sw/index.d.ts diff --git a/main.go b/main.go index b70b1e1f..74fa4eb8 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( "github.com/animenotifier/notify.moe/pages/music" "github.com/animenotifier/notify.moe/pages/newsoundtrack" "github.com/animenotifier/notify.moe/pages/newthread" + "github.com/animenotifier/notify.moe/pages/notifications" "github.com/animenotifier/notify.moe/pages/posts" "github.com/animenotifier/notify.moe/pages/profile" "github.com/animenotifier/notify.moe/pages/search" @@ -126,6 +127,9 @@ func configure(app *aero.Application) *aero.Application { // Browser extension app.Ajax("/extension/embed", embed.Get) + // API + app.Get("/api/test/notification", notifications.Test) + // Middleware app.Use(middleware.Log()) app.Use(middleware.Session()) diff --git a/pages/notifications/notifications.go b/pages/notifications/notifications.go new file mode 100644 index 00000000..3f3d6c64 --- /dev/null +++ b/pages/notifications/notifications.go @@ -0,0 +1,28 @@ +package notifications + +import ( + "fmt" + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/utils" +) + +// Test ... +func Test(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusBadRequest, "Not logged in", nil) + } + + for _, sub := range user.PushSubscriptions().Items { + err := sub.SendNotification("Yay, it works!") + + if err != nil { + fmt.Println(err) + } + } + + return "ok" +} diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 34d69748..b96a30cd 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -21,6 +21,29 @@ component Settings(user *arn.User) InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh") //- InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") + .widget.mountable + h3.widget-title + Icon("bell") + span Notifications + + .widget-input + label Enable: + button.action(data-action="enableNotifications", data-trigger="click") + Icon("toggle-on") + span Enable notifications + + .widget-input + label Disable: + button.action(data-action="disableNotifications", data-trigger="click") + Icon("toggle-off") + span Disable notifications + + .widget-input + label Test: + button.action(data-action="testNotification", data-trigger="click") + Icon("paper-plane") + span Send test notification + .widget.mountable h3.widget-title Icon("user-plus") @@ -49,17 +72,6 @@ component Settings(user *arn.User) Icon("circle-o") span Not connected - .widget.mountable - h3.widget-title - Icon("puzzle-piece") - span Extensions - - .widget-input - label Chrome Extension: - button.action(data-action="installExtension", data-trigger="click") - Icon("chrome") - span Get the Chrome Extension - .widget.mountable h3.widget-title Icon("download") @@ -78,6 +90,17 @@ component Settings(user *arn.User) Icon("upload") span Export anime list as JSON + .widget.mountable + h3.widget-title + Icon("puzzle-piece") + span Extensions + + .widget-input + label Chrome Extension: + button.action(data-action="installExtension", data-trigger="click") + Icon("chrome") + span Get the Chrome Extension + //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title //- Icon("cogs") diff --git a/patches/add-push-subs/add-push-subs.go b/patches/add-push-subs/add-push-subs.go new file mode 100644 index 00000000..932487a2 --- /dev/null +++ b/patches/add-push-subs/add-push-subs.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Adding push subscriptions to users who don't have one") + + // Get a stream of all users + allUsers, err := arn.StreamUsers() + + if err != nil { + panic(err) + } + + // Iterate over the stream + for user := range allUsers { + exists, err := arn.DB.Exists("PushSubscriptions", user.ID) + + if err == nil && !exists { + fmt.Println(user.Nick) + + err := arn.DB.Set("PushSubscriptions", user.ID, &arn.PushSubscriptions{ + UserID: user.ID, + Items: make([]*arn.PushSubscription, 0), + }) + + if err != nil { + color.Red(err.Error()) + } + } + } + + color.Green("Finished.") +} diff --git a/scripts/Actions.ts b/scripts/Actions.ts index bd5672d2..2b38f95e 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -214,6 +214,23 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard arn.diff("/search/" + term) } +// Enable notifications +export function enableNotifications(arn: AnimeNotifier, button: HTMLElement) { + arn.pushManager.subscribe(arn.user.dataset.id) +} + +// Disable notifications +export function disableNotifications(arn: AnimeNotifier, button: HTMLElement) { + arn.pushManager.unsubscribe(arn.user.dataset.id) +} + +// Test notification +export function testNotification(arn: AnimeNotifier) { + fetch("/api/test/notification", { + credentials: "same-origin" + }) +} + // Add anime to collection export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) { button.innerText = "Adding..." diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index f3964900..0c6db38c 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -1,10 +1,11 @@ -import { Application } from "./Application" -import { Diff } from "./Diff" +import * as actions from "./Actions" import { displayAiringDate, displayDate } from "./DateView" import { findAll, delay, canUseWebP } from "./Utils" +import { Application } from "./Application" +import { Diff } from "./Diff" import { MutationQueue } from "./MutationQueue" import { StatusMessage } from "./StatusMessage" -import * as actions from "./Actions" +import { PushManager } from "./PushManager" export class AnimeNotifier { app: Application @@ -14,6 +15,7 @@ export class AnimeNotifier { contentLoadedActions: Promise statusMessage: StatusMessage visibilityObserver: IntersectionObserver + pushManager: PushManager imageFound: MutationQueue imageNotFound: MutationQueue @@ -104,6 +106,9 @@ export class AnimeNotifier { // Service worker this.registerServiceWorker() + + // Push manager + this.pushManager = new PushManager() } onContentLoaded() { diff --git a/scripts/PushManager.ts b/scripts/PushManager.ts new file mode 100644 index 00000000..671f6c8b --- /dev/null +++ b/scripts/PushManager.ts @@ -0,0 +1,105 @@ +export class PushManager { + pushSupported: boolean + + constructor() { + this.pushSupported = ("serviceWorker" in navigator) && ("PushManager" in window) + } + + async subscribe(userId: string) { + if(!this.pushSupported) { + return + } + + let registration = await navigator.serviceWorker.ready + let subscription = await registration.pushManager.getSubscription() + + if(!subscription) { + subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array("BAwPKVCWQ2_nc7SIGltYfWZhMpW54BSkbwelpa8eYMbqSitmCAGm3xRBdRiq1Wt-hUsE7x59GCcaJxqQtF2hZPw") + }) + + this.subscribeOnServer(subscription, userId) + } else { + console.log("Using existing subscription", subscription) + } + } + + async unsubscribe(userId: string) { + if(!this.pushSupported) { + return + } + + let registration = await navigator.serviceWorker.ready + let subscription = await registration.pushManager.getSubscription() + + if(!subscription) { + console.error("Subscription does not exist") + return + } + + await subscription.unsubscribe() + + this.unsubscribeOnServer(subscription, userId) + } + + subscribeOnServer(subscription: PushSubscription, userId: string) { + console.log("Send subscription to server...") + + let rawKey = subscription.getKey("p256dh") + let key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : "" + + let rawSecret = subscription.getKey("auth") + let secret = rawSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawSecret))) : "" + + let endpoint = subscription.endpoint + + let pushSubscription = { + endpoint, + p256dh: key, + auth: secret, + platform: navigator.platform, + screen: { + width: window.screen.width, + height: window.screen.height + } + } + + return fetch("/api/pushsubscriptions/" + userId + "/add", { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(pushSubscription) + }) + } + + unsubscribeOnServer(subscription: PushSubscription, userId: string) { + console.log("Send unsubscription to server...") + console.log(subscription) + + let pushSubscription = { + endpoint: subscription.endpoint + } + + return fetch("/api/pushsubscriptions/" + userId + "/remove", { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(pushSubscription) + }) + } +} + +function urlBase64ToUint8Array(base64String) { + const padding = "=".repeat((4 - base64String.length % 4) % 4) + const base64 = (base64String + padding) + .replace(/\-/g, "+") + .replace(/_/g, "/") + + const rawData = window.atob(base64) + const outputArray = new Uint8Array(rawData.length) + + for(let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i) + } + + return outputArray +} \ No newline at end of file diff --git a/sw/index.d.ts b/sw/index.d.ts new file mode 100644 index 00000000..2278ea56 --- /dev/null +++ b/sw/index.d.ts @@ -0,0 +1,4 @@ +type NotificationEvent = any; +type InstallEvent = any; +type FetchEvent = any; +type PushEvent = any; \ No newline at end of file diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 0a918432..f888ab26 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -4,7 +4,7 @@ const CACHE = "v-1" const RELOADS = new Map>() const ETAGS = new Map() -self.addEventListener("install", (evt: any) => { +self.addEventListener("install", (evt: InstallEvent) => { console.log("Service worker install") evt.waitUntil( @@ -37,7 +37,7 @@ self.addEventListener("message", (evt: any) => { evt.waitUntil( refresh.then((response: Response) => { // If the fresh copy was used to serve the request instead of the cache, - // we don't need to tell the client to do a refresh. + // we don"t need to tell the client to do a refresh. if(response.bodyUsed) { return } @@ -60,9 +60,10 @@ self.addEventListener("message", (evt: any) => { ) }) -self.addEventListener("fetch", async (evt: any) => { +self.addEventListener("fetch", async (evt: FetchEvent) => { let request = evt.request let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") + let ignoreCache = request.url.includes("/api/") || request.url.includes("chrome-extension") // Delete existing cache on authentication if(isAuth) { @@ -70,7 +71,7 @@ self.addEventListener("fetch", async (evt: any) => { } // Do not use cache in some cases - if(request.method !== "GET" || isAuth || request.url.includes("chrome-extension")) { + if(request.method !== "GET" || isAuth || ignoreCache) { return evt.waitUntil(evt.respondWith(fetch(request))) } @@ -109,6 +110,38 @@ self.addEventListener("fetch", async (evt: any) => { return evt.waitUntil(evt.respondWith(networkOrCache)) }) +self.addEventListener("push", (evt: PushEvent) => { + var payload = evt.data ? evt.data.text() : "no payload" + + evt.waitUntil( + (self as any).registration.showNotification("beta.notify.moe Service Worker", { + body: payload + }) + ) +}) + +self.addEventListener("pushsubscriptionchange", (evt: any) => { + console.log("pushsubscriptionchange", evt) +}) + +self.addEventListener("notificationclick", (evt: NotificationEvent) => { + console.log(evt) + + evt.notification.close() + + evt.waitUntil( + (self as any).clients.matchAll().then(function(clientList) { + // If there is at least one client, focus it. + if(clientList.length > 0) { + return clientList[0].focus() + } + + // Otherwise open a new window + return (self as any).clients.openWindow("https://notify.moe") + }) + ) +}) + function installCache() { return caches.open(CACHE).then(cache => { return cache.addAll([ From 460d90d9578a4031e182247ce62f4be58076399b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 01:32:06 +0200 Subject: [PATCH 249/527] Improved push notifications --- pages/notifications/notifications.go | 14 +++++++------- pages/settings/settings.pixy | 10 +++++----- scripts/Actions.ts | 10 ++++++---- scripts/AnimeNotifier.ts | 22 ++++++++++++++++++++-- scripts/PushManager.ts | 16 ++++++++++++++++ sw/service-worker.ts | 27 +++++++++++++++++++++++---- tests.go | 1 + 7 files changed, 78 insertions(+), 22 deletions(-) diff --git a/pages/notifications/notifications.go b/pages/notifications/notifications.go index 3f3d6c64..0ff4fc4d 100644 --- a/pages/notifications/notifications.go +++ b/pages/notifications/notifications.go @@ -1,10 +1,10 @@ package notifications import ( - "fmt" "net/http" "github.com/aerogo/aero" + "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/utils" ) @@ -16,13 +16,13 @@ func Test(ctx *aero.Context) string { return ctx.Error(http.StatusBadRequest, "Not logged in", nil) } - for _, sub := range user.PushSubscriptions().Items { - err := sub.SendNotification("Yay, it works!") - - if err != nil { - fmt.Println(err) - } + notification := &arn.Notification{ + Title: "Anime Notifier", + Message: "Yay, it works!", + Icon: "https://" + ctx.App.Config.Domain + "/images/brand/300", } + user.SendNotification(notification) + return "ok" } diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index b96a30cd..876347b7 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -26,19 +26,19 @@ component Settings(user *arn.User) Icon("bell") span Notifications - .widget-input + #enable-notifications.widget-input label Enable: button.action(data-action="enableNotifications", data-trigger="click") - Icon("toggle-on") + Icon("toggle-off") span Enable notifications - .widget-input + #disable-notifications.widget-input label Disable: button.action(data-action="disableNotifications", data-trigger="click") - Icon("toggle-off") + Icon("toggle-on") span Disable notifications - .widget-input + #test-notification.widget-input label Test: button.action(data-action="testNotification", data-trigger="click") Icon("paper-plane") diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 2b38f95e..7b81702c 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -215,13 +215,15 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard } // Enable notifications -export function enableNotifications(arn: AnimeNotifier, button: HTMLElement) { - arn.pushManager.subscribe(arn.user.dataset.id) +export async function enableNotifications(arn: AnimeNotifier, button: HTMLElement) { + await arn.pushManager.subscribe(arn.user.dataset.id) + arn.updatePushUI() } // Disable notifications -export function disableNotifications(arn: AnimeNotifier, button: HTMLElement) { - arn.pushManager.unsubscribe(arn.user.dataset.id) +export async function disableNotifications(arn: AnimeNotifier, button: HTMLElement) { + await arn.pushManager.unsubscribe(arn.user.dataset.id) + arn.updatePushUI() } // Test notification diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 0c6db38c..ccceb9c0 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -111,7 +111,7 @@ export class AnimeNotifier { this.pushManager = new PushManager() } - onContentLoaded() { + async onContentLoaded() { // Stop watching all the objects from the previous page. this.visibilityObserver.disconnect() @@ -120,9 +120,11 @@ export class AnimeNotifier { Promise.resolve().then(() => this.lazyLoadImages()), Promise.resolve().then(() => this.displayLocalDates()), Promise.resolve().then(() => this.setSelectBoxValue()), - Promise.resolve().then(() => this.assignActions()) + Promise.resolve().then(() => this.assignActions()), + Promise.resolve().then(() => this.updatePushUI()) ]) + // Apply page title let headers = document.getElementsByTagName("h1") if(this.app.currentPath === "/" || headers.length === 0) { @@ -134,6 +136,22 @@ export class AnimeNotifier { } } + async updatePushUI() { + if(!this.pushManager.pushSupported) { + return + } + + let subscription = await this.pushManager.subscription() + + if(subscription) { + this.app.find("enable-notifications").style.display = "none" + this.app.find("disable-notifications").style.display = "flex" + } else { + this.app.find("enable-notifications").style.display = "flex" + this.app.find("disable-notifications").style.display = "none" + } + } + onIdle() { this.pushAnalytics() } diff --git a/scripts/PushManager.ts b/scripts/PushManager.ts index 671f6c8b..0f4195f8 100644 --- a/scripts/PushManager.ts +++ b/scripts/PushManager.ts @@ -5,6 +5,21 @@ export class PushManager { this.pushSupported = ("serviceWorker" in navigator) && ("PushManager" in window) } + async subscription(): Promise { + if(!this.pushSupported) { + return Promise.resolve(null) + } + + let registration = await navigator.serviceWorker.ready + let subscription = await registration.pushManager.getSubscription() + + if(subscription) { + return Promise.resolve(subscription) + } + + return Promise.resolve(null) + } + async subscribe(userId: string) { if(!this.pushSupported) { return @@ -59,6 +74,7 @@ export class PushManager { p256dh: key, auth: secret, platform: navigator.platform, + userAgent: navigator.userAgent, screen: { width: window.screen.width, height: window.screen.height diff --git a/sw/service-worker.ts b/sw/service-worker.ts index f888ab26..a1d9a31e 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -17,8 +17,25 @@ self.addEventListener("install", (evt: InstallEvent) => { self.addEventListener("activate", (evt: any) => { console.log("Service worker activate") + // Delete old cache + let cacheWhitelist = [CACHE] + + let deleteOldCache = caches.keys().then(keyList => { + return Promise.all(keyList.map(key => { + if(cacheWhitelist.indexOf(key) === -1) { + return caches.delete(key) + } + })) + }) + + let immediateClaim = (self as any).clients.claim() + + // Immediate claim evt.waitUntil( - (self as any).clients.claim() + Promise.all([ + deleteOldCache, + immediateClaim + ]) ) }) @@ -111,11 +128,13 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { }) self.addEventListener("push", (evt: PushEvent) => { - var payload = evt.data ? evt.data.text() : "no payload" + var payload = evt.data ? evt.data.json() : {} evt.waitUntil( - (self as any).registration.showNotification("beta.notify.moe Service Worker", { - body: payload + (self as any).registration.showNotification(payload.title, { + body: payload.message, + icon: payload.icon, + image: payload.image }) ) }) diff --git a/tests.go b/tests.go index aae85607..48a1aadc 100644 --- a/tests.go +++ b/tests.go @@ -185,6 +185,7 @@ var routeTests = map[string][]string{ "/import/myanimelist/animelist/finish": nil, "/import/kitsu/animelist": nil, "/import/kitsu/animelist/finish": nil, + "/api/test/notification": nil, "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, From 7cd8a8153e1380828e20131d418b10d68596122b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 01:53:08 +0200 Subject: [PATCH 250/527] Improved content refresh --- scripts/AnimeNotifier.ts | 2 +- sw/service-worker.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index ccceb9c0..e761dbb3 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -137,7 +137,7 @@ export class AnimeNotifier { } async updatePushUI() { - if(!this.pushManager.pushSupported) { + if(!this.pushManager.pushSupported || !this.app.currentPath.includes("/settings")) { return } diff --git a/sw/service-worker.ts b/sw/service-worker.ts index a1d9a31e..70f4c6af 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -3,6 +3,7 @@ const CACHE = "v-1" const RELOADS = new Map>() const ETAGS = new Map() +const CACHEREFRESH = new Map>() self.addEventListener("install", (evt: InstallEvent) => { console.log("Service worker install") @@ -72,7 +73,15 @@ self.addEventListener("message", (evt: any) => { url } - return evt.source.postMessage(JSON.stringify(message)) + let cacheRefresh = CACHEREFRESH.get(url) + + if(!cacheRefresh) { + return evt.source.postMessage(JSON.stringify(message)) + } + + return cacheRefresh.then(() => { + evt.source.postMessage(JSON.stringify(message)) + }) }) ) }) @@ -99,10 +108,12 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { let clone = response.clone() // Save the new version of the resource in the cache - caches.open(CACHE).then(cache => { + let cacheRefresh = caches.open(CACHE).then(cache => { return cache.put(request, clone) }) + CACHEREFRESH.set(request.url, cacheRefresh) + return response }) From c4bc17ae7f750ab67f6a342b422db36cf9ac6674 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 02:01:14 +0200 Subject: [PATCH 251/527] Improved offline mode --- scripts/AnimeNotifier.ts | 4 ++++ styles/status-message.scarlet | 1 + 2 files changed, 5 insertions(+) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e761dbb3..4d551d09 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -154,6 +154,10 @@ export class AnimeNotifier { onIdle() { this.pushAnalytics() + + if(navigator.onLine === false) { + this.statusMessage.showError("You are viewing an offline version of the site now.") + } } registerServiceWorker() { diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet index 9d7ae0eb..e08f83f5 100644 --- a/styles/status-message.scarlet +++ b/styles/status-message.scarlet @@ -13,6 +13,7 @@ .status-message-action color white !important + pointer-events all !important .error-message color white From 1fd9c284de93681a17ae541b85e6ea1ed22c5c5b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 02:32:54 +0200 Subject: [PATCH 252/527] Forum notifications --- pages/settings/settings.pixy | 2 ++ sw/service-worker.ts | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 876347b7..f4b745d6 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -43,6 +43,8 @@ component Settings(user *arn.User) button.action(data-action="testNotification", data-trigger="click") Icon("paper-plane") span Send test notification + + p Notifications are currently used for forum replies only. .widget.mountable h3.widget-title diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 70f4c6af..47867d4b 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -145,7 +145,8 @@ self.addEventListener("push", (evt: PushEvent) => { (self as any).registration.showNotification(payload.title, { body: payload.message, icon: payload.icon, - image: payload.image + image: payload.image, + data: payload.link }) ) }) @@ -155,12 +156,18 @@ self.addEventListener("pushsubscriptionchange", (evt: any) => { }) self.addEventListener("notificationclick", (evt: NotificationEvent) => { - console.log(evt) - - evt.notification.close() + let notification = evt.notification + notification.close() evt.waitUntil( (self as any).clients.matchAll().then(function(clientList) { + // If we have a link, use that link to open a new window. + let url = notification.data + + if(url) { + return (self as any).clients.openWindow(url) + } + // If there is at least one client, focus it. if(clientList.length > 0) { return clientList[0].focus() From 10709fe25ad021202ff23d916de97f78b22a77d5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 17:07:24 +0200 Subject: [PATCH 253/527] Fixed image loading bug --- scripts/Diff.ts | 4 +++- sw/service-worker.ts | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 620b7797..051aa55a 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -53,7 +53,9 @@ export class Diff { } // Ignore lazy images if they have the same source - if(elemA.classList.contains("lazy") && elemA.dataset.src === elemB.dataset.src) { + if(elemA.classList.contains("lazy")) { + elemA.dataset.src = elemB.dataset.src + elemA.title = elemB.title continue } diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 47867d4b..a6a381fe 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -73,6 +73,11 @@ self.addEventListener("message", (evt: any) => { url } + // If a subpage has refreshed, refresh the main page cache, too. + if(url.includes("/_/")) { + + } + let cacheRefresh = CACHEREFRESH.get(url) if(!cacheRefresh) { @@ -87,7 +92,7 @@ self.addEventListener("message", (evt: any) => { }) self.addEventListener("fetch", async (evt: FetchEvent) => { - let request = evt.request + let request = evt.request as Request let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") let ignoreCache = request.url.includes("/api/") || request.url.includes("chrome-extension") From 35f548e41d102d1aa8847b697fc34db96e4b0954 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 18:15:20 +0200 Subject: [PATCH 254/527] Implemented anime notifications --- jobs/jobs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/jobs.go b/jobs/jobs.go index f3fd08ad..2c635708 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -30,7 +30,7 @@ var jobs = map[string]time.Duration{ "statistics": 15 * time.Minute, "popular-anime": 20 * time.Minute, "avatars": 30 * time.Minute, - "twist": 8 * time.Hour, + "twist": 1 * time.Hour, "sync-shoboi": 8 * time.Hour, "refresh-episodes": 10 * time.Hour, "refresh-track-titles": 10 * time.Hour, From f3751262f2ef79cf9a0c83c3a37d156d71e0d9e0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 18:19:31 +0200 Subject: [PATCH 255/527] Removed old text --- pages/settings/settings.pixy | 2 -- 1 file changed, 2 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index f4b745d6..876347b7 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -43,8 +43,6 @@ component Settings(user *arn.User) button.action(data-action="testNotification", data-trigger="click") Icon("paper-plane") span Send test notification - - p Notifications are currently used for forum replies only. .widget.mountable h3.widget-title From e5e8cd9d25bceb52875ff900b69709b1715136b1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 18:51:25 +0200 Subject: [PATCH 256/527] Style changes --- pages/animelist/animelist.scarlet | 2 +- sw/service-worker.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index faa7d726..8d2d609a 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -71,7 +71,7 @@ .anime-list-item-airing-date display block text-align right - flex-basis 120px + flex-basis 150px < 1100px .anime-list-item-rating diff --git a/sw/service-worker.ts b/sw/service-worker.ts index a6a381fe..a528e0b7 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -74,9 +74,9 @@ self.addEventListener("message", (evt: any) => { } // If a subpage has refreshed, refresh the main page cache, too. - if(url.includes("/_/")) { + // if(url.includes("/_/")) { - } + // } let cacheRefresh = CACHEREFRESH.get(url) From 92c6a7e4b48f29d743e2aed4da54b3548143a892 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 19:31:34 +0200 Subject: [PATCH 257/527] New statistics --- jobs/statistics/statistics.go | 17 +++++++++++++++++ patches/add-episodes/add-episodes.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 patches/add-episodes/add-episodes.go diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index 74a4d81d..6ed1fdf1 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -20,6 +20,7 @@ func main() { Name: "Users", PieCharts: userStats, })) + arn.PanicOnError(arn.DB.Set("Cache", "anime statistics", &arn.StatisticsCategory{ Name: "Anime", PieCharts: animeStats, @@ -40,6 +41,8 @@ func getUserStats() []*arn.PieChart { country := stats{} gender := stats{} os := stats{} + notifications := stats{} + activity := stats{} for _, info := range analytics { pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ @@ -68,6 +71,18 @@ func getUserStats() []*arn.PieChart { os[user.OS.Name]++ } + + if len(user.PushSubscriptions().Items) > 0 { + notifications["Enabled"]++ + } else { + notifications["Disabled"]++ + } + + if user.IsActive() { + activity["Active last week"]++ + } else { + activity["Inactive"]++ + } } println("Finished user statistics") @@ -77,6 +92,8 @@ func getUserStats() []*arn.PieChart { arn.NewPieChart("Screen size", screenSize), arn.NewPieChart("Browser", browser), arn.NewPieChart("Country", country), + arn.NewPieChart("Activity", activity), + arn.NewPieChart("Notifications", notifications), arn.NewPieChart("Gender", gender), arn.NewPieChart("Pixel ratio", pixelRatio), } diff --git a/patches/add-episodes/add-episodes.go b/patches/add-episodes/add-episodes.go new file mode 100644 index 00000000..111d0644 --- /dev/null +++ b/patches/add-episodes/add-episodes.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" +) + +func main() { + count := 0 + + for anime := range arn.MustStreamAnime() { + episodes := anime.Episodes() + + if episodes == nil { + episodes = &arn.AnimeEpisodes{ + AnimeID: anime.ID, + Items: []*arn.AnimeEpisode{}, + } + + if episodes.Save() == nil { + count++ + } + } + } + + fmt.Println("Added empty anime episodes to", count, "anime.") +} From 273d876758d5af8f1b2a6650bb410323d79cdf95 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 20:18:24 +0200 Subject: [PATCH 258/527] Improved iframe diff --- pages/profile/stats.go | 4 +++- scripts/Diff.ts | 16 ++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pages/profile/stats.go b/pages/profile/stats.go index 05b79b81..0a28bbef 100644 --- a/pages/profile/stats.go +++ b/pages/profile/stats.go @@ -35,7 +35,9 @@ func GetStatsByUser(ctx *aero.Context) string { } for _, item := range animeList.Items { - duration := time.Duration(item.Episodes * item.Anime().EpisodeLength) + currentWatch := item.Episodes * item.Anime().EpisodeLength + reWatch := item.RewatchCount * item.Anime().EpisodeCount * item.Anime().EpisodeLength + duration := time.Duration(currentWatch + reWatch) userStats.AnimeWatchingTime += duration * time.Minute ratings[strconv.Itoa(int(item.Rating.Overall+0.5))]++ diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 051aa55a..b1fe5318 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -47,15 +47,19 @@ export class Diff { let elemA = a as HTMLElement let elemB = b as HTMLElement - // Skip iframes - if(elemA.tagName === "IFRAME") { + // Ignore lazy images if they have the same source + if(elemA.classList.contains("lazy")) { + if(elemA.dataset.src !== elemB.dataset.src) { + elemA.dataset.src = elemB.dataset.src + elemA.title = elemB.title + } continue } - // Ignore lazy images if they have the same source - if(elemA.classList.contains("lazy")) { - elemA.dataset.src = elemB.dataset.src - elemA.title = elemB.title + // Skip iframes + // This part needs to be executed AFTER lazy images check + // to allow lazily loaded iframes to update their data src. + if(elemA.tagName === "IFRAME") { continue } From bfe436f9479f0ee5c87388f2b55b986dab8bef7d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 15 Jul 2017 20:27:49 +0200 Subject: [PATCH 259/527] Added android app link --- pages/settings/settings.pixy | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 876347b7..06c9ec58 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -93,7 +93,7 @@ component Settings(user *arn.User) .widget.mountable h3.widget-title Icon("puzzle-piece") - span Extensions + span Apps .widget-input label Chrome Extension: @@ -101,6 +101,12 @@ component Settings(user *arn.User) Icon("chrome") span Get the Chrome Extension + .widget-input + label Android App: + a.button(href="https://www.youtube.com/watch?v=opyt4cw0ep8", target="_blank", rel="noopener") + Icon("android") + span Get the Android App + //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title //- Icon("cogs") From 9cdbb3f8a87c4cc834162bba1dc588c71e09940c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 16 Jul 2017 01:46:36 +0200 Subject: [PATCH 260/527] Added post like notification --- pages/dashboard/dashboard.pixy | 76 ++++++++++++++++++++++++---------- styles/widgets.scarlet | 2 +- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 2d0638ad..ccad9043 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -86,30 +86,62 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound Icon("address-card") span ... - .widget.mountable - h3.widget-title Follow + //- .widget.mountable + //- h3.widget-title Follow - a.widget-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") - .widget-element-text - Icon("microphone") - span Discord + //- a.widget-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") + //- .widget-element-text + //- Icon("microphone") + //- span Discord - a.widget-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") - .widget-element-text - Icon("facebook") - span Facebook + //- a.widget-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") + //- .widget-element-text + //- Icon("facebook") + //- span Facebook - a.widget-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") - .widget-element-text - Icon("twitter") - span Twitter + //- a.widget-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") + //- .widget-element-text + //- Icon("twitter") + //- span Twitter - a.widget-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") - .widget-element-text - Icon("google-plus") - span Google+ + //- a.widget-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") + //- .widget-element-text + //- Icon("google-plus") + //- span Google+ - a.widget-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") - .widget-element-text - Icon("github") - span GitHub + //- a.widget-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") + //- .widget-element-text + //- Icon("github") + //- span GitHub + + .footer + span Anime Notifier + span | + + a(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") + Icon("microphone") + span Discord + + span | + + a(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") + Icon("facebook") + span Facebook + + span | + + a(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") + Icon("twitter") + span Twitter + + span | + + a(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") + Icon("google-plus") + span Google+ + + span | + + a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") + Icon("github") + span GitHub diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index 6091e200..f60c3534 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -17,7 +17,7 @@ .widget vertical - margin-bottom content-padding + margin-bottom 1rem overflow hidden .widget-element From 1a7718373770f88a44ae5235bd4f794c0b85a8cd Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 16 Jul 2017 01:59:05 +0200 Subject: [PATCH 261/527] Improved dashboard --- pages/dashboard/dashboard.pixy | 32 +++++++++++++++---------------- pages/dashboard/dashboard.scarlet | 12 +++++++++++- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index ccad9043..bb8f161b 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -62,6 +62,15 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound Icon("video-camera") span ... + .widget.mountable + h3.widget-title Reviews + + for i := 1; i <= 5; i++ + .widget-element + .widget-element-text + Icon("book") + span ... + .widget.mountable h3.widget-title Groups @@ -114,34 +123,25 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound //- Icon("github") //- span GitHub - .footer - span Anime Notifier - span | + .footer.text-center + span.footer-element Anime Notifier - a(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") + a.footer-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") Icon("microphone") span Discord - - span | - a(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") + a.footer-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") Icon("facebook") span Facebook - span | - - a(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") + a.footer-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") Icon("twitter") span Twitter - span | - - a(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") + a.footer-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") Icon("google-plus") span Google+ - span | - - a(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") + a.footer-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") Icon("github") span GitHub diff --git a/pages/dashboard/dashboard.scarlet b/pages/dashboard/dashboard.scarlet index 3fd2c3c8..16566790 100644 --- a/pages/dashboard/dashboard.scarlet +++ b/pages/dashboard/dashboard.scarlet @@ -9,4 +9,14 @@ align-items center .schedule-item-date - text-align right \ No newline at end of file + text-align right + +.footer-element + :after + content " | " + color text-color + opacity 0.5 + + :last-child + :after + display none \ No newline at end of file From 649672429dab21a01f6201455e9af4bda9e64238 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 16 Jul 2017 07:50:57 +0200 Subject: [PATCH 262/527] Started implementing PayPal --- main.go | 8 +++++--- mixins/Navigation.pixy | 4 ++-- pages/paypal/paypal.go | 41 ++++++++++++++++++++++++++++++++++++++ pages/profile/profile.pixy | 2 +- scripts/Actions.ts | 2 +- tests.go | 9 +++++---- 6 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 pages/paypal/paypal.go diff --git a/main.go b/main.go index 74fa4eb8..d847bbf9 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ import ( "github.com/animenotifier/notify.moe/pages/newsoundtrack" "github.com/animenotifier/notify.moe/pages/newthread" "github.com/animenotifier/notify.moe/pages/notifications" + "github.com/animenotifier/notify.moe/pages/paypal" "github.com/animenotifier/notify.moe/pages/posts" "github.com/animenotifier/notify.moe/pages/profile" "github.com/animenotifier/notify.moe/pages/search" @@ -76,12 +77,12 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/forum/:tag", forum.Get) app.Ajax("/thread/:id", threads.Get) app.Ajax("/post/:id", posts.Get) - app.Ajax("/track/:id", tracks.Get) + app.Ajax("/soundtrack/:id", tracks.Get) app.Ajax("/character/:id", character.Get) app.Ajax("/new/thread", newthread.Get) app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) - app.Ajax("/music", music.Get) + app.Ajax("/soundtracks", music.Get) app.Ajax("/users", users.Get) app.Ajax("/login", login.Get) @@ -90,7 +91,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick", profile.Get) app.Ajax("/user/:nick/threads", profile.GetThreadsByUser) app.Ajax("/user/:nick/posts", profile.GetPostsByUser) - app.Ajax("/user/:nick/tracks", profile.GetSoundTracksByUser) + app.Ajax("/user/:nick/soundtracks", profile.GetSoundTracksByUser) app.Ajax("/user/:nick/stats", profile.GetStatsByUser) app.Ajax("/user/:nick/animelist", animelist.Get) app.Ajax("/user/:nick/animelist/watching", animelist.FilterByStatus(arn.AnimeListStatusWatching)) @@ -129,6 +130,7 @@ func configure(app *aero.Application) *aero.Application { // API app.Get("/api/test/notification", notifications.Test) + app.Get("/api/paypal/payment/create", paypal.CreatePayment) // Middleware app.Use(middleware.Log()) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index edff724b..4ce84de4 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -15,7 +15,7 @@ component LoggedOutMenu .extra-navigation NavigationButton("Users", "/users", "globe") - NavigationButton("Music", "/music", "headphones") + NavigationButton("Soundtracks", "/soundtracks", "headphones") NavigationButton("Login", "/login", "sign-in") @@ -29,7 +29,7 @@ component LoggedInMenu(user *arn.User) NavigationButton("Forum", "/forum", "comment") .extra-navigation - NavigationButton("Music", "/music", "headphones") + NavigationButton("Soundtracks", "/soundtracks", "headphones") FuzzySearch diff --git a/pages/paypal/paypal.go b/pages/paypal/paypal.go new file mode 100644 index 00000000..6e1c1846 --- /dev/null +++ b/pages/paypal/paypal.go @@ -0,0 +1,41 @@ +package paypal + +import ( + "net/http" + "os" + + "github.com/aerogo/aero" + "github.com/logpacker/PayPal-Go-SDK" +) + +// CreatePayment ... +func CreatePayment(ctx *aero.Context) string { + // Create a client instance + c, err := paypalsdk.NewClient("clientID", "secretID", paypalsdk.APIBaseSandBox) + c.SetLog(os.Stdout) // Set log to terminal stdout + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not initiate PayPal client", err) + } + + _, err = c.GetAccessToken() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not get PayPal access token", err) + } + + amount := paypalsdk.Amount{ + Total: "7.00", + Currency: "USD", + } + redirectURI := "http://example.com/redirect-uri" + cancelURI := "http://example.com/cancel-uri" + description := "Description for this payment" + paymentResult, err := c.CreateDirectPaypalPayment(amount, redirectURI, cancelURI, description) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not create PayPal payment", err) + } + + return ctx.JSON(paymentResult) +} diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 90da6855..464cda20 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -64,7 +64,7 @@ component ProfileNavigation(viewUser *arn.User, uri string) Icon("comments") span.tab-text Posts - a.button.tab.action(href="/+" + viewUser.Nick + "/tracks", data-action="diff", data-trigger="click") + a.button.tab.action(href="/+" + viewUser.Nick + "/soundtracks", data-action="diff", data-trigger="click") Icon("music") span.tab-text Tracks diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 7b81702c..66bd6dde 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -194,7 +194,7 @@ export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) } arn.post("/api/new/soundtrack", soundtrack) - .then(() => arn.app.load("/music")) + .then(() => arn.app.load("/soundtracks")) .catch(err => arn.statusMessage.showError(err)) } diff --git a/tests.go b/tests.go index 48a1aadc..c0a6ef7f 100644 --- a/tests.go +++ b/tests.go @@ -22,8 +22,8 @@ var routeTests = map[string][]string{ "/+Akyoto/posts", }, - "/user/:nick/tracks": []string{ - "/+Akyoto/tracks", + "/user/:nick/soundtracks": []string{ + "/+Akyoto/soundtracks", }, "/user/:nick/animelist": []string{ @@ -75,8 +75,8 @@ var routeTests = map[string][]string{ "/search/Dragon Ball", }, - "/track/:id": []string{ - "/track/h0ac8sKkg", + "/soundtrack/:id": []string{ + "/soundtrack/h0ac8sKkg", }, // API @@ -186,6 +186,7 @@ var routeTests = map[string][]string{ "/import/kitsu/animelist": nil, "/import/kitsu/animelist/finish": nil, "/api/test/notification": nil, + "/api/paypal/payment/create": nil, "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, From d33b93d115685ba8ed7c5f9d6fdcc4febf1014eb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 16 Jul 2017 20:29:10 +0200 Subject: [PATCH 263/527] Added paypal callbacks --- main.go | 4 +++ pages/paypal/cancel.go | 15 +++++++++++ pages/paypal/paypal.go | 59 ++++++++++++++++++++++++++++++++--------- pages/paypal/success.go | 37 ++++++++++++++++++++++++++ tests.go | 2 ++ 5 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 pages/paypal/cancel.go create mode 100644 pages/paypal/success.go diff --git a/main.go b/main.go index d847bbf9..5f2a2019 100644 --- a/main.go +++ b/main.go @@ -130,6 +130,10 @@ func configure(app *aero.Application) *aero.Application { // API app.Get("/api/test/notification", notifications.Test) + + // PayPal + app.Ajax("/paypal/success", paypal.Success) + app.Ajax("/paypal/cancel", paypal.Cancel) app.Get("/api/paypal/payment/create", paypal.CreatePayment) // Middleware diff --git a/pages/paypal/cancel.go b/pages/paypal/cancel.go new file mode 100644 index 00000000..d607bf5c --- /dev/null +++ b/pages/paypal/cancel.go @@ -0,0 +1,15 @@ +package paypal + +import ( + "fmt" + + "github.com/aerogo/aero" +) + +// Cancel ... +func Cancel(ctx *aero.Context) string { + token := ctx.Query("token") + fmt.Println("cancel", token) + + return ctx.HTML("cancel") +} diff --git a/pages/paypal/paypal.go b/pages/paypal/paypal.go index 6e1c1846..94adce8e 100644 --- a/pages/paypal/paypal.go +++ b/pages/paypal/paypal.go @@ -2,17 +2,15 @@ package paypal import ( "net/http" - "os" "github.com/aerogo/aero" + "github.com/animenotifier/arn" "github.com/logpacker/PayPal-Go-SDK" ) // CreatePayment ... func CreatePayment(ctx *aero.Context) string { - // Create a client instance - c, err := paypalsdk.NewClient("clientID", "secretID", paypalsdk.APIBaseSandBox) - c.SetLog(os.Stdout) // Set log to terminal stdout + c, err := arn.PayPal() if err != nil { return ctx.Error(http.StatusInternalServerError, "Could not initiate PayPal client", err) @@ -24,18 +22,55 @@ func CreatePayment(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Could not get PayPal access token", err) } - amount := paypalsdk.Amount{ - Total: "7.00", - Currency: "USD", + // webprofile := paypalsdk.WebProfile{ + // Name: "Anime Notifier", + // Presentation: paypalsdk.Presentation{ + // BrandName: "Anime Notifier", + // LogoImage: "https://notify.moe/brand/300", + // LocaleCode: "US", + // }, + + // InputFields: paypalsdk.InputFields{ + // AllowNote: true, + // NoShipping: paypalsdk.NoShippingDisplay, + // AddressOverride: paypalsdk.AddrOverrideFromCall, + // }, + + // FlowConfig: paypalsdk.FlowConfig{ + // LandingPageType: paypalsdk.LandingPageTypeBilling, + // }, + // } + + // result, err := c.CreateWebProfile(webprofile) + // c.SetWebProfile(*result) + + // if err != nil { + // return ctx.Error(http.StatusInternalServerError, "Could not create PayPal web profile", err) + // } + + p := paypalsdk.Payment{ + Intent: "sale", + Payer: &paypalsdk.Payer{ + PaymentMethod: "paypal", + }, + Transactions: []paypalsdk.Transaction{paypalsdk.Transaction{ + Amount: &paypalsdk.Amount{ + Currency: "USD", + Total: "7.00", + }, + Description: "Pro Account", + }}, + RedirectURLs: &paypalsdk.RedirectURLs{ + ReturnURL: "https://" + ctx.App.Config.Domain + "/paypal/success", + CancelURL: "https://" + ctx.App.Config.Domain + "/paypal/cancel", + }, } - redirectURI := "http://example.com/redirect-uri" - cancelURI := "http://example.com/cancel-uri" - description := "Description for this payment" - paymentResult, err := c.CreateDirectPaypalPayment(amount, redirectURI, cancelURI, description) + + paymentResponse, err := c.CreatePayment(p) if err != nil { return ctx.Error(http.StatusInternalServerError, "Could not create PayPal payment", err) } - return ctx.JSON(paymentResult) + return ctx.JSON(paymentResponse) } diff --git a/pages/paypal/success.go b/pages/paypal/success.go new file mode 100644 index 00000000..35512c4d --- /dev/null +++ b/pages/paypal/success.go @@ -0,0 +1,37 @@ +package paypal + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" +) + +// Success ... +func Success(ctx *aero.Context) string { + paymentID := ctx.Query("paymentId") + // token := ctx.Query("token") + // payerID := ctx.Query("PayerID") + + c, err := arn.PayPal() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not initiate PayPal client", err) + } + + _, err = c.GetAccessToken() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not get PayPal access token", err) + } + + payment, err := c.GetPayment(paymentID) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not retrieve payment information", err) + } + + arn.PrettyPrint(payment) + + return ctx.HTML("success") +} diff --git a/tests.go b/tests.go index c0a6ef7f..e6176bb8 100644 --- a/tests.go +++ b/tests.go @@ -187,6 +187,8 @@ var routeTests = map[string][]string{ "/import/kitsu/animelist/finish": nil, "/api/test/notification": nil, "/api/paypal/payment/create": nil, + "/paypal/success": nil, + "/paypal/cancel": nil, "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, From 4f9cea89df3114714f73bc5d429722543697467e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 03:14:05 +0200 Subject: [PATCH 264/527] Implemented paypal payments --- images/elements/thank-you.jpg | Bin 0 -> 302551 bytes pages/paypal/paypal.go | 11 ++++- pages/paypal/success.go | 74 ++++++++++++++++++++++++++++++---- pages/paypal/success.pixy | 10 +++++ pages/paypal/success.scarlet | 12 ++++++ pages/settings/settings.go | 2 +- scripts/AnimeNotifier.ts | 29 ++++++++++++- sw/service-worker.ts | 2 +- 8 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 images/elements/thank-you.jpg create mode 100644 pages/paypal/success.pixy create mode 100644 pages/paypal/success.scarlet diff --git a/images/elements/thank-you.jpg b/images/elements/thank-you.jpg new file mode 100644 index 0000000000000000000000000000000000000000..75ae85c13883a79f96075d03a566071587d08a25 GIT binary patch literal 302551 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�}U}7;Ly0cQG*D zlIG33zlbq-TSlAujyulRIg?yN*EUan`PpeApT-@A1SXD#h8qb?icXKW_+H+?p1(}! zNtcX^q6?4t+piIdZYI|*=`abLOS~ah_)9@JOvSo~akb!qY=(A^zJDti?-x9jxqUeG z{e6y`I*MzY88_S$>4~|`!k%C6!r-oM_BVR3LGugOc*F1alsO{iE}F>k{4u+9ik8C4 z9SOY0{xf{jf77b06rKB9mqEZqM@fKTxAzt{q3^O9k%Ar8Y`=dkluOz2c;|a5Rha{p zyNzlM-Yy9%JX2P{JMV-y_r2R0SNW$ezj|G;d`_FC`4*>H2e;(k64?7*u~cU28&`!p zY`2@_PAYt7pJaQOS#8$2$1FwP`Zp;}xiht^+(2*Abs6!sRsWb)u>WV6xL$HbXPmAb zyY4Odi>FWi;bU^Ee5`gnqggFEFf)BItG$VTq|Urd!KO)>lfs^FoR#FVb@G+Bw`QIb zWHbM&`JaJjAJ4SDBD-rZZ_vxf3D>*H{>Q21+JS1}R)c(0!=D*%* zb9Pq#)s1sqa+nM~wwce1Tg`uwL11m}hw@qX1aCcVdSu4XvLTzdRZ4*2#|jhZ=r}SZ zydO(nmS7i&`A->=k-+#P+cNm1&T@$t9uiWk4;7JF@}JUa86?!NVDlc(iI zZJly9C|%1sw_H|RmT~*1!gqIPwXTex__=*aWxQAEiJQHL1oum3hnC&^DOWZB>)irr z4kw$vb{TG-{uM7W6&#q$qApCFw^`o$ic{{6x3Ai#JU4ef6qK%aDOscH=f$pWv)rkX zrzii|5|l5w%U#w_l3wPeE6I(aw*h<^;)AcegY;*eT{l+(0ak_YT^AzbXbIzS} z*j?3@aeP9}xyL70o?I#Cyxe3~sqC)XCw}dA^nCF~CRume>&`1Xli7?slPzu5?#jLC zeOvA9CXLCHT}=e9D9Yt`tX@Aox?A_|oj!p-=5e1Ys`jneSTaF@F=- z4}pXC#U4M@(QSTz^w0Z6vKqDz^&<{g{Oq5ykNtAmY~Q)eotK`3OqwaAe$A%tb=?sq zN!NIzb5d>F?#?Z?Ed6HP73CVjd0pbDNo22H>ef$QlACUwtjeru=SzB|Jm+$l+nS_B zy;G-ps+Rmt+Bjv4Yj?;IBeNXesFaR~$<^JB>gxo|O0F8!F4^OK?~~$6?q}Pkhiu!k zV(BS?6MG|L_BozsuRfv3r)_M0@y4b(Cqr(W671FLODO&|dy3n#m~dnxUF@6Tzz$(MW`#!!q3lbq$WMc3d)Q0nC7r(Wtqq|y?+Wve+A4wD`m1E+3K}; zhK${Gi*md9#vW%o)|H*y;oegEqhsDQw({4nV*A+lltouPi!O?(J2dmG>9>e$ngBef_#~!l}96o;eEL zsAf}7ex#!C0CZT-;a&2YuX(e6F0%A2j{E&f*`M3@?2~nyUf&Cu6B6edyMuw@_qSTX z7gEM-FHTm=AD%fg^xj)x!`*>5WBL@N7fkP{zNNTJ@xnchpoZSJEZ><0?g}_pC@Hdi z=l=HYv$N*$oncPryrZ6Ne3{%Rbf)-F<&oADuFNxAgywt1KKXIT|4Gy0XBrEC`Am!# zY>c>m{i@Ur9{-X{(MErYei_e-GI*47ELFF%Jv1#P`;2A9oJm=;=WmnQT zx!5HuOxAtR{a(vATWrepn7$nWm+RY7IJA~q3T64%uQSxxva~GOOOM~^z3hcY<{Owx z7`AakS7tBazI~a0erL1cHsgRd8-;narCdL9Z4#boy`)IKaHnjATbop2rupQZNiDY~ zc`q-1ez?OV`HanxUB8-Vm>ZWaF@ApBRh09S|2^4?7X|O$Jzw@<`C4D?uPMn#XHR)% zme>8;>d}=yIvf5ocwc9lv1hMfAD)>0Ys>ui}}I)0ej|Qt8p_D2i0N zS*h*nTzL6;{-YShn0pU4x)ds$vYcf8D35pcqN`S&3L4#~W2Waz9DXhLVxzOyDWmWk zyB@F_mMyg`TGKCHx8}FmSGRZaTl{9lSp8lQHCfBa+lV<^Y7tk>pV^C-9w=+l^W3+s z_JU!{V~%LuHAV_?_QnduR^==u0hdjlt*@@LKgFgxGjYwUxI(SW8MpUz?38r!JK0<4 zpP0OF%9*9hc3w37W!}F?EJ8HgZl7{Y+#`qnaJ`_}ri=CVF;(rh`r|t9QObHRo7i;e zn->J9>exou&gWlocF9HYo7V;0zMswz^4=Z5W$BafwQ}3h6$ekB@D^#6+ogY6qTcS` zjk0a~S8sasR%*`6PtM0|_j_&<4ch0~+P$zf>Y>7}w#QYq0yl$pJ+l9%6MIH`F~?Nv zAE$*k@6$dtb=!24_|DmhE^?p5vgEsqRO&+)FS@vKi|u@8zoky^udxT*KPx5nZ}U3V zimTuH6yI5|v3}1fHT{;qMG1h24@xe@}8 z_Duc8!razzi$SSzow)4Wf!@7O>!wYSK0AAYD!;pC-9BZp zRPxR1#HP7AsaFG?E8V{w6-zl^sFG*;&M+oZqp{g?$G^WHUWD#9`kcje^tw_}O5CEP z*&5GJ&ad&8T)rXwR#0p$&!d?KI0Cl*`L|=w(%*qPM<*UAX5f2wVy@|qy?$QTA6anc zJ^1<8yj@t(;y*+6>jqYXh?;#51i!p(k(TG+XlRJuCMx>6qV*ILV_p9`!M7Y0ZZmQi zXH-25xHY?tA@@$aL2j!uPvh-Z4eSgNx(wV59I6cW7@z+wU|Y9>?ZG?#6b6R*;`OVy zNqw89xm)E1-vPdl&krq}`Zu?k$9nzr<(YLebPsrh@-!vvOo+@C^N%;3DRljYq|NnT ziiWiM8RWO)dr+9dRP7^gjGF*uRWHF1B^U*Z1l^s-4L}x83d=?hUig z4-wla>Y1&i(3ZG^@8S#jbwWo@dG?(W+rEywxeGLh!CkobzG0J7zrX(^@ur7&Kg-zM zSO0hJhf74$J!d}iv$}h(H?~~+=qt^U`G0d3_S~AS z`lLv9!KS6^-{TL?t6yyL(K(|; z<=+1cH%{{Bf0NXew|;4VJ*#qa(A&txYO{N*zkDro6?SP$j_wj*xUuTd<1HD}+P~jg zeNF7~<(Hpb9`zb-j!s?6z%9xK?p}fp%L30{uz-5k%(+{bx82*Zk)Pv6z9FCO71yO3 z`nJt}Z8VcJ&nd-Tv-Bs&wTt`gvt;h}MqX%b^llRRd9f>K*Ze(oPm^mF9_5_)?fCu- z)2y&3``pj&-M3}SvenN&nf-Zs%C=c*%6^YucPj79TO0E0hBHU8^6Wc|8)|Fc=JD^FM*x^S!H#;m`WH%k@XJp1WWo%%fOB(3taY2r*9H-^3X zcO@flVnn~)ky}f*_5SeNIBnLMIZxKAtvSEOFIM|{mb}sq^QXa2uJjvUy5*KPXY!+8 zH>&HCcN_XMP5Jhw`bWLAc{$65s<{V#NyWRJxa=R*DP(i=`6>HZS(T+tHn+viqZUuu zK5y2FRr?lwldP{v@#>u~c;)W34bdr{p5>L3_RLF)Qh#5`dgwy=o~Fl(6q#V_It9v| zA3kzu2|2)ayLKAC-H+|^oQXLGeD=v#wHrmUJO8{q7&d#4(%F?SmDGPcx%x|S{S}4w zCl6E2zD?5%%>6pg-5`U zI6QuZ{FlPijF&dQeso%CdEDaBx-_jR^q^GjEU&l)>nD^eE$X;mcVe2A;lBMwQ+-rY z-fmuRoU)q7Z_;`N^8>*lu~sE6Th+7HZYnu^t*^(Zw)&LGhnvr`WeCmOt)02Mph`yUyRxhz-RzOMDRo^4@tYG+i`t<>N1T*IQ{_A5NJSbg`H z-rKpoGErB<9_+f2!g_DT3Tpnb4A?SrZX}Zg+y05H*k5tE} zfK#5SwNInIBnL_`1^Pd7>nL?x9iv&Y)v?Wwf9>20Cz{sr9n>w`n~>D5G^tQdH99?- zFJ5x-gWF-R_0r=EoMl$*DPKDKipc6CKRhcnx6FB+E$1D-aB|y%9c7v)-A%i$?q7ZF zaLa1%VDk-UQ_tqkt6Tl^vE5wTqMoIzOnhhWP*Sr}dO;0R=?+%&2PQt8{cx>-ZwVk zKN1YK|C;qOZOzN>VDo9$T(0YEp5X zD=uZ7xqCJ0)=`UNFLze&YhY+UHgzXoj3A4{vKzT~4;d;?iZtR{ZSrHvbsymnzP!BC zZ@xIW&Ha>;DsbsCD55Pv3lhi04W{?#d%uMct?9OG!OE5IrSy ziAun+(>KMGwWh4CpCF>TG*3u?r}^eP!Nt7FLDIkeFia@8vOw{WpvG*4KXY<}-?Dmm zye+XROWDnDrkSP9^orkqdzZPv zue`bCdHY{J6TXwAc~0o;jMt(r+x6;ve%jn#9Aq^$Y}+%FR9uYI7C7+Kr?^t_4M$4yDz%LrS&^OdYy!4FIee98&ekHQHJ^2h zZ#q1hZn&+GTSO~Q>Eh4RcH6{l*-jp=T)lLTY2?m*Nq0ZosF=7_XKK$W+rX{fmGiaw zlHP6T$n7+nba{Eb;eza^&ue2ZEG@EOaM@A%YvDGX@+m)953j$<+Pr7^ftI8=nb4qp z&I@Er{#^WAoxr##;pG{*h6V=T+`Z=e*GY9QdDAxIQ0-rVw=G7~-!sg5WcKCX3hzRd zl@VLCj%O}8aB{{?pM2%=oUpYUr)Y&0?d>sAE|hl(NcDUtDSB+m)b_9k3uJ=+8cgWj zY|<_a>TE35os{yBVyRq&((+cFKA6?eJ!q70aAAFm!zB(F`eJ z5a7DHDY=2^x`b0y$=A(E4GiUnVnl)%1^9mG@OiQ&yly&U`be^Q9k;>tj{N3?7e0<# zx4mu7@JkUcZeiYhQ)Kr!*Q&x|=TE)6tUtBLK39qidiaCU?eLVU@86HKFumos{2Sh| z!|Hpt*@hD#XY@043+DP9F`p#s#?iOz!CY@+mY2qVQ&;Pq6#38aw_{IQ$c;aJ&i?0r zEuZ%MgUx>ix57uyE@Zg>{#@erPk0CCUoEdiGWoBH|4G(J>dH%gJ8^KqHj%{Txsr-s z3Y4Pk$}ZKfyYe}0@{N?IY)9YU6cMbFKH<1aVSZ;{(qV=|?^(ZdGZ+%Y9wof$PHydL z#L3%-9rpmrI`wz~txSkz9#Yl(4XMS6SU&o6rHqtBRn|^e; zs`x5;?b7XwM7CTy!@O9~uq)5?=6{CiQ6JyUGShUdtmU8iJ6BX|*XjPVvo-2fsvif< ze<>f`r`2_Jd6HP*tELz2L5@?Z7VP5n6jWStkb&ohypPMPRRw3}^S4I4I_bW?%9P*B zr#_nZ>8D$4-42)6c&zQ4%*G@6Dx~P>=IirW^*)x!)SixTO>Vzz<;LarzW@Bhx3<&X zT>ok~Wv6}f-^Us+b}l~uRe716+^XIWZEB$`o?o3!iNjAr#@_hEtX|8r zk(Dy;OTyQftUhYlW|73?$L;Ph|3enntdBX@!$LM*a7xqoyJp$eT`fN)`j*T&*Cg`k zO6Tt0gK00P^jDP}e43ct5TBmk;;@reCF<L~&GR+s4m^2XKOxUnljCFU-TIe4MW>_IR;r?De>h%H5ukcKeyrOTw3E>eQ92744Ye zAD5V$a$we^GyT@-dTE~nPbjW%@;_BGC!&wfd;OWVWonu+x4otx{2Z(znPC$;%j??t zx$Rj8Wo;NJ=F$$WmJay)&WpA!q z$4`HH^o;#4J@55rS|7b!o;rWatv_-X@;NqaJXD-HJNa6v;*M8m(z{t6?&tcwiTByd zqi@Xm-qplb|KwfWRjNE|`}`+)v(?U~zU8{wT{3g~PqVXv51y#UD+yK=25#;7E}NO+ z5UI5Kyr%E=)35IClMIuLeCF>jtW~r>o>?MfN2JD_*L=4ht@k~CKF^1Jo$;%H+_`MR zzPmEjuf1d25i-T+k;B83YyMF)`Hw`je4N-D&2>F~=EcBE1yd&Z=f`mCEZ;C=flbmJ zkD{DMWv;5e2cMN#)VQRry?$VZ*_FNaJ|Zuo6LU>IyKGPk^-2lzHJuo+ldtQ@HMM#l zotO8Ow))s@tFHQNsx6TD$MHbalpWU_em$MFIZ)lSRp`bZMf2Q%r`IFdtd2yhEn21d z&nIC0;!XSGg_)%nt9C7a^Sa7v=i+@6MDiw>XiLqTT`~Vq=j3MprS8!Y&!YbFbnL%= z`@_sbmb-r0-d`7Uj#;{Kp3vgxUC+{&awHYE=dZ9@thapSmKDoPxMwd@UUqx$arMHY z4&Gfg-&*#p+O05U{g$2(JzKj8w>SM2T8Qh zM9%m3exH>dRRtGP=Yc6MJrcK^!}H=RY7&&&>9U~M;Jm6@I51YQo% z;y;egz7L=O7RtJo)-kJW+R1Tr!;RWQLPDO;_e?W#pR=EzO~_(K;FLb)%?`^~aC2$1{Pi_?m-@E&ZT)p&yU?uCPwzTo ze(-HLSep1^f86a~iwxaGPe|6p&ChlpLWXx6b+ zDf-hVm*+m(>MQQKc6ZIcg*%qcWL7_~p?>r0G2a@NH@9Nzl(v^oj9hk+`P_^t6*uQB zad`PN<4o;SnOQ%?4bJZQ9vkyw$xdFEoilaMxGQ`ujk3B`bYAz_)Ota)Y|GjX2?A~l;cShCnyls;5!ZS}_IO?@k(@S%qe%42|6BC89F5l`Z z2&}w4%QP*TP4{%&@8eD#3&j+8c+|y=WCG;^4ZgMKUuWQFnp3krpw=LVgXx~hAeqxM|ef2Cbwsk;1o(XOkpmot_JA2}_f)Oz`bYmo2Td(M^zcv$6^ zNj+TiuH(@y&GX&IHNM`um(eGu61nKRsM)Ea)SJRR>&0u6zASwfBYI+4a$(5b{W;ze z%ieTq*oG$?EXz4L?RMRcogDFIF<~jkve`bzZzywV-LmeD!7*WHRrcyd8AlbB0^LP& z6l?=pin{Y&bg}JlXyUF&V~}8-ed;jFHEDLYA3>TAzmZYU^FR5I z>sILauRPNg9K(>XA*)!C(be})bmjYs=&4iWJ}HP8<=#H}OHt3#F<)}pZ|C}e?}925AIV>{nG}A&tu_{=r`M4-}9=7J^Q(DL;KVdNyYsB`{ywzi(Kb^Dqr}vZSRsVSFg|HDdsz8 z`sYKAyjJ-Q6R+fYxjTL<);QlSmSb4%_wPT$I)-;Udo@MQAKSmfpw0!Vj0{N-BIQ(D-@V^WW~^v$OmY zCtZq%)BV)##<3*f%)~@tj=d;g%D<;!U@Dk73Et~#)>o2}| z>cjc9?WgB1Kk|KRzMlT$Cuxc@#{^&hT688lC|5%M&9OAO9igbN{g7+EDa*Y+ z&S+ZYU-|b^PZ#kCp3|;e{8rV_ab~a0s;%oXJA*$l&1dzxueu_B!pAy8BW*eRWgjQM zm)iGlyWHQ#Lw_et@@|j5dGXUTmQ!5P5&844%N)(zzP)nU^VA7ZTF0!G7d+8Y+Z@nkIm#w-ugMC`g6wkZk*!`norKeg{t8Avqj(_TNc`de_ zTlmpg=Eei9P{V+wnbT+enmgOL|J|V#I*Y~6e0|}zrQoE_uJ3cVbzRIV^!@OwSgWX4 zMeDoo(ktJD9!ft|DY_DraPw5Yps(>O!7EJH3g&ox;IuZq{wS-qiCb@SJ&?(LkO^4eV{SW$>XvQ-SIIw%O$zY+uf`VFIaR@>1B6#=1g|GuqoNxL(6Ea)#WtLoRc+n!dtZ^rQ>iIb*&ub*zb;9#aZ z%YwV}k0(9IK5{MMSI~pAceVyJ&HP&RcHxHVZ!U&SS#Mq&+&HebV|VF>NlZ`m62JWV z#uB>X;##ilkrNu0+NGQeS~Y8q>xrUlVG~j9*Nct_c^#V&`Zn8E;G?Zf?x8PtZ4bZf zo_2g)<`(}o9-Vw&CpaHneoFI`r>o`Vp!1dIU)qZM3zz5aEp*S_P%rv2CVgSb*P~j0 z#eLLr8=fojpHkXnpQE%(w#!|1Nx>`0HS4bnpV=s4z5RML&-}@TmDk=&S0&qX`-YUg zUUModtjxuwL;U8h-^%6uiyyC9zwPzf#af$_VrgeRre@wX}&AqwaDwm zEW@G{=Cv6oj=W^|eSYnn%c?m+XQqkn+UxlB7@NAE_xkj^y2mD0?rmAME`Pi2@2b6m zownv?pH8oTs1tm6?c5rVs^34^j=l&iy6-=Ch6!!IREzSS;(-XZ>HuH8-s|yXkV+rd2Cb%G3{5x}RRR zC%$KE*<3Zgsw=-9lsfZgscnr_RGhytxW#;y*9;q_Bj)yTo#Gu_qK~G|to-5{G%IXw zYeR+usFSJgqUE3A8TBH#_xM$j*S7A{JbJB-=gbqSVz~ORv-EYytq<)!fmtu*6behZ zv$vPz_0C~aG@JbCN0F5-e|Xr^>Cyb$35~P7PK1gG>&)s}wq1DUY4_CjxtGH3>|6Gd$%3Qzgj8EdCB(n?Yly+k2od7PC0n-Zv8smiiS@sg3Fv6dZ)DW?EW`nou+?e zmr|ViD4ELnBc<=8R(avy)cHR`j@IL&)FLB?oB?7n*! zI@1r`?eoO-Cr3N>*_}CTsz&+`P8?gt4voPPd+KcK*y{H3)8fz8%Jsaryrw>y{P6TohvUI_RFnQanw6r# zZmhmSxte=!rUPxm2ENEFALg>${$y*RyZ7&b>P!^iC9$ z7|$`YHT&&UmhS14o%|sq@z$j*Zn45iNtY*2J9*X3?`cx)-B~a6MOO#;E0)xHpFa7Q zuWrTL%sJPJ%?po5B^s5iJipuG^^})QQ&r|}m;Q7^L)_r@>YD5vLQNQ5zFnRH5Mf>>|eZ1^OeRM?WN^2wG`(Z z6Z|eAoSkseT&6>>d&b<^`2x;IA{IUpo%myCb=9t8>bh5&QZsk8DNH%~$^UixN|SfN z#-^6W;yDMFJs0ng-=ycX{-=1R=Z?G6E&Z$C+|8&_xxHCu)lBXJ+22ahMy}yy2VE37 zlhy9dn6RX=I(P3{20rGepZ^w2)4n=&o{Qtzx~BGkJJs*Gw2rLIy?dQ;Rlf=IHo;=$ zmDc$TytB7`DJyfj;rDsM64~k7{@q&v+OOAgdrNsqiDRP3y-$WN3_1qWWR`7jXpk^- z@L9e;_A1vH^ z--s>leuy~V`xy(w_V8%F7dd#8@vXcIhgP0Mz>eFOx!hb0Vyvqa7#JA2e=BZNEskzE zVg5Yl9rvLpF0Zyu*vWnMwL}0<>#MIl3>Hh7-rWD z)LzzpeWbJC^;gHc8?JX;7GbH%za)3UdS7z7ZRiB1@|(%kKNr0F*`mH?vy$`i2G*tv z4Cgd==NR1T*6v%$a`$Fa$#m%lM>B8whcNzUIB~t_)NJ{Q$I4o_`h`9C$3FkXS(}7g zyd3`->he17A1Y_GU+Nj+-22zB{;+D#@Tcbst~f$oP(pw=&XO zY-8hCq@JFMZ<*2mpTWrKwentC%%!U3m7|70=)1tbVIj_$at-QYg=><^^%gvnD+G9ea8T zqxqQ}`xzCVUThhKgq-4v(Y~%AAe=OCA zUf;N(V@U`D!`|KhR$N@Pwxn=Id*%JLhkqqrxfrXh&5^rQ-p6OAO4_5fTPhe>g7=EzQ-hQ+&uE=qJ2_vUsg4Ep2&WWCv_hsQz%Q;)0tV+0R&J(Y^vZ!mxwKR_;iDPf3Ej86`n|MhyH-5n}?M46M7Ek}d8{;#{ z^Ud~S>i*WTu}(6w{F0AFt{tsOV+=pPzVp<}y<&IO-d=xH{%G#(Q&}^$HJ|i;(qFp% zw$j7R(_Sl;oSPDTaqsM&z4`fv-^&H|FV_;4@t&&U(0KLcb?FUS#`SFaOmezCXExIPR zZ{51vZf?q6&Z)mnPgJ$a)i-i=XWt&5_u}d5%ufIG_`AARdOveieYPZDJ?`g!_4S>| z<=i?aYo6JiSRP(zEuFh3?}lbRi*@0S`I~b@wl_&RZn(#_yRkzcWZEOHlCM0vOZY^+ zH+FNqTBZB5v~9+zxk{HiH;Q{JCv-k|x2{*qG{j6fyoB|L>S>=V68ZreDX)6$XPpgn z6^}FcBqApis`a12oii=s(v(=w>^zSb37xxKoUaP_s~1~Ex`!wkf{yplU_D}Oc4W); zu9j!B)7>xg&b8UGu+PjP`I&r5(7Y{yalu#1HueWKj|gntksBqu$EN=JvhsZ!!ouHHOG6{rca@-1?Z{ zH0R;oAItO?ePHwo>`VOiwmZ2qReFWPp3rGZg?-t#FUfL7&iJ%d_ww}q@7@8QUvEF2 zc=N2H#ZB9p)wRdYiFtXi+dkc~JNxa+Z7;sesy^|0^|LOQhZi;{t(q6mkn-!BV|bzO zjL1-*Rm%+btms`EyEO829em#M04I485Djt$2dY!H+sl?aGmBJ~p`*?e7k_^|kax{e1q_kBqitWv%k>zwkh0Lr7>R&+;ClZi(Of-U}yg z`+DZhtADMP`V-B?y_74Kz5U9=V8C{GlNK-c+x@u@7>jv-TkVgrkBf2L@nBmsVoJ7G;&?dkR|^^bA@qmL;_?s+SooE^1%Icp>_PSB~7l+e`|x?*uH@J-#@!Xie%}r>^$6&cXy(Uf^nvbGY5Z+r^*Bq+Wx*uDK?_6Yz+Cd5u?m-jf7w29`((tM3dFE}4$c6YhyM zS4LJ!PqrO@E9}4zr7O}_>$ksUSME|*ja0sQ?Njc+kxo- zs7f^OE@t?`;K2NVp@CuHQGqx7KA=ztuNM9 zbfo4SoAPs)S&JC2U$V>Ni<<;`(h|4s&#~CRkh3_iZ+?zZ9ownNbJkC6+A>W#;cotI z{}A~%s*m+WA55Qq;mMaj(@UD7W!`**@jrZA_ z!0u&tU;gI#+3xtCLHtW+*+*61+&`WAa?jiDKH0~9`c~ZPYr9?>ecA8uzPmVXwNYry zj{b%TnY(O0JaG>$#sjN@ulzZixWw=rfk0XG3H{_g?`R^iN_>_7R(oq3=ez$ zBdqLW{x<&9DOa-hyH$LYweFtteD>=PcFmJQj6CiN@*HLU`kwFXL5YCLE5!?Mb;WHL zG=D2GWsU+v151N$O=|-@97GzAZBL1g?A8z2{%YrMt}sWl6=~k0yDe?DUjEuwT>OJe zJwEuKOV-1cTh?FubWS$gI$D3)=W|PL^U7sj?JGW$a3s8M_QSjChEdTjr;E5-?r+_d zKV$8R!j+-+Q$7YRchZhFzEqMXWs$Zs`svxv=GUek))MQ@y1S-r_H;wzf=4=?mHs{% ztM1;~c*#PmV?Uo#apCqSCwG*oiYJFh7e=j3m)P`q(Monxmy;%^m}ZxpnRqRtWkXq% zX~C8&i3cLD^^ZaDtC8YEjr7hYHQ3y>n+ubjwr9% zd@C%#-%L8OHe#J~s;B9^gJmvxi??~SUW%UZwsD)1=*ioLJ9pa_Y*Q@No$cI^C*Gps z_v(Ri!XAdZQ;$iM-CiXsc=maM#@p>NJPMyT{BR1)-Sk&{`(+08oC}BfUX;YnJ@G1M z!}QLnE+Wq!PZs-^!29sNfcv!%?`6gB?KX;hEfT*yaCcLhR?CT^)Vl&OKS=8cNT zjxLWmTe;R?L8rpvn|irvrG6zvOog+f-8J66PB&hCaaUZ>r0#0nPfxOcZ+j;#6C5e5 zKIJEapZBlD)^E-kZM(PU+^%_ML47s5ZbV+~mG%n{`F*QwNyg-3`m?osx`iWGmag2y zUDxT+dtpl6>2==1Y}1ZkS7v*$xNq}ciH)YRG9T^I9%f19Y+8Q!*W90PCg}!TUC?cM zd{WN5t%vTHcF(A(S)r{nJ*H3C_eI3Xo$mSm-9Kb+`vr^Z0e->`&1} z`CHcQKk@0-l{?1Yc`XmH{jJ_s>MWb`EybDV7}wmygtrn+5!ZQs{Sdl*_~zHIijoH% zidxsc{_K{f?0w2^z5MBS(_dab9V&FZ@bdQjWtLIDf{IT4tz)_Ja;5erUsoRGM47vd zl14}T7GK*R->9!xM9} z+KDUrATNj3J2opNfr4V+=igjs#fj)Wp0?w@uieG>p0~s$pJqu)sqLsc+xlej=p@!(bK)_nU82O`&$X-UC;cO4{W=?`htMKWY^*u zX8GR^F!wyV_q%QeLxJV?@Vxiy7=)vOJIczwemlS-JY|u!tzm-GyiJvd@1GSrF0^QB zdq(fxUr7rt`Gz#;ZhzT+WlGeUc_p{Xx(xFaCM$U7Z?W)>|8ntiS*33Nfm;{9yq7*% zIx&uMr%N@Pt=qg$W|PCx%vL>{2#5Fe zeR0ES-ksn5eyp_T)(o5fW=7BB8Hej{il&8J-*?Qw>BEiFRl56d+hTpO==i%Au`aGdU$7J5cqc;-X z%4bBe7;#rh98UV$ad1!HqFWPAH$;8g9{QF~yUYCc_1o8PGw?D?xG&zN#Vadz9K28( z)X)%MaA5Af{H!Hq%J(jTo{hFg7;o6x+MG`SH7$;K1Q|doAI6`~tE9_(R|@LL#@#TM zd#O5O+DqOI+Z79{+oOb@&aHEbsXF}Au<7CY6W;RgYYSpbe+rzPUGV133$W0e{izQ* zjvel1l1`Uid`EHe2a}LFsx>|QOJ>ae)q8C=)BWVD9QXe-B}T22=kI@1%lA_~ zbEDv_ij7;WgnGA}y(s^~_ksGCrCJ?^`&XX-k@_KJOHKN1X|~?kTm={EzZyCITM1i1 z?)k>3Xs%qvf|%ujc@hdWyUR;z#T%;UZ(-riot>n$yB*un0g#>k3XDgEQ+Kfbu1b3T zX3C@g3@f)s`_)yr?O(Gx=;MydY0~=->Av4&)pzFK>|6UZPQT^4cCoz1&h5yx~IZ-rhE|&U7|?6W8p-Q~K4& z;A>*6-pM?EJtMWN^3&`Jx+1zZE%jYgE7|h?W8k9A*Rmf!E1u<-d2EK1|H-Gnu8JT2 zV^}nK<|KC8n>)*PsXNv`7w*@+{3~?j*6Rm;wQsWGveM_4dSCVW+sxZ#YlV`oZZ>;* zs=Fk}RY>^66=k{f&Uw2EV{@#&^QC-!gpFLX=JS5^lbkVO-^29(~TOP11H_$nlky)q$|OmoZcZ5El>E%tdiH2&di!O(Qn?jIOSskFIuLQ zcD~QKX|A8P__@b+7oOC_InNl0X;*tYHr4%v#0iCmjk*6wCIS_o}3 zu`)LF1?Jx6zsq|ocFOwMiif)nNI#I)=U94Az_w+=NB*|SEDTNtF}k~Swk(zDzOZuD zyJWA`d{2vyh=g=z#q`TGIk8KxPH>B@k)BptBK(CK-N=@(ez`_D)}tNpx6>V$|=yipW)2$DE>#qXPgd&Jl&kyv*gpnnLbbQIX33y?sn{QBHr9bBocWyJg?* zNou^##IrYrF0b7AMx-H`A@^}z>aK*9!9_P%Htsz9{-U6($(rLKA2*9V3W!jh zF%f<5!bDg=`C^o zd(LzB#ie|`)M9+2Z%##|`m!MJ)-9hba~5gM`BT4xQF-sue9?6P6r zibaaqEX=DyuAO9UDSy-SqBrRN2H)EcBrP`F-^I`p$?Mz1e4k-LjXCdz5+`wGn~r7A z#LvEI5nP>eGPv0N?A!LK(HlEgt4Ce$T>jli%lW9~51*6obOLTkSLN5+1W)hie5)Y6 zTrX+%@)DiWBr`Duf$6vGzO~HRw4Y(N+=4sXUjN;2tMKaC-Zr0QjXdrLw#(k#<`mztKDf|~oq>Tnw{ww-`P7ptH$9Ul2HIt|D?U7VXBw|&?98RJ zn|64ba^>9Y+@yD?>}-zS@}!c*)nAWX_cw`JG^JRiig|sF7T4#88@RWyC?tB`zTtjD zA>jVz{6#EAyLTL9`f~gHuZM2i?p+dCvEa{zF9#F_X7BmW;KE>TdZcE-i<=DmXTLA| z;=mm8;`0F($NLNuKQ!bws6OOi z2DZGz3q%wcZ!XmFczz&9Q~HSj!|p^QzW34Ayp{~PuNnQUw`4LZ*9JpCf!Z{V8JVy-ZR^Aq5k_b;EG)S%W|!bLX?V}`Jd*8 z>wkPF{F*6{44FeP%d~Ct7blGKf3ywzRozd@AY?= zn_0%G%Kp>M%|Z@eK5)6eWbr1uc;zQ+HNGx+dj45=2Y11qNaH78Lr=LpDmxZ(DYyAOy&pA=(+wr;$v!> zuzBX5i>L1A$f}>N(Q5zs@$sx=;d@tWYxr`J+SCT1S9w1#kv7v z642%xlwxrbFWmia1;f;;2fee`JoVGcy%?BW^2vi?z3_v}QzlpbXPEVC(!+)8E>+(= z+|DogIB(|E)Re;UokG-^sE_r+HymI=d2y5;=$%Wp5sT&?;$1Qnxq+8Ww?If|g1~HRN?-(U8C+=Zp z=WMBFVwCdF&gD5@=ZP|EBoQ>th2Fl1iMXDw4< zmcVIOboT{XF5>uf@9f7@$uIZj>!0|%r%W{QBd_Z9M>)?nxG%mgG5PAfv|X7w5{WzR zuT$#vcylvxyLSfLi^RBK~cV6C!A5}t4J@Aze|=&cQlYsy@8Y*3-^7DwEEQsHy1nQ2 z??qFBTOOxx?YO(N_+)$g(~ZqSD>SDCO|@e+P~@LH#iHe;!OOn=Zk}7~;+^-r`CK)z z`_4Z1dEeLhBz*sN?A{)~B@0%(*4%m1iMOI*<(WX{6vN(KA5~IwT9hOYoARD2+?_a8 z?Wo7(!0yza3#Wf%uQ4;)cS-7Y+aGBom&FHtl53yOUZr#VP}95*I`=XzaJh57dud;H zViLc#(4s9Ze`imcXu8w5vP{Br^=^Y|k3}xx=E|$h_uXiJ^S9*0?&U`&#Cbe4lDP9; ze}R?ax~@C2_l>$%d8uElaCohzviaxY=PX73nTXUPO z{$}pDd6iExG9{LBe>U5FsOQJI>@3M?`64GQ=6^NH-EdrRiM`j}{X(|F{$aXvuGqb| zs8(3`&{TEHa{1GrkM8`@wM6N2`!1#SRwm^e%r&|v7yJD=BEayj)mh6uTy;W|OrYHR z9R7x1h5M2lj&A$F@WqM0Zn1dX57s#Wd3PC{G}?-$Fj%wit`ScT)zfI-#CVD~WkU0g z+Fyy~t*KLw$>gwE?n_}yjlB7E&%v$6Ez@VLO8GPS=ib1eI-w=s#a`rf6@3)6U1H4c z@J#OG&F5Y3)|Ex_%yZe;cKqhW{B=5)V;^id!%#eXgTf^5glAqlZ0s(ERb@ZliY|FS zGp&2m#vqTK-mGtDDb0ISz;y8TW#NU2dz?P{DdsI~GTN6hadpa>Uhnf>4%)2y>IHs3 zsyKPHYq`d1WfhJadW>7{t!_Q0lXF)5vDqe<*x5(Sj<&qtuvuc&%&x#44US3ao@a*a~BtY%gN z=kaI19%LPq%;s;HxVu-Vy+mC3-TO&mN5Aip?0dJ9wflXAG2iP62e=Q<-pG@@#_E9d z!|f`))}I+pSv$Y{m2jrKeD&ORCk7Ucm)eHQ1bBZdI6v5tFBP12Gux?OmVrlL!_V^9 z+)lh#JNX_nWF7GcJjlSOS#j|4Tuu1N~|1vC8Ah|wx{)f{K4pvP3cKqO?{|raX zB>yvX7D@k{sh;ukK4|IYkD1G5m-$%SIe7ESv0S&c&oT~I+`gUf<0`S!@ZtW9!w%qX z1+*+jrKI=QL@bsL-WX`Umi<|VeoNqq%?I1`s)T24wfnbn60>Z6+S0#y>rFpuWNOXY zB`jRW{ivtk^-;-^D_44!<*oFJ433MovM=8F%y~V}7Mh zep|e1-@cHaq6h3bC)s3gi8%7@>GrklS8e+)u4i_;{P5G~^gPL>2c@r2 zD6dgpIXi4o(R81Sw(ZAlGfMJb_r8_)Ji7Gw^W=E9s;gCx`aE7F`tMG4@LlEEF8Maq zDBC15b>EFA#tU<6N&=R@dTVR!9-K96U#xQ88A;z;@gZ?@zAGL1le6lyq~zymZ!bUB zsoDGOzSs-xr^k-@OGAcy;k{|-9f%j%v7a=IICSHI2M z-BQ87OzNUdYR$Zh>;=ES9C|MwdN;*PWF7N97M;0wFFb1ZQBGbc`-tzxw0WO?b>9=~ z%(D*BXNYfzaH)PExa_#3X#4W|`~OZf2IuFwR-T$uUgK4A%*(UeHK63qDz%~%>6y}R z?`NzJ>ppSe(4&`^x9&?@8vdi`k;me8hIyL;Hh#@sJUMda@{r;gd#tBaJw5Zz#B9R* zxb>SKo_KC${PSApjK{ibm#o&wQC&NK$+?9I#ed>LUZ!q+KkrZSWdF&3c#1;{ii2Li z%1$mx+1NcfHs8JA<;^GiR!sIz-?Vgc!Lf{^yT6?l_%m;1$<_yoGS~E4r(XNdaC+v` zA3f$px2C;6v6d%w#qL*|)>=&{-z+8tZLdKnr)8ISF-}Z1xq9JY{uUN1h3_d3>R105deOH_>OaF%#jRr7QohJtUJ#PeDaWbBT5eJX`Ei_nsVq4&=UrK8;cG|RRdD?3_ zJv%ELe9vt5mSC4p!L3|k`sSDU=C^qDeaxL|v^#G@(NRY8eJ5(N@*?{-@_d?E^=G-c z`5F807@?IrpNOeUn`-64llG4Hd&~LUkBQdS9p00&jgqXk1{<~RpDv`ibjIt2t3rB1 z%t9xhzWb&nfLrL4T-+0u;DDXmKmS^IG9t*+r8IeS|BG+VG8UXk0Wb9eo)y+rg z3BNK?k84i(^z!{aTwK;{6Sm&m_Wtuor+D_*gav1xyTWI`;w}-NAnD$JngngT<}j6*mib;a=+TW` zaaCKc?wDzR$mhf9$E-%T4ShmVUly9DY}sk?aLcZJlf2hau4(7+d`MZ z)=he?bp3bS4k0IVL5T@H%|`{_{yw*Yp&(thaq}LBa;K=9lPq^W(fu^XdHu^<0;zpb_T<~J6i!>ooRHsa zrnBmOB~QC(^uC;_RW)m`?!OcIrRXur8vz`n~{Ql(Z$@?9pCeX!LxP@Vp<~pHGo!{gy*3DSl;iRhxxd<9cN$#CUT>tSy}I4+?q9>a!d)9)ShH~6H_;R{<(aryyoMoVR@q|K zQ(<{$k8Nn=|NQ7k*503!E=`^7Us*45AmLWRo`~Mu+uogbV|omY^bS{w%(?LSccLfT zt%py)^X{F%$j2adox@3M<`WmD4iW417w>bIgk70%J9m5T!X-y47`$W5oUY`BtAysy zxX9b~PCmt*XWfile_Ax|EPV0BW$|$b2E|JPYQb|%%Q%=b%zAp(oN$w#cXuIcyWi4p z6CH9lakFSeC-6(Kn7f3EuF!K%1e;?{MscsW!^Lx|gcr$NB zP~vq40ltY1tdG97FB5v=m0!tnNSXDx=Gw<6H{{;`dPq<;XTn9(9_t(hW$r|oNhiC+ z>^|PTS~nx+_^y)^t_n5enC>of%u^9sncK2(uF{0Nsb290r7nf@!ct7tHs>^DPTAvP zXENjazr7ok%Fpt&Tq`?MekjMw&Sqia{mZXA=Db_uvijGxG~P>231@5M*LqEQIr|@P zh$@46`~mMbJ#$=U``SJ?zQuKVclm>V>?}&1PuMQJaQ*$c#QmS}4)(uVrxx)*b{I?O z3)$VcVPXH#x6S$DH%UeLf4mk*4Fm7JN8F55wPzX1W>33UZ{zE$T{%f{V+_j4{v0+5 zpMNXGy`Hg6PIbrq1qttVwzhFZRo@q{6_<&1RolLP@}r*Z5B?o|Zc@r^b>!r= zm2pe&eT%G!SJ7!}UjB5c&&ru%oFY$7{`hOS zbKTnFODk3VW_{AnS-`&E?c&RKTOwNIK5pF<9_{nGC1l6$>JO`1)^#bCY!;rk;@Cg7 zwk@^)8JK%6UwtaFcF!%Q#`4cb{q>8?MC3kqt}dUX#*+9=sYicHM(=N3KCh#Z-VqE; z*JZc2s4VyOjO~)P&i~?IR{!d5{d@s=*F7It?5~xTevsyJgSL3Y8nUNu;&v=d`nt(M zF>wp?tj!x76dv9Xm>rqdQ_iI-!2P$pGxNQAQ1xlMP3vFWv7auL8Ge4o$s1GVe4Tbp zbJsm3)eXCjExKY=e&N7FHxpiK<8|Wtg2J1X&R&j;|8*!p_*irGX+@iqOPBVaSt{TD zD(KA>d!@~vyyH^5mwj*8_^smY>qnkzTdUGSlNP2+^c)MAxUnGp( zrYXC7XZ>ebG2=f&Ro%pAdCnHcK0P@%@$HA~)8GEAu`0hRc|YU!e};cYLkfO-%}NzW zy0;`b!h527kc+>ztEm{@alX*5`m|lIUu7h0EIjOedL7S^{ueTC>s3rkm3y}IAJmaE zD{0x5;G}%wL%+}_hWkg4a^0}dSHCX2=}=kQtS;a5x1auAn!TXTGDIXc+y3HW5xJsI zUt7c&(o-KXqmb3#sE=9BZ5 z3;mXDyC=ii)po)Gl9iwosKLR|!p!xO+hKX9g?;3uHjh<$pCZ(A=gc`8{pml$H>I=o zQRXRM_ITgQUf=v#@X9KKrBy~L>oTu$8`SGpIsZ5te#nbS{_G_2iE4^jTk=;-*Z6%u z#WlPzv=f|f8R9CczksLoA4$z|y!q@)oObv}(N+5+O)A&_XDE?rs!yCz@L2bn<}Ee$ zx03Jf$CTWXzTUs#eYMT4u5WTt@?B|Xq6_!F-)5e)?%ety8ScOI*pv zYVz{-?z9sTwnx*Po=wkq_qyFTwZO%|a@Su+PXCHm`_$aDZd=@2(C&XoS7$L7n<2^Hu$OXonz|RcS1*9(tNM0YXto~x%;A^S7mpS zW2QHIQ*!EK>H8^Bq56}OVy8WR6`0bIY#eawY`Va-?Fr6be{vUf1p7+fS*@0w^5W{( zH?6_fZkQPdsWX+iTAFsRerbPM$ZgW3ol31I53s$SVw!yP`)xtX_=3}uR(tz&q^V0A zG(2ezp0Q1ZVVUZ;)Hhqq*0;EQ_Sl%+T&ViHep58>ZqtjK7u*is^(Z@0dF`9m&!V?x z{k)bpKgiyts`^H1Cck)0x5CjKWi4zacOT98nZ#CVlGRbTS$D!;$BIKot_LmKb+1=O z=5My0WDVcSjH+!ujuX|dtPaX~ds}Y*P3zNcYCi=sS1f0_SybU1ay@y;_GeR@15Rc? z`FmFN@q|mAwrcB-Y+Urm?)<{VZD#Q{5u4uYUpsXFciavE_T2ZE_n+rcQfz44CUtlA zAqHjkh(mTPQyw2!zOSTPWNWvy#vw78D@IZ$ANgI; z-}&l8J8np+UDo~f<*)Q2OYaazhl)dS4Zlwx&HLwQAFkpqwoG{W$MQ($Q`V-FUv`PE zy89&d@aom2dsc9n-;2F_@5J`D6|>LemaHw^Gue`%@$&78omSJd)lSCD-M1pC?5uF# z$=k0wFYn86Jej81z2*9!ijZ|WajSPu%DmMa5wy9add8m1UtBi&x!$l@6Q8hxzdrqS z=R@shy&Cz-cN|!(*mU^>_Pi_IE3Lrjl4st)^jn{EPm|o%bv8vejQ*{5u=2XiqVTc# z{F|mjuZopA*D#qi9b9>adHGq^_cP<#s-5c&-eMNKawl?%jt1+(Cr$^LU81ijW=(TiI7T8+jcsNL1ft}^OlW5&G*bcL=-jMV-;%nXAkDRVqSRoDILCyVEz{`zLkPWJa_FEaly?|}WwpiqIw zkO7LL(f-@a`2I6Q>6rd!5LT6cRrqs~#(LoqPJD2;1TNo4B&x{b65t z%q(}>7B}Vu`205$gU~_#>(d2xL@0pB4F}*O6`Aebc?--p=^P91{V}ob%HMXo8kJU! z7k`o~UF$BZ-T&pe<4xaur(>Ud^R}Nje(C-X(|G;g6O%72KkI(N`Cm`=vnTm-Hy5W@ zL{IuDFH_bV?P-4O>9KVW-hXr5>%R8Pp%wer?dM-`iJ|Yo+5Cb=b6?KJp4HdA^U_u) zyR1BB^yrzygE`yZnj5F_zuFS;RQQMHCw0e!ILkM6%GX{`SAKc!;De`5J?XmNeimEL z_77g0*_G$&x5cN^eA1~I-hn5)KlwyXFwf&aleajiWF77+Mev-{? z&uhUeF5N78?o-}APj|+r?z|TjZ(Cia+*{{UqR1R3;Nnttz>{_A0R_%A>vUK$V?b*} zS9C3VxM4{momT0TnVXO^o3~zZ-S0V(PvVwq z>}}g}szhUUlJT;yeiKE5e@%S0yJg;N#jiQH>)7McU6`fgvOESq)%k3X*<`mGXC3Chr)!t?a=vZK1n+=m&@JCG zb`uVry4)z?* z$N9S!T6n!*+itsL!>ZZ)<>zm?dHSBQy5H$er$hWMR?|K>FH(T#G4K$fM9J0{Yi71R z4HXUi7pJ;>Pfc8yWo_#l@1|KPp;NkCf7tnVI7~ja^xyHwFrBTh-~UmXs5`UtsNdYn zRulFej=!IF!hPChySeu9k-3UT6F3>^%{�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

OBx6Smsabe1uiGMkpjb<*CJN+`)TD(*> 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[mMcOHkpTBM1F+u#0 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 z z>#l^U{~2~Zy1abe<)&k_i%k3SJAp}tL4e`0i1$9mRh|=ivU7Mp^xlk} zd~EWpU8c?6yZ&C2e^450=yP9chHn1sut$q30NV~M=jGO+u zCOphpd*`Ta%&Da!%g;G0yf$5}d|Rf?EPVd-Csotf=B`w~b~wSya{bGj5>xdqrA%)7 zx*@T-_x)8?>m@gwr0*Sc$wQk>)hj{uZlA_{t|vD{?%;x!SXcMC$~y7 zw_JU6+R?MGZ-&SQZ9PC_wVc`v)MbJ7xt~> zS$ypMO|FQpO`B!d&PY4w_)BG<+301ps6Bj{O5lcDm#(H;{80%mInJ9Ba?mGEFwi&F zP)S~++W7kIt74jO($jC;s=vOxc5O|MprKZgjlN!T)~Rc=vz&j<5>9ZPHM>84%^l4p z*WNFjzemdGt{;bwnq~b{$t_o&SpM1Y!`0)=%1y@`zs(9?J6-&vmEvX5M_*&l$lnlI zD!6x{&7nQd)r!6!(zP;fzY*^$usk45)MjO>hvQGxi-8=U*In5%W&NRere}O>9?$z; zxA$nB+wCS#)t>IN+4>o6-o>UI$ItKhsc!aT`zM3w-8-|c_{`VYmbO&5=Fi;bi;`dC zwuE;1zM6K*Q>STWjL4DT_urj8yytfB<4T%smiMnCsOXtfRrjPSig13KYga9~pcXL(#HnXgdJ6%@{Zr!x~Td2Ezvq~Uez}CLUs8P%+FEiKr?&%b{Xw=*s(1HlAC!pvhL5f6CU) zNiBk19;~-{dTrwkSvtj{T{%n}A_^LLr<6`@@W@G+cBYm0!=$OxHn8XjqfA*sE5B~?^Ush{1@NZkj z*ScXk&+-2ZK}+Per3kjF8x>5Cu&nvWvwT(jl4;lXJe@b~m-{)jua*VI93nON`L;a$sSM6?EM=8Rt8(ymq)D&nVcCJ_>i;rv9uX?Pue*cT~%&I<-@c9>}RvJVuYSY|&WB0GcXH(VH3a_SC z+?*Wr^H%b4!%eNfzOgPcH`#PAX!2%#wPKbKeT({(aH-JAR^_~D@#pV8YdgWIekN*; zY3KCb^9>uKpNro9R`t7$K|w4h<=YCm2$zjhTeOQ_?n*ps{-!B#gZQ6aB~FbNJNMOn zxK?s(9nN?TwqsFYxw076P-WxfZF5pW$MI6d| z#GPJQEL0E(dcQ~ch84F>I}gWp_GwO>>!R6xw|#Rxs;4@4tyxjOf3M8*zVt2YCf>-J z)p=!$cH7DjpIL8$QdQ$pX20C@y4>99V$!^0B~s=xFMe&Re>$nU=Wy$cZjGZ}dg0-x z!}QY621OpW?%OzP^`EYfI$7Jk-Fda(>zaF?elc!n_~LT0JY42#*ERWNo8GJCP0>pb zIN?11(}f?QxoK(1N|R2nZ7mnPX*lG~MazFQHheywRio(Pv-#wOqV1XoYmW`HOF-=P34w#*Uy%mv0nIFO)oFs^s4E`PIK9{ z>W|9@xSKqkmcIPqf_3vA2b4ThoN}>Qs4L(xw`E2COYzJje#^qs&Sc(|VCH_Q`pTDS z$yrx9flV4aE1cv{u<47$xLL)@Z>_d}Ynq|swYmCFPj}<`z%?aVZb2U^oX%u!e>?4F zeQ#Fy6?bp3ODC@RRbQOHMQirvcs{XZUJE{~%6$|x-Q&+$C$a95mGNtr>-W4{@pjrd zpS#kMq8AGKRTkMCPo0$lGO4)v{F|2R{lc@ZSM6z=KgTXV#Yy7^Ll0L>*&D_drGn~q ziG@?D+qfrM=pK5?w?gl5km3}p1-p4Yx)~loM=E*5`C}Li_Bg%0UwgsG^PN<2+-)Wn zfqTxaDjW<2Vh^hiFefkwZs2_=J88#B_wY1dfyuWEKK~VDF0Y&=Q8UTXez|S>-@uPd zZ3lkU`X=SdZJi&|rufJGV6e6hs4;F+{NvQVb^H1jV&Dkzbi=G*6 zd#Wio-OP29rQM@`mtxT+Pt~ee>{lK-S5X%xA1SxP19$bm@{GdIdz^VLdP#JKNNCnL(b}{ zoBy8P#bENCW98S4- z*UNtVd zXB0m5dNf~Eu4?B4x#%W~VLiIYCvY_go2>g&;wnCElh{2J}R zM-1=ZwW{lGtopcKE#&OJ+hr{beo9X-d}=f7O8jJ2a)9-~#anH9!P0g2IyNZAXU*AL z@>wV7*&o%t6J?fM-Xt=u;^GAHNruHa{4tik`2yRnnVeaWv?!RdUiiGws!98j?krii zdcntKX76kaoTT`D*d9w8?u|`m5dHSfXT7yv{E@e}H%W_^Fsq%pxzAr|Ue0_Mp)!ZW ztP7u3DLI<2T;}IB+cG;fR_1whc%7T6iJxR9k5l)feK8j|FE+UDyKh^uP1(zZ{hxU% zj&Xi@5T~)Fb@JBeleg1<>{<-oizauYhRhjck-fowRxi6%v-Sb;{OVxK{ziaUe z_T`)VROxzkDo@R?)_vJD>x#=#vkQ-0OXkeXnp)1gbl2>h)~C*v`%27rK8abo^y9VD z|L#AP(^w|w=wP+!&?>H*Q>RV3B%E(ufBK8mB2A7Q&9d63dnep`KEYdj$qVc4{~6pb zEL(G66~~M{Gh3?v92P3CIjf_dVd$x9yW6~8lhH78=BF8L0lT+<`Qozd$XuNoU77vz zCss@>k}%`nU&C(Pe^GDF-q`N?hiCeQ-J*_VIBL9Rm?*O7!a|m$8{E5u=GvZU&zSw_ zN#@fO2Ihjji)1T4%CAw~oml!vYEkPh#nP?q4Y!x^#wKv*bS$-gvQ6;luR|_dz9~$* zn0Rr*T25|Fm9{Qr!AsqJ7g;)^MHCkZFmOakN@g=`G`hj$Si;D9ftyRgfyv3Xsm1M< z*Be&39bHq_Pk*2!q$Z-6EHZ(c)ACh^bmGR9Yo9T>blWa!Q4V9wy)97wYj?{ImkaYY zu-NYAl3u9gbTdylBT9SKTJWXt9e0FpPKe_BKH)gqwloF-tG=5;q07E4OfpydHo?j2 zrs5*8Zox^Dbh`vaBh%A&xTzS-P+)XpF_J!G-&cR_*0TQ$%zpJZxet63I(}r&IrpPU zKR3hA4SvU1?^U#-BzcWiS^Di{w(i-S8}=JCh-A#*&W zWjoZpq~Y@PTt%HLDRH*E6j5AKdE zi#AvD+|r!!?9p54yuO09miNsnE$62CYFCy^YL8M> zyaOXmLgyV?rCh}EtJW~6Fz=e;q@8N1mmO8bVx5+CxtuEvvs-JVnz!TnfrQjp-Ppf{ z&ulK9iM&)+XtL6NWgYYBf2Xt#%Lg0HQL2sE=lQ*KLhY$-!IJ~Jj#n=%k@cA!V;5gh z6BX^<%)2Fc+KRtg_a*0R#$G?A!RMIXes%k_)!${E>wNu!Z24ImN@hL$7xR;M&Zb@F zhO6DyOz_uw|UO|D!=aJ1iQ1l4__Bk)(ukCQ)bsZ@br0_ zn)A1BN24b0-%?~z-}!pa#q)mWH!(d+c1>C{siwT;iO%cK6&BZ@NMh5k3awN+`F{aW8Dt%#W%CV zXExmVTv_i@`SV=pooM})Pla|){B!z?)6{vF`ew?T+DrIeJkGyBK~gk#>9tEa3)H73 zy*BDkQPjE-I{VB;vokx(zPlLD{S?h{oAs{JdQsuhcQ4u$Kiu=+nd*M@(aN853nku0 zOy+sg7sYq(-wNh1)kN_T>l=UA708Hl|suiA(&aop?`oqv6CK zas?V^`uufO_o?PTSH5hm+1F~VzVF`eBJ&j|EuK`ZTbz{^+N!CzGWE4+*=En|&=W=S zi-SDAol$wL9r^C{BOT@Jh#lWsqK>5Odg5`b(>NpU`$W-07s~fEJ#tB_fK5s;HN>>Y=1lMEle1)8n_0r9fUPb(6+L88k)0#Vrs(vZT20&+o+FXZ{3xn_oNh zq2lcBO{oz(cX3|kO20h)&E|QN=5Wmn_!XnvdFemHou4v4`t}J~iaa^DVzUxQUe~AY zPBy)zk4vu|Ue&Q;(w&qLtcdiG@Li7&f~GM{8k zo2B{u;;#Mr!DajE`zC2$@b!)^y||ETb&`rn>@>Ci46d0vOO~GMy5#xc$sdhHOTQh@ zT>Rj1_NSjxA!^q$pCs(%o!1%lXe-z6$zk4hp3P?N|E|VGm2Z_c`0v-o18N zF;StbIP+l5JdS>$>pvqVJTtj#_V~JSf=~49{mWNfXcuK$>AR9+W*(FBT~3*)o!VX> z%LM*B{9LW1HS@`-6-9}AY7TxmEOGLkl0=T&3XTJd3%)uqB&TjWeUE`@28;g7hO#v+ zh6lF2+{3t9QGe6l>8)yt5}@ zIy{?x^7~z(v#CW{1>5p7xh;47brkT5T69k8=GrCE&-gY-^o5#!HhLXaICYyuzm%b! zck_)(`?czQ!D-E6d_ft-b1XAgy;#8$_*bi?Yx5B|?rT=H{B0Gj;!i(4>+{UGaV<6G z%&RpOYouO3+xqyaT99nv5eu2Mzif7C)J?Q_@-cL=>hx&-po0&-Mm<>j_U8GCTe%w8 zm#5EBmhwAq9CeD@rXy7^??8*zmdl%!9avoctTzc+J9`(K$-!j)a2IdqL(*+`suHh! zpX9V$IAqI)m9YkkpO)-aHQKp)*-B4~_NT0>mu_!T(NQh7dbm{K-qgjKJFPUH{ymkj zdCt7(skhYEcZXzJdPc16XkHf0bI5MI@*wloVZBZ6lQSh( zhG&X5OmNV%w*9U2a@MmwhrThq`{kyzVtvwwAv)M zAE{^Y&biCB(XFN( zD{GE?vMqehCR)$w&Xsw%IeS-m`5{pbZk5H(Tw2nK-EUge)gO@%lrCY75G^Hb__sG;-=+cX3(G;$1HoqC1KV=!;emwy$%T&ECsiWLe zjfCx2FJjOCHT zJwN89Pc!UFmUMp2awl2hKZ8&8r{&+O1Y5V)+MJV?41Q~|T=n{a#cP%}-CMD_Xx)CL zz6JMGmVN7ZeqxI0fx|Dq9BJN~WcHXL>qzF2qW;;p8M2Nv7dDtCDYPYgE8}5)@hD8p z^ZxpSQ?_0VXg}j{dz*5pPHBX$vee1+YcV?~>s~#h5cr(YcA@Ckt0xSj)I+oNwiT$IfBbPHWMRJW8kB=lXFz@p62 zaCniB5c`%4hiwf`49<%fvz;tsYUUndd1DY!0bl>fDcA*>Z@bJHe64pvgR-C#uk{8G zj|~$RGCK%lGdQkcI^4p+;E}gtA=?TTtuV%Xfh&`p7m2@V(+WFjI88_`aMOu^`93;0~xG1oD{gQu-g}e4Y4hs>qW%;^J`uvvFIgV2k{dz;D z7Mn&2ZJ+S)&DxuSR)^P=tMZ(encL%xxBLmkc?{@MOw0t-BSUY_*rFcQ$w5zs`E()k@d6 z7k9!wZJcTAEB5kP!O{~dulj=Cy?bc2?ZeDFckq zxpiuLpue=-%A0S+eQ$1^7E`wPD5M3 z8MBr-bHZGF3Vo;CTlb>Wbj{S`nuV5z4;vS%UtTLCbNx$(q}H;w&{aM2&3_k)%(1zy zfAH;(iLFZ~he%$1As-d__Rijp2Wg$gTy47+#Drf}YAX#4@mBhj^7Q0CgVIth<^K#x zc4cpa{dQ+}&b`g{U{cM#c;~b^bI!gk=o5IJ^-f{Axr^`Aq{1}@O{UrAU6o&Uhp!0^ zVt1Hn7s?lBCCeQb@?`DK^1A2dx8>I1>G#)Jb9MSz9VwNL{Gcpc(4OOBQo~>-vH+a1 zK5NIiMW5sgb;T65SAP_b4>`2saZ6B4%Hfl5Wi1>{ z!^*l&d(D%X9u)OQ=!ux~r$U>o^z@kuhYr&ebep4|FQarIzgJpVTebxAA`P`g<3AzixOjm+3!)mY>Az-qn%Gix%4K zi(aPYrlwvW!!t>Fc6WCCvhR1dG27|xU9$M6x8y?8d}+1qXU^}KaB}JDGBbngC$>HL zygkJEtHl0m7E?8k=M+3O)pWIdsy+49gW%Vn^$$JrwW>X(A$E7JypLxW?)Ul`ba@W@+8IidPR&e-`lLJY+KT!<^_?7>-C4GM{ixO=eP(w_#G*Og zAEGAYJ!F;M{@`3kiI{)b!k@WwT1rI~qIy`Kh+Gb3H!( zIoZr4{3yk8`49%Dt|_ zyg61#qAF-sL3aL6>Aai1)gfYx+cv6PgQ$iZ=d=>Q`GL+b;^R@Ju03~w`enc_m21nZqhKAh7FTWgIzjo@$ht^NN zb!HT`Z93z5t)Kif*rsyd>`TE5&8Ew3H{NkkZPY(#sG<80{rs=-sDa}uT=YuW@%|2fr!}prKCur)fxX1bYiGuqs z+nEiA z(?3*U81&kc@5H48yU(xFICkmB+$di6^@V#sZEH1WQrxDwlF1r#wl;@lbpC0L*b}@;H{heZ<2S##TS_lINSv#B>e12_8;&)m zC1)?6%C$#z`^h!amYO~~vqz_WOKJ5Ulj$Pm*CtO^^bZdCvT|2Kmu||YBcFtiPL1Pm zh`TsF`cQ?cr)5a^qw6PjSUuvXn3%R`OHOv%w2Hp-yarzDYgRu@`;@6uu9i6YbjiI`r16QQz)fGy4d9QgG+={L9in2CK=lxqdF~RBZ45O?U-(8MrZvJ!juK=Ta zN~y%DcERWqI>YfTjU@?=qIpvJa^Q`XK}$5;HRGJlhK ziLI*T=UqP*UF+-H>b>{=#K+p+bHZmnI%gaze4Kwu$yv*$wKFF?-ys}$g73ty#hW5H z5;ZlWi+|LgP>XhM)tl+H`w||$ly>}`7F#VR+tFNCd%`42lPiZ}N{XM_ki75>e zgx|_emz&oyY2Jqv>6SQ^T@qRhdAIEPV;D=Eh2t1@Nf+=gGGM*1QM~+&XW;uXK8+Zs zzaOQS?_HRAt|Def>6r;-6W7m_UH<9cj@4Q<<#W~gFRhk*<((fhLE+H>mFT36rlzTd zH||PH-PgBMF^DmU zBx(7*Z~^VEp3Km=0eoKoWaijBBA_{fRU&eChw86Kx~}Tg{2ea6y<*opmYVE2vSa$T zAJg;;7AR+^d}o+2>rB~}=7hH#rZciV3RoP2s%}dh(>Sawz46069h08JJD&f|U6udA zF+xFIsJebM=siWyGX)r3rGQJ%JbjklN8<-kX^(I2gL zR(8EJ&}V(J>*&g#tg7DUv=-bvcy7|0A53qq|5d4cp7rs=tgEp(PfCB8^RE=O+g@yT zY~sRL-&5y2Ro%72QaojgG;f-nR6bE8f23byYIx2!;qW8s)nDJ4J(_a(rBCB7^@6*m z&p6v}tLtm{&Hc!{qO11vqj;xi-i6lpT+BtxKo?qcC(7=hCvfw}jGp`ufsg!cpUjFH zrtCj*ck}9fx|f%(og`!sw(qb<$2 z>2D>QUq4ddXn6MaYlnkhqOFQ%N;Z$+5liW|Tc1|@ot<{&$-XP9tN-mdd_4AH$_{0B z$1tx&*=yUU_)2)(^Y!vuxz8wV@|nr$6aKj?EvdPB>sFt{%EJ9;Hr|T9(zLJI&aG

3hCyf;h{zsxAgq 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 literal 0 HcmV?d00001 diff --git a/pages/paypal/paypal.go b/pages/paypal/paypal.go index 94adce8e..d2a1b7e4 100644 --- a/pages/paypal/paypal.go +++ b/pages/paypal/paypal.go @@ -5,11 +5,18 @@ import ( "github.com/aerogo/aero" "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/utils" "github.com/logpacker/PayPal-Go-SDK" ) // CreatePayment ... func CreatePayment(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + c, err := arn.PayPal() if err != nil { @@ -56,9 +63,9 @@ func CreatePayment(ctx *aero.Context) string { Transactions: []paypalsdk.Transaction{paypalsdk.Transaction{ Amount: &paypalsdk.Amount{ Currency: "USD", - Total: "7.00", + Total: "10.00", }, - Description: "Pro Account", + Description: "Top Up Balance", }}, RedirectURLs: &paypalsdk.RedirectURLs{ ReturnURL: "https://" + ctx.App.Config.Domain + "/paypal/success", diff --git a/pages/paypal/success.go b/pages/paypal/success.go index 35512c4d..954531db 100644 --- a/pages/paypal/success.go +++ b/pages/paypal/success.go @@ -1,17 +1,33 @@ package paypal import ( + "errors" "net/http" + "strconv" "github.com/aerogo/aero" "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" ) +const adminID = "4J6qpK1ve" + // Success ... func Success(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + paymentID := ctx.Query("paymentId") - // token := ctx.Query("token") - // payerID := ctx.Query("PayerID") + token := ctx.Query("token") + payerID := ctx.Query("PayerID") + + if paymentID == "" || payerID == "" || token == "" { + return ctx.Error(http.StatusBadRequest, "Invalid parameters", errors.New("paymentId, token and PayerID are required")) + } c, err := arn.PayPal() @@ -19,19 +35,63 @@ func Success(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Could not initiate PayPal client", err) } - _, err = c.GetAccessToken() + c.SetAccessToken(token) + execute, err := c.ExecuteApprovedPayment(paymentID, payerID) if err != nil { - return ctx.Error(http.StatusInternalServerError, "Could not get PayPal access token", err) + return ctx.Error(http.StatusInternalServerError, "Error executing PayPal payment", err) } - payment, err := c.GetPayment(paymentID) + if execute.State != "approved" { + return ctx.Error(http.StatusInternalServerError, "PayPal payment has not been approved", err) + } + + sdkPayment, err := c.GetPayment(paymentID) if err != nil { return ctx.Error(http.StatusInternalServerError, "Could not retrieve payment information", err) } - arn.PrettyPrint(payment) + arn.PrettyPrint(sdkPayment) - return ctx.HTML("success") + transaction := sdkPayment.Transactions[0] + + payment := &arn.PayPalPayment{ + ID: paymentID, + PayerID: payerID, + UserID: user.ID, + Method: sdkPayment.Payer.PaymentMethod, + Amount: transaction.Amount.Total, + Currency: transaction.Amount.Currency, + Created: arn.DateTimeUTC(), + } + + err = payment.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not save payment in the database", err) + } + + // Increase user's balance + user.Balance += payment.AnimeDollar() + + // Save in DB + err = user.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not save new balance", err) + } + + // Notify admin + go func() { + admin, _ := arn.GetUser(adminID) + admin.SendNotification(&arn.Notification{ + Title: user.Nick + " bought " + strconv.Itoa(payment.AnimeDollar()) + " AD", + Message: user.Nick + " paid " + payment.Amount + " " + payment.Currency + " making his new balance " + strconv.Itoa(user.Balance), + Icon: user.LargeAvatar(), + Link: "https://" + ctx.App.Config.Domain + "/api/paypalpayment/" + payment.ID, + }) + }() + + return ctx.HTML(components.PayPalSuccess(payment)) } diff --git a/pages/paypal/success.pixy b/pages/paypal/success.pixy new file mode 100644 index 00000000..ee13b1d4 --- /dev/null +++ b/pages/paypal/success.pixy @@ -0,0 +1,10 @@ +component PayPalSuccess(payment *arn.PayPalPayment) + h1 Thank you for your support! + + .new-payment.mountable + span + + .new-payment-amount.count-up= payment.AnimeDollar() + span.new-payment-currency AD + + p.mountable + img.new-payment-thank-you-image(src="/images/elements/thank-you.jpg", alt="Thank you!") \ No newline at end of file diff --git a/pages/paypal/success.scarlet b/pages/paypal/success.scarlet new file mode 100644 index 00000000..ebb83b08 --- /dev/null +++ b/pages/paypal/success.scarlet @@ -0,0 +1,12 @@ +.new-payment + horizontal + margin 2rem auto + font-size 4rem + color green + +.new-payment-currency + margin-left 1rem + margin-bottom content-padding + +.new-payment-thank-you-image + width 683px \ No newline at end of file diff --git a/pages/settings/settings.go b/pages/settings/settings.go index 761cc9a5..a7a03e0a 100644 --- a/pages/settings/settings.go +++ b/pages/settings/settings.go @@ -13,7 +13,7 @@ func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) if user == nil { - return ctx.Error(http.StatusForbidden, "Not logged in", nil) + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } return utils.AllowEmbed(ctx, ctx.HTML(components.Settings(user))) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 4d551d09..759772c5 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -121,7 +121,8 @@ export class AnimeNotifier { Promise.resolve().then(() => this.displayLocalDates()), Promise.resolve().then(() => this.setSelectBoxValue()), Promise.resolve().then(() => this.assignActions()), - Promise.resolve().then(() => this.updatePushUI()) + Promise.resolve().then(() => this.updatePushUI()), + Promise.resolve().then(() => this.countUp()) ]) // Apply page title @@ -214,6 +215,32 @@ export class AnimeNotifier { } } + countUp() { + for(let element of findAll("count-up")) { + let final = parseInt(element.innerText) + let duration = 2000.0 + let start = Date.now() + + element.innerText = "0" + + let callback = () => { + let progress = (Date.now() - start) / duration + + if(progress > 1) { + progress = 1 + } + + element.innerText = String(Math.round(progress * final)) + + if(progress < 1) { + window.requestAnimationFrame(callback) + } + } + + window.requestAnimationFrame(callback) + } + } + pushAnalytics() { if(!this.user) { return diff --git a/sw/service-worker.ts b/sw/service-worker.ts index a528e0b7..2f843de4 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -94,7 +94,7 @@ self.addEventListener("message", (evt: any) => { self.addEventListener("fetch", async (evt: FetchEvent) => { let request = evt.request as Request let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") - let ignoreCache = request.url.includes("/api/") || request.url.includes("chrome-extension") + let ignoreCache = request.url.includes("/api/") || request.url.includes("/paypal/") || request.url.includes("chrome-extension") // Delete existing cache on authentication if(isAuth) { From 301e115981e2bef8aad3103a59c15a9f55498177 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 05:05:44 +0200 Subject: [PATCH 265/527] New guide --- Installation.md | 78 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 87 ++++++++++++++++++------------------------------- 2 files changed, 109 insertions(+), 56 deletions(-) create mode 100644 Installation.md diff --git a/Installation.md b/Installation.md new file mode 100644 index 00000000..0e825372 --- /dev/null +++ b/Installation.md @@ -0,0 +1,78 @@ +# Anime Notifier + +## Installation + +### Prerequisites + +* Install a Debian based operating system +* Install [Go](https://golang.org/dl/) (1.9 or higher) +* Install [Aerospike](http://www.aerospike.com/download) (3.14.0 or higher) + +### Download the repository and its dependencies + +* `go get github.com/animenotifier/notify.moe` + +### Build all + +* Run `make tools` to install [pack](https://github.com/aerogo/pack) & [run](https://github.com/aerogo/run) +* Run `make all` +* Run `make ports` to set up local port forwarding *(80 to 4000, 443 to 4001)* +* You should be able to start the server by executing `run` now + +### Database + +* Remove all namespaces in `/etc/aerospike/aerospike.conf` +* Add a namespace called `arn`: + +``` +namespace arn { + storage-engine device { + file /home/YOUR_NAME/YOUR_PATH/notify.moe/db/arn-dev.dat + filesize 64M + data-in-memory true + + # Maximum object size. 128K is ideal for SSDs but we need 1M for search indices. + write-block-size 1M + + # Write block size x Post write queue = Cache memory usage (for write block buffers) + post-write-queue 1 + } +} +``` + +* Download the [database for developers](https://mega.nz/#!iN4WTRxb!R_cRjBbnUUvGeXdtRGiqbZRrnvy0CHc2MjlyiGBxdP4) to notify.moe/db/arn-dev.dat +* Start the database using `sudo service aerospike start` +* Confirm that the status is "green": `sudo service aerospike status` + +### Hosts + +* Add `127.0.0.1 arn-db` to `/etc/hosts` +* Add `127.0.0.1 beta.notify.moe` to `/etc/hosts` + +### HTTPS + +* Create the certificate `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) +* Create the private key `notify.moe/security/privkey.pem` + +### API keys + +* Get a Google OAuth 2.0 client key & secret from [console.developers.google.com](https://console.developers.google.com) +* Create the file `notify.moe/security/api-keys.json`: + +```json +{ + "google": { + "id": "YOUR_KEY", + "secret": "YOUR_SECRET" + } +} +``` + +### Fetch data + +* Run `jobs/sync-anime/sync-anime` from this repository to fetch anime data + +### Run + +* Start the web server in notify.moe directory: `run` +* Open `https://beta.notify.moe` which should now resolve to localhost \ No newline at end of file diff --git a/README.md b/README.md index 091f8fd9..c2ee8994 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,57 @@ # Anime Notifier -## Info +## What kind of website is this? -notify.moe is powered by the [Aero framework](https://github.com/aerogo/aero) from the same author. The project also uses Go and Aerospike. +An anime tracker where you can add anime to your list and edit your episode progress using either the website, the chrome extension or the mobile app. -## Installation +## Why is it called notify.moe? -### Prerequisites +Because we made a notifier that takes your watching list, checks it against external websites (currently twist.moe) and notifies you when there is a new episode on that external site. -* Install a Debian based operating system -* Install [Go](https://golang.org/dl/) (1.9 or higher) -* Install [Aerospike](http://www.aerospike.com/download) (3.14.0 or higher) +## So it's just a notifier? -### Download the repository and its dependencies +In the past it was, but we're growing bigger by establishing a database that combines information from multiple sources. We also have our own anime lists now due to popular requests of adding episode progress changes to our browser extension. -* `go get github.com/animenotifier/notify.moe` +## How does the rating system work? -### Build all +You can rate each entry in your anime list in 4 different categories: -* Run `make tools` to install [pack](https://github.com/aerogo/pack) & [run](https://github.com/aerogo/run) -* Run `make all` -* Run `make ports` to set up local port forwarding *(80 to 4000, 443 to 4001)* -* You should be able to start the server by executing `run` now +* Overall (this will determine the sorting order) +* Story (how interesting was the story/plot?) +* Visuals (art & effect & animation quality) +* Soundtrack (music rating) -### Database +Each rating is a number on a scale of 0 to 10. A rating of 0 counts as "not rated" and will be ignored in average rating calculations for that anime. Thus the lowest possible rating you can assign to an anime is 0.1. The highest possible rating is 10. The average is close to the number 5. -* Remove all namespaces in `/etc/aerospike/aerospike.conf` -* Add a namespace called `arn`: +## How do I use the search? -``` -namespace arn { - storage-engine device { - file /home/YOUR_NAME/YOUR_PATH/notify.moe/db/arn-dev.dat - filesize 64M - data-in-memory true +Press the "F" key and start searching for anime. - # Maximum object size. 128K is ideal for SSDs but we need 1M for search indices. - write-block-size 1M +## How do I add an anime to my list? - # Write block size x Post write queue = Cache memory usage (for write block buffers) - post-write-queue 1 - } -} -``` +Once you open the anime page you should see a button called "Add to my collection". Clicking that will add the anime to your "Plan to watch" list. To move it to your current "Watching" list, you need to click "Edit in collection" and change the status to "Watching". -* Download the [database for developers](https://mega.nz/#!iN4WTRxb!R_cRjBbnUUvGeXdtRGiqbZRrnvy0CHc2MjlyiGBxdP4) to notify.moe/db/arn-dev.dat -* Start the database using `sudo service aerospike start` -* Confirm that the status is "green": `sudo service aerospike status` +## How do notifications work from a technical perspective? -### Hosts +There are many, many ways how notifications can be implemented from a technical standpoint. There is e.g. "polling" which means that an app periodically checks external sites and tells you when something new is available. We are not using polling because these periodic checks can quickly drain your battery on a mobile phone. We are using so-called "push notifications" instead. The advantage of push notifications is that your mobile phone or desktop PC doesn't have to do periodic checks anymore - instead the website will send new episode releases to all of your registered devices. This consumes less CPU/network resources and is much more battery friendly for mobile devices. -* Add `127.0.0.1 arn-db` to `/etc/hosts` -* Add `127.0.0.1 beta.notify.moe` to `/etc/hosts` +## Can you tell me more about the history of this software? -### HTTPS +From a technological standpoint we went through quite a few different approaches: -* Create the certificate `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) -* Create the private key `notify.moe/security/privkey.pem` +* Version 1.0: This version was just a browser extension with **client-side JS**. +* Version 2.0: To decrease the number of requests/pressure on external sites we made a central website. It was written in **PHP**. +* Version 3.0: A complete remake of the website in **node.js** supporting 4 different list providers and 2 anime providers. Episode changes were not possible. +* Version 4.0: We switched to our own hosted anime lists to make episode updates in the extension as smooth as possible. The website is now written in **Go**. -### API keys +## How many developers are working on this? -* Get a Google OAuth 2.0 client key & secret from [console.developers.google.com](https://console.developers.google.com) -* Create the file `notify.moe/security/api-keys.json`: +Since 2014 it's been just me, though I do plan to start a company and hire talented people to help me out with this project once the stars align. -```json -{ - "google": { - "id": "YOUR_KEY", - "secret": "YOUR_SECRET" - } -} -``` +## Can I help with coding or change stuff as this is Open Source? -### Fetch data +Sure, the setup to start contributing is not that hard. Try to get in contact with me on Discord. -* Run `jobs/sync-anime/sync-anime` from this repository to fetch anime data +## Can I apply to be a data mod / editor? -### Run - -* Start the web server in notify.moe directory: `run` -* Open `https://beta.notify.moe` which should now resolve to localhost \ No newline at end of file +Sure, just contact me on Discord if you want to help out with the database. \ No newline at end of file From 20da49bc32fb9de18e1e5df2859e27be0a30955b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 05:35:27 +0200 Subject: [PATCH 266/527] Improved guide --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c2ee8994..854ed4ba 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,26 @@ Because we made a notifier that takes your watching list, checks it against exte ## So it's just a notifier? -In the past it was, but we're growing bigger by establishing a database that combines information from multiple sources. We also have our own anime lists now due to popular requests of adding episode progress changes to our browser extension. +In the past it was, but we're growing bigger by establishing a database that combines information from multiple sources and also growing as a community. Many of us are hanging out on Discord and you are welcome to join us. We also have our own anime lists now due to popular request of adding episode progress changes to our browser extension. + +## How do I use the search? + +Press the "F" key while you're browsing the site and start searching for an anime title. + +## How do I add an anime to my list? + +Once you open the anime page you should see a button called "Add to my collection". Clicking that will add the anime to your "Plan to watch" list. To move it to your current "Watching" list, you need to click "Edit in collection" and change the status to "Watching". + +## How do I edit my episode progress? + +There are 2 ways of editing your progress: + +1. Click on the "+" button that shows up when you hover over the episode number. This will increase your progress by one episode on each click. +1. Click on the episode number so that a text input cursor shows up. Use backspace/delete keys and enter your new number directly. Press Enter or click somewhere else to confirm. + +## How do I edit my rating? + +Your "Overall" rating can be edited with the same method as episodes by clicking on the number directly so that the text input cursor shows up, then entering a new number and confirming with Enter. The other 3 rating numbers for Story, Visuals and Soundtrack can only be edited by going into edit mode (click on the anime title in your list). ## How does the rating system work? @@ -23,13 +42,36 @@ You can rate each entry in your anime list in 4 different categories: Each rating is a number on a scale of 0 to 10. A rating of 0 counts as "not rated" and will be ignored in average rating calculations for that anime. Thus the lowest possible rating you can assign to an anime is 0.1. The highest possible rating is 10. The average is close to the number 5. -## How do I use the search? +## What are the community rules for conversations on the forum? -Press the "F" key and start searching for anime. +* Be respectful to each other. +* Realize that every person has his or her own opinion and that you should treat that opinion with respect. You do not have to agree with strangers on the internet but it's worth thinking about their viewpoint. +* Do not spam. +* Do not advertise unrelated products. If anything it needs to be related to anime or the site itself. +* We absolutely do not mind links to competitors or similar websites. Feel free to post them. -## How do I add an anime to my list? +## How do I import my former anime list? -Once you open the anime page you should see a button called "Add to my collection". Clicking that will add the anime to your "Plan to watch" list. To move it to your current "Watching" list, you need to click "Edit in collection" and change the status to "Watching". +We added importers for what we consider to be the 3 most popular list providers: + +* anilist.co +* kitsu.io +* myanimelist.net + +To use an importer, enter your nickname for the site you want to import from and click the "Import" button with the list provider name that just appeared. + +## How do I install the site as an Android App? + +This website uses a very recent and modern technology that allows you to install websites as local apps. To install notify.moe as a mobile app, do the following: + +1. Go to https://notify.moe on your Android device. +2. Open the menu by tapping the top right part of your browser. +3. Choose "Add to Home screen" and confirm it. +4. Now you can access your anime list easily from your home screen and get notified about new episodes. + +## How do I install the site as a PC/Desktop App? + +In Chrome, open the top right menu and go to **More tools > Add to desktop**. ## How do notifications work from a technical perspective? @@ -42,12 +84,16 @@ From a technological standpoint we went through quite a few different approaches * Version 1.0: This version was just a browser extension with **client-side JS**. * Version 2.0: To decrease the number of requests/pressure on external sites we made a central website. It was written in **PHP**. * Version 3.0: A complete remake of the website in **node.js** supporting 4 different list providers and 2 anime providers. Episode changes were not possible. -* Version 4.0: We switched to our own hosted anime lists to make episode updates in the extension as smooth as possible. The website is now written in **Go**. +* Version 4.0: We switched to our own hosted anime lists to make episode updates in the extension as smooth as possible. The website is now written in **Go** and uses 3 separate servers/machines (web server, database and the scheduler). ## How many developers are working on this? Since 2014 it's been just me, though I do plan to start a company and hire talented people to help me out with this project once the stars align. +## Can I show my support for this site? Do you accept donations? + +I'm planning to add "pro accounts" for an extended feature set. You do not have to donate without getting something back, instead I'd rather be happy to see you profit from the donation as well. It would be my dream to work on this full-time. + ## Can I help with coding or change stuff as this is Open Source? Sure, the setup to start contributing is not that hard. Try to get in contact with me on Discord. From 98519b7b025c692351123a068a01fbeb8e38c815 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 05:44:29 +0200 Subject: [PATCH 267/527] Updated forum style --- styles/forum.scarlet | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 7ce5e863..ad97aec8 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -24,10 +24,10 @@ post-content-padding-y = 0.75rem position relative h1, h2, h3 - font-weight normal + font-weight bold text-align left line-height 1.5em - margin typography-margin 0 + margin 1rem 0 h1 font-size 1.5rem From 98c4b90e9f7a11c684bf78dc76d49b32d6610418 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 19:49:52 +0200 Subject: [PATCH 268/527] Avatar struct patch --- pages/profile/profile.pixy | 2 +- pages/profile/profile.scarlet | 2 +- pages/settings/settings.pixy | 14 +++++++++++ pages/settings/settings.scarlet | 5 +++- .../update-user-struct/update-user-struct.go | 24 +++++++++++++++++++ 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 patches/update-user-struct/update-user-struct.go diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 464cda20..641ead74 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -2,7 +2,7 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) .profile img.profile-cover(src=viewUser.CoverImageURL(), alt="Cover image") - .image-container.mountable.never-unmount + .profile-image-container.mountable.never-unmount ProfileImage(viewUser) .intro-container.mountable.never-unmount diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 29cec37b..74ff58a2 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -68,7 +68,7 @@ profile-boot-duration = 2s width 100% height auto -.image-container +.profile-image-container flex 1 max-width 280px max-height 280px diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 06c9ec58..bfd0e87c 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -106,6 +106,20 @@ component Settings(user *arn.User) a.button(href="https://www.youtube.com/watch?v=opyt4cw0ep8", target="_blank", rel="noopener") Icon("android") span Get the Android App + + .widget.mountable + h3.widget-title + Icon("picture-o") + span Avatar + + .widget-input + label(for="AvatarSource") Source: + select.widget-element.action(id="AvatarSource", data-field="AvatarSource", value="Gravatar", data-action="save", data-trigger="change") + option(value="Gravatar") Gravatar + + if "Gravatar" == "Gravatar" + .profile-image-container.avatar-preview + img.profile-image(src=user.Gravatar(), alt="Gravatar") //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet index a773b757..85f1c180 100644 --- a/pages/settings/settings.scarlet +++ b/pages/settings/settings.scarlet @@ -1,4 +1,7 @@ .widget-input button, .button - margin-bottom 1rem \ No newline at end of file + margin-bottom 1rem + +.avatar-preview + margin 0 auto \ No newline at end of file diff --git a/patches/update-user-struct/update-user-struct.go b/patches/update-user-struct/update-user-struct.go new file mode 100644 index 00000000..2045746b --- /dev/null +++ b/patches/update-user-struct/update-user-struct.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" + "github.com/jinzhu/copier" +) + +func main() { + color.Yellow("Updating user struct") + + // Iterate over the stream + for user := range arn.MustStreamUsers() { + newUser := &arn.UserNew{} + + copier.Copy(newUser, user) + newUser.Avatar.Extension = user.Avatar + + // Save in DB + arn.PanicOnError(arn.DB.Set("User", user.ID, newUser)) + } + + color.Green("Finished.") +} From e014a5f6284298b3f1d3e6fb05efc4a7e9893a7c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 19:56:26 +0200 Subject: [PATCH 269/527] Updated struct --- jobs/active-users/active-users.go | 2 +- jobs/avatars/AvatarOriginalFileOutput.go | 2 +- jobs/avatars/avatars.go | 8 ++++---- .../delete-invalid-avatars.go | 4 ++-- .../update-user-struct/update-user-struct.go | 18 ++++++++---------- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/jobs/active-users/active-users.go b/jobs/active-users/active-users.go index ae99e4b4..b804ec03 100644 --- a/jobs/active-users/active-users.go +++ b/jobs/active-users/active-users.go @@ -15,7 +15,7 @@ func main() { // Filter out active users with an avatar users, err := arn.FilterUsers(func(user *arn.User) bool { - return user.IsActive() && user.AvatarExtension != "" + return user.IsActive() && user.Avatar.Extension != "" }) if err != nil { diff --git a/jobs/avatars/AvatarOriginalFileOutput.go b/jobs/avatars/AvatarOriginalFileOutput.go index 5232ef6b..9d173d5b 100644 --- a/jobs/avatars/AvatarOriginalFileOutput.go +++ b/jobs/avatars/AvatarOriginalFileOutput.go @@ -52,7 +52,7 @@ func (output *AvatarOriginalFileOutput) SaveAvatar(avatar *Avatar) error { } // Set user avatar - avatar.User.AvatarExtension = extension + avatar.User.Avatar.Extension = extension // Write to file fileName := output.Directory + avatar.User.ID + extension diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index 3a6a5bd5..dd346004 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -123,7 +123,7 @@ func StartWorkers(queue chan *arn.User, work func(*arn.User)) { // Work handles a single user. func Work(user *arn.User) { - user.AvatarExtension = "" + user.Avatar.Extension = "" for _, source := range avatarSources { avatar := source.GetAvatar(user) @@ -141,7 +141,7 @@ func Work(user *arn.User) { // Avoid quality loss (if it's on the file system, we don't need to write it again) if sourceType == "FileSystem" { - user.AvatarExtension = avatar.Extension() + user.Avatar.Extension = avatar.Extension() break } @@ -157,7 +157,7 @@ func Work(user *arn.User) { } // Since this a very long running job, refresh user data before saving it. - avatarExt := user.AvatarExtension + avatarExt := user.Avatar.Extension user, err := arn.GetUser(user.ID) if err != nil { @@ -166,6 +166,6 @@ func Work(user *arn.User) { } // Save avatar data - user.AvatarExtension = avatarExt + user.Avatar.Extension = avatarExt user.Save() } diff --git a/patches/delete-invalid-avatars/delete-invalid-avatars.go b/patches/delete-invalid-avatars/delete-invalid-avatars.go index 58360437..18e7016c 100644 --- a/patches/delete-invalid-avatars/delete-invalid-avatars.go +++ b/patches/delete-invalid-avatars/delete-invalid-avatars.go @@ -8,8 +8,8 @@ import ( func main() { for user := range arn.MustStreamUsers() { - if !strings.HasPrefix(user.AvatarExtension, ".") { - user.AvatarExtension = "" + if !strings.HasPrefix(user.Avatar.Extension, ".") { + user.Avatar.Extension = "" } user.Save() diff --git a/patches/update-user-struct/update-user-struct.go b/patches/update-user-struct/update-user-struct.go index 2045746b..512cd1ee 100644 --- a/patches/update-user-struct/update-user-struct.go +++ b/patches/update-user-struct/update-user-struct.go @@ -1,24 +1,22 @@ package main import ( - "github.com/animenotifier/arn" "github.com/fatih/color" - "github.com/jinzhu/copier" ) func main() { color.Yellow("Updating user struct") - // Iterate over the stream - for user := range arn.MustStreamUsers() { - newUser := &arn.UserNew{} + // // Iterate over the stream + // for user := range arn.MustStreamUsers() { + // newUser := &arn.UserNew{} - copier.Copy(newUser, user) - newUser.Avatar.Extension = user.Avatar + // copier.Copy(newUser, user) + // newUser.Avatar.Extension = user.Avatar - // Save in DB - arn.PanicOnError(arn.DB.Set("User", user.ID, newUser)) - } + // // Save in DB + // arn.PanicOnError(arn.DB.Set("User", user.ID, newUser)) + // } color.Green("Finished.") } From 360f9f9ce21bead02894f14f97057f279b613d35 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 20:03:07 +0200 Subject: [PATCH 270/527] New avatars job --- jobs/avatars/avatars.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index dd346004..70eeb352 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -134,13 +134,13 @@ func Work(user *arn.User) { } // Name of source - sourceType := reflect.TypeOf(source).Elem().Name() + user.Avatar.Source = reflect.TypeOf(source).Elem().Name() // Log - fmt.Println(color.GreenString("✔"), sourceType, "|", user.Nick, "|", avatar) + fmt.Println(color.GreenString("✔"), user.Avatar.Source, "|", user.Nick, "|", avatar) - // Avoid quality loss (if it's on the file system, we don't need to write it again) - if sourceType == "FileSystem" { + // Avoid JPG quality loss (if it's on the file system, we don't need to write it again) + if user.Avatar.Source == "FileSystem" { user.Avatar.Extension = avatar.Extension() break } @@ -158,6 +158,7 @@ func Work(user *arn.User) { // Since this a very long running job, refresh user data before saving it. avatarExt := user.Avatar.Extension + avatarSrc := user.Avatar.Source user, err := arn.GetUser(user.ID) if err != nil { @@ -167,5 +168,6 @@ func Work(user *arn.User) { // Save avatar data user.Avatar.Extension = avatarExt + user.Avatar.Source = avatarSrc user.Save() } From 808a29756d51ba6fb13af0a0c12729fbbbd39105 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 22:43:26 +0200 Subject: [PATCH 271/527] Bots in separate directory now --- {jobs => bots}/discord/discord.go | 0 pages/dashboard/dashboard.go | 2 +- pages/profile/stats.go | 2 +- pages/settings/settings.pixy | 7 ++++--- 4 files changed, 6 insertions(+), 5 deletions(-) rename {jobs => bots}/discord/discord.go (100%) diff --git a/jobs/discord/discord.go b/bots/discord/discord.go similarity index 100% rename from jobs/discord/discord.go rename to bots/discord/discord.go diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 61d06347..825473b8 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -40,7 +40,7 @@ func dashboard(ctx *aero.Context) string { flow.Parallel(func() { forumActivity, _ = arn.GetForumActivityCached() }, func() { - animeList, err := arn.GetAnimeList(user) + animeList, err := arn.GetAnimeList(user.ID) if err != nil { return diff --git a/pages/profile/stats.go b/pages/profile/stats.go index 0a28bbef..722db07b 100644 --- a/pages/profile/stats.go +++ b/pages/profile/stats.go @@ -27,7 +27,7 @@ func GetStatsByUser(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "User not found", err) } - animeList, err := arn.GetAnimeList(viewUser) + animeList, err := arn.GetAnimeList(viewUser.ID) animeList.PrefetchAnime() if err != nil { diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index bfd0e87c..7b982b2f 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -112,9 +112,10 @@ component Settings(user *arn.User) Icon("picture-o") span Avatar - .widget-input - label(for="AvatarSource") Source: - select.widget-element.action(id="AvatarSource", data-field="AvatarSource", value="Gravatar", data-action="save", data-trigger="change") + .widget-input(data-api="/api/user/" + user.ID) + label(for="Avatar.Source") Source: + select.widget-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Avatar.Source, data-action="save", data-trigger="change") + option(value="") Automatic option(value="Gravatar") Gravatar if "Gravatar" == "Gravatar" From 7add32b4a7a119ef9fc68046c906ee33be26c170 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 22:50:14 +0200 Subject: [PATCH 272/527] Updated makefiles --- bots/build.sh | 4 ++++ jobs/build.sh | 1 + makefile | 7 +++++-- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100755 bots/build.sh diff --git a/bots/build.sh b/bots/build.sh new file mode 100755 index 00000000..bf4b428d --- /dev/null +++ b/bots/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh +MYDIR="$(dirname "$(realpath "$0")")" +cd "$MYDIR" +for dir in ./*; do ([ -d "$dir" ] && cd "$dir" && echo "Building bots/$dir" && go build); done \ No newline at end of file diff --git a/jobs/build.sh b/jobs/build.sh index 2839f06f..48a4e138 100755 --- a/jobs/build.sh +++ b/jobs/build.sh @@ -1,4 +1,5 @@ #!/bin/sh MYDIR="$(dirname "$(realpath "$0")")" cd "$MYDIR" +go build for dir in ./*; do ([ -d "$dir" ] && cd "$dir" && echo "Building jobs/$dir" && go build); done \ No newline at end of file diff --git a/makefile b/makefile index d8ca17b9..9c2a429c 100644 --- a/makefile +++ b/makefile @@ -6,6 +6,7 @@ GOINSTALL=$(GOCMD) install GOTEST=$(GOCMD) test BUILDJOBS=@./jobs/build.sh BUILDPATCHES=@./patches/build.sh +BUILDBOTS=@./bots/build.sh TSCMD=@tsc IPTABLES=@sudo iptables @@ -13,6 +14,8 @@ server: $(GOBUILD) jobs: $(BUILDJOBS) +bots: + $(BUILDBOTS) patches: $(BUILDPATCHES) js: @@ -42,6 +45,6 @@ clean: ports: $(IPTABLES) -t nat -A OUTPUT -o lo -p tcp --dport 80 -j REDIRECT --to-port 4000 $(IPTABLES) -t nat -A OUTPUT -o lo -p tcp --dport 443 -j REDIRECT --to-port 4001 -all: assets server jobs patches +all: assets server bots jobs patches -.PHONY: jobs patches ports +.PHONY: bots jobs patches ports From c205195a7cec2b7f7bd8122c75a6eb0ba4366d55 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 23:49:11 +0200 Subject: [PATCH 273/527] Improved airing anime --- jobs/airing-anime/airing-anime.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/jobs/airing-anime/airing-anime.go b/jobs/airing-anime/airing-anime.go index d1dec2ee..09de8914 100644 --- a/jobs/airing-anime/airing-anime.go +++ b/jobs/airing-anime/airing-anime.go @@ -7,6 +7,8 @@ import ( "github.com/fatih/color" ) +const currentlyAiringBonus = 4.0 + func main() { color.Yellow("Caching airing anime") @@ -19,7 +21,18 @@ func main() { } sort.Slice(animeList, func(i, j int) bool { - return animeList[i].Rating.Overall > animeList[j].Rating.Overall + scoreA := animeList[i].Rating.Overall + scoreB := animeList[j].Rating.Overall + + if animeList[i].Status == "current" { + scoreA += currentlyAiringBonus + } + + if animeList[j].Status == "current" { + scoreB += currentlyAiringBonus + } + + return scoreA > scoreB }) // Convert to small anime list From 78468e4f60d8151272582d29346ba2c201854f6a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 17 Jul 2017 23:56:05 +0200 Subject: [PATCH 274/527] Updated README --- README.md | 73 +++++++++++++++++++++++++++++++---- jobs/statistics/statistics.go | 10 ++++- pages/settings/settings.pixy | 6 +-- 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 854ed4ba..bb3c0450 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,44 @@ An anime tracker where you can add anime to your list and edit your episode prog ## Why is it called notify.moe? -Because we made a notifier that takes your watching list, checks it against external websites (currently twist.moe) and notifies you when there is a new episode on that external site. +Because we made a notifier that takes your watching list, checks it against external websites and notifies you when there is a new episode on that external site. It's also a terrible wordplay combining "notify me!" and [moe](https://en.wikipedia.org/wiki/Moe_(slang)). ## So it's just a notifier? In the past it was, but we're growing bigger by establishing a database that combines information from multiple sources and also growing as a community. Many of us are hanging out on Discord and you are welcome to join us. We also have our own anime lists now due to popular request of adding episode progress changes to our browser extension. +## What does the current feature set look like? + +* [Chrome extension](https://chrome.google.com/webstore/detail/anime-notifier/hajchfikckiofgilinkpifobdbiajfch) for quick watching list access and episode updates +* Edit episode progress and rating by clicking on the number +* Airing dates +* Offline browsing +* Push notifications +* Soundtracks +* Anime & user search +* Anime rating system +* [twist.moe](https://twist.moe) integration +* [anilist.co](https://anilist.co/), [myanimelist.net](https://myanimelist.net/) and [kitsu.io](https://kitsu.io/) import +* [osu](https://osu.ppy.sh/) ranking view +* [Gravatar](https://gravatar.com) support +* User profiles +* Dashboard +* Forums +* Responsive layout (looks good on 1080p and on mobile devices) + +## How do I enable notifications? + +Use a browser that supports push notifications (Chrome or Firefox). Then go to your [settings](/settings) and click "Enable notifications". This might take a while, especially on mobile devices. After that you can press "Send test notification". If you get a notification saying "Yay, it works!" then everything's fine. The real thing looks like this: + +![Anime Notifications](https://puu.sh/wKpcm/304a4441a0.png) + ## How do I use the search? -Press the "F" key while you're browsing the site and start searching for an anime title. +Press the "F" key and start searching for an anime title. -## How do I add an anime to my list? +![Anime search](https://puu.sh/wM45s/ffe5025c63.png) + +## How do I add anime to my list? Once you open the anime page you should see a button called "Add to my collection". Clicking that will add the anime to your "Plan to watch" list. To move it to your current "Watching" list, you need to click "Edit in collection" and change the status to "Watching". @@ -42,13 +69,23 @@ You can rate each entry in your anime list in 4 different categories: Each rating is a number on a scale of 0 to 10. A rating of 0 counts as "not rated" and will be ignored in average rating calculations for that anime. Thus the lowest possible rating you can assign to an anime is 0.1. The highest possible rating is 10. The average is close to the number 5. +## What does the Chrome extension offer me? + +A quick access to your watching list: + +![Anime Notifier Chrome extension](https://puu.sh/wM47V/af25b23755.png) + +## How can I format text and include images in the forum? + +You need to use [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). + ## What are the community rules for conversations on the forum? * Be respectful to each other. * Realize that every person has his or her own opinion and that you should treat that opinion with respect. You do not have to agree with strangers on the internet but it's worth thinking about their viewpoint. * Do not spam. * Do not advertise unrelated products. If anything it needs to be related to anime or the site itself. -* We absolutely do not mind links to competitors or similar websites. Feel free to post them. +* We do not mind links to competitors or similar websites. Feel free to post them. ## How do I import my former anime list? @@ -60,23 +97,45 @@ We added importers for what we consider to be the 3 most popular list providers: To use an importer, enter your nickname for the site you want to import from and click the "Import" button with the list provider name that just appeared. -## How do I install the site as an Android App? +![Anime list import](https://puu.sh/wM4dP/11d43e5f71.png) -This website uses a very recent and modern technology that allows you to install websites as local apps. To install notify.moe as a mobile app, do the following: +## How do I install the site as an Android app? + +This website uses a modern technology that allows you to install websites as local apps. To install notify.moe as a mobile app, do the following: 1. Go to https://notify.moe on your Android device. 2. Open the menu by tapping the top right part of your browser. 3. Choose "Add to Home screen" and confirm it. 4. Now you can access your anime list easily from your home screen and get notified about new episodes. -## How do I install the site as a PC/Desktop App? +You need to enable notifications on each device separately. To receive notifications on both desktop and mobile phone you need to click "Enable notifications" on both. + +## How do I install the site as a PC/desktop app? In Chrome, open the top right menu and go to **More tools > Add to desktop**. +![Anime Notifier desktop app](https://puu.sh/wM4pB/542add3113.png) + +## What do I get notified about? + +At the time of writing this, you get notified when: + +* A new episode from your watching list is released on twist.moe +* Somebody replies in a thread you have participated in +* Somebody likes your post + ## How do notifications work from a technical perspective? There are many, many ways how notifications can be implemented from a technical standpoint. There is e.g. "polling" which means that an app periodically checks external sites and tells you when something new is available. We are not using polling because these periodic checks can quickly drain your battery on a mobile phone. We are using so-called "push notifications" instead. The advantage of push notifications is that your mobile phone or desktop PC doesn't have to do periodic checks anymore - instead the website will send new episode releases to all of your registered devices. This consumes less CPU/network resources and is much more battery friendly for mobile devices. +## Is this website well-optimized for performance? + +You are free to [judge it yourself](https://twitter.com/eduardurbach/status/885631801171091460). + +![Anime Notifier - Lighthouse](https://pbs.twimg.com/media/DEplUsNXgAEF-UT.jpg:large) + +![Anime Notifier - PageSpeed](https://pbs.twimg.com/media/DEplXmpWsAAPzb6.jpg:large) + ## Can you tell me more about the history of this software? From a technological standpoint we went through quite a few different approaches: diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index 6ed1fdf1..53645ed0 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -43,6 +43,7 @@ func getUserStats() []*arn.PieChart { os := stats{} notifications := stats{} activity := stats{} + avatar := stats{} for _, info := range analytics { pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ @@ -52,7 +53,7 @@ func getUserStats() []*arn.PieChart { } for user := range arn.MustStreamUsers() { - if user.Gender != "" { + if user.Gender != "" && user.Gender != "other" { gender[user.Gender]++ } @@ -83,6 +84,12 @@ func getUserStats() []*arn.PieChart { } else { activity["Inactive"]++ } + + if user.Avatar.Source == "" { + avatar["none"]++ + } else { + avatar[user.Avatar.Source]++ + } } println("Finished user statistics") @@ -92,6 +99,7 @@ func getUserStats() []*arn.PieChart { arn.NewPieChart("Screen size", screenSize), arn.NewPieChart("Browser", browser), arn.NewPieChart("Country", country), + arn.NewPieChart("Avatar", avatar), arn.NewPieChart("Activity", activity), arn.NewPieChart("Notifications", notifications), arn.NewPieChart("Gender", gender), diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 7b982b2f..59a8c0a3 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -112,10 +112,10 @@ component Settings(user *arn.User) Icon("picture-o") span Avatar - .widget-input(data-api="/api/user/" + user.ID) + .widget-input(data-api="/api/settings/" + user.ID) label(for="Avatar.Source") Source: - select.widget-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Avatar.Source, data-action="save", data-trigger="change") - option(value="") Automatic + select.widget-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change") + option(value="") Automatic option(value="Gravatar") Gravatar if "Gravatar" == "Gravatar" From 1923a1a88961ef276ba5c4434ed042a94aea7b46 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Jul 2017 00:04:43 +0200 Subject: [PATCH 275/527] Fixed forum notifications --- mixins/AnimeGrid.pixy | 2 +- mixins/Avatar.pixy | 2 +- mixins/Character.pixy | 2 +- mixins/ProfileImage.pixy | 2 +- mixins/SoundTrack.pixy | 4 ++-- pages/profile/profile.pixy | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mixins/AnimeGrid.pixy b/mixins/AnimeGrid.pixy index 71f62dd6..43537328 100644 --- a/mixins/AnimeGrid.pixy +++ b/mixins/AnimeGrid.pixy @@ -2,4 +2,4 @@ component AnimeGrid(animeList []*arn.Anime) .anime-grid each anime in animeList a.anime-grid-cell.ajax(href="/anime/" + toString(anime.ID)) - img.anime-grid-image.lazy(src="", data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")") \ No newline at end of file + img.anime-grid-image.lazy(data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")") \ No newline at end of file diff --git a/mixins/Avatar.pixy b/mixins/Avatar.pixy index 526ef8e1..74bfd4d4 100644 --- a/mixins/Avatar.pixy +++ b/mixins/Avatar.pixy @@ -4,7 +4,7 @@ component Avatar(user *arn.User) component AvatarNoLink(user *arn.User) if user.HasAvatar() - img.user-image.lazy(src="", data-src=user.SmallAvatar(), data-webp="true", alt=user.Nick) + img.user-image.lazy(data-src=user.SmallAvatar(), data-webp="true", alt=user.Nick) else SVGAvatar diff --git a/mixins/Character.pixy b/mixins/Character.pixy index 9e54317b..42b274e7 100644 --- a/mixins/Character.pixy +++ b/mixins/Character.pixy @@ -1,4 +1,4 @@ component Character(character *arn.Character) a.character.ajax(href="/character/" + character.ID) - img.character-image.lazy(src="", data-src=character.Image, alt=character.Name, title=character.Name) + img.character-image.lazy(data-src=character.Image, alt=character.Name, title=character.Name) //- span.character-name= character.Name \ No newline at end of file diff --git a/mixins/ProfileImage.pixy b/mixins/ProfileImage.pixy index 958d2bce..9b7a7735 100644 --- a/mixins/ProfileImage.pixy +++ b/mixins/ProfileImage.pixy @@ -1,6 +1,6 @@ component ProfileImage(user *arn.User) if user.HasAvatar() - img.profile-image.lazy(src="", data-src=user.LargeAvatar(), data-webp="true", alt="Profile image") + img.profile-image.lazy(data-src=user.LargeAvatar(), data-webp="true", alt="Profile image") else svg.profile-image(viewBox="0 0 50 50", alt="Profile image") circle.head(cx="25", cy="19", r="10") diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index 970180fc..41931775 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -9,9 +9,9 @@ component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia) .sound-track.mountable(id=track.ID) .sound-track-content a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) - img.sound-track-anime-image.lazy(src="", data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) + img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - iframe.lazy(src="", data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") + iframe.lazy(data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") .sound-track-footer a.ajax(href=track.Link()) Icon("music") diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 641ead74..e06736f5 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -108,7 +108,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, each item in animeList.Items if item.Status == arn.AnimeListStatusWatching || item.Status == arn.AnimeListStatusCompleted a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") - img.profile-watching-list-item-image.lazy(src="", data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + img.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) //- .profile-category.mountable //- h3 From 1706ef9bc48bfc7c07896d5eae00b93bc574de84 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Jul 2017 03:55:47 +0200 Subject: [PATCH 276/527] Improved avatars background job --- bots/avatars/avatars.go | 23 +++++++ jobs/avatars/Avatar.go | 87 ------------------------ jobs/avatars/AvatarOriginalFileOutput.go | 60 ---------------- jobs/avatars/AvatarSource.go | 10 --- jobs/avatars/AvatarWebPFileOutput.go | 27 -------- jobs/avatars/AvatarWriter.go | 6 -- jobs/avatars/FileSystem.go | 48 ------------- jobs/avatars/Gravatar.go | 37 ---------- jobs/avatars/MyAnimeList.go | 60 ---------------- jobs/avatars/avatars.go | 36 ++++++---- jobs/jobs.go | 2 +- pages/settings/settings.pixy | 11 ++- 12 files changed, 56 insertions(+), 351 deletions(-) create mode 100644 bots/avatars/avatars.go delete mode 100644 jobs/avatars/Avatar.go delete mode 100644 jobs/avatars/AvatarOriginalFileOutput.go delete mode 100644 jobs/avatars/AvatarSource.go delete mode 100644 jobs/avatars/AvatarWebPFileOutput.go delete mode 100644 jobs/avatars/AvatarWriter.go delete mode 100644 jobs/avatars/FileSystem.go delete mode 100644 jobs/avatars/Gravatar.go delete mode 100644 jobs/avatars/MyAnimeList.go diff --git a/bots/avatars/avatars.go b/bots/avatars/avatars.go new file mode 100644 index 00000000..340308d0 --- /dev/null +++ b/bots/avatars/avatars.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "log" + "net/http" +) + +func refreshAvatar(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("ok")) +} + +var port = "8001" + +func init() { + flag.StringVar(&port, "port", "", "Port the HTTP server should listen on") + flag.Parse() +} + +func main() { + http.HandleFunc("/", refreshAvatar) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} diff --git a/jobs/avatars/Avatar.go b/jobs/avatars/Avatar.go deleted file mode 100644 index f1748c37..00000000 --- a/jobs/avatars/Avatar.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "image" - "net/http" - "strings" - "time" - - "github.com/animenotifier/arn" - "github.com/parnurzeal/gorequest" -) - -var netLog = avatarLog.NewChannel("NET") - -// Avatar represents a single image and the name of the format. -type Avatar struct { - User *arn.User - Image image.Image - Data []byte - Format string -} - -// Extension ... -func (avatar *Avatar) Extension() string { - switch avatar.Format { - case "jpg", "jpeg": - return ".jpg" - case "png": - return ".png" - case "gif": - return ".gif" - default: - return "" - } -} - -// String returns a text representation of the format, width and height. -func (avatar *Avatar) String() string { - return fmt.Sprint(avatar.Format, " | ", avatar.Image.Bounds().Dx(), "x", avatar.Image.Bounds().Dy()) -} - -// AvatarFromURL downloads and decodes the image from an URL and creates an Avatar. -func AvatarFromURL(url string, user *arn.User) *Avatar { - // Download - response, data, networkErrs := gorequest.New().Get(url).EndBytes() - - // Network errors - if len(networkErrs) > 0 { - netLog.Error(user.Nick, url, networkErrs[0]) - return nil - } - - // Retry HTTP only version after 5 seconds if service unavailable - if response == nil || response.StatusCode == http.StatusServiceUnavailable { - time.Sleep(5 * time.Second) - response, data, networkErrs = gorequest.New().Get(strings.Replace(url, "https://", "http://", 1)).EndBytes() - } - - // Network errors on 2nd try - if len(networkErrs) > 0 { - netLog.Error(user.Nick, url, networkErrs[0]) - return nil - } - - // Bad status codes - if response.StatusCode != http.StatusOK { - netLog.Error(user.Nick, url, response.StatusCode) - return nil - } - - // Decode - img, format, decodeErr := image.Decode(bytes.NewReader(data)) - - if decodeErr != nil { - netLog.Error(user.Nick, url, decodeErr) - return nil - } - - return &Avatar{ - User: user, - Image: img, - Data: data, - Format: format, - } -} diff --git a/jobs/avatars/AvatarOriginalFileOutput.go b/jobs/avatars/AvatarOriginalFileOutput.go deleted file mode 100644 index 9d173d5b..00000000 --- a/jobs/avatars/AvatarOriginalFileOutput.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "image/gif" - "image/jpeg" - "image/png" - "io/ioutil" - - "github.com/nfnt/resize" -) - -// AvatarOriginalFileOutput ... -type AvatarOriginalFileOutput struct { - Directory string - Size int -} - -// SaveAvatar writes the original avatar to the file system. -func (output *AvatarOriginalFileOutput) SaveAvatar(avatar *Avatar) error { - // Determine file extension - extension := avatar.Extension() - - if extension == "" { - return errors.New("Unknown format: " + avatar.Format) - } - - // Resize if needed - data := avatar.Data - img := avatar.Image - - if img.Bounds().Dx() > output.Size { - img = resize.Resize(uint(output.Size), 0, img, resize.Lanczos3) - buffer := new(bytes.Buffer) - - var err error - switch extension { - case ".jpg": - err = jpeg.Encode(buffer, img, nil) - case ".png": - err = png.Encode(buffer, img) - case ".gif": - err = gif.Encode(buffer, img, nil) - } - - if err != nil { - return err - } - - data = buffer.Bytes() - } - - // Set user avatar - avatar.User.Avatar.Extension = extension - - // Write to file - fileName := output.Directory + avatar.User.ID + extension - return ioutil.WriteFile(fileName, data, 0644) -} diff --git a/jobs/avatars/AvatarSource.go b/jobs/avatars/AvatarSource.go deleted file mode 100644 index 5d7c2253..00000000 --- a/jobs/avatars/AvatarSource.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" -) - -// AvatarSource describes a source where we can find avatar images for a user. -type AvatarSource interface { - GetAvatar(*arn.User) *Avatar -} diff --git a/jobs/avatars/AvatarWebPFileOutput.go b/jobs/avatars/AvatarWebPFileOutput.go deleted file mode 100644 index a3aaf150..00000000 --- a/jobs/avatars/AvatarWebPFileOutput.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" - "github.com/nfnt/resize" -) - -// AvatarWebPFileOutput ... -type AvatarWebPFileOutput struct { - Directory string - Size int - Quality float32 -} - -// SaveAvatar writes the avatar in WebP format to the file system. -func (output *AvatarWebPFileOutput) SaveAvatar(avatar *Avatar) error { - img := avatar.Image - - // Resize if needed - if img.Bounds().Dx() > output.Size { - img = resize.Resize(uint(output.Size), 0, img, resize.Lanczos3) - } - - // Write to file - fileName := output.Directory + avatar.User.ID + ".webp" - return arn.SaveWebP(img, fileName, output.Quality) -} diff --git a/jobs/avatars/AvatarWriter.go b/jobs/avatars/AvatarWriter.go deleted file mode 100644 index eef1f5fc..00000000 --- a/jobs/avatars/AvatarWriter.go +++ /dev/null @@ -1,6 +0,0 @@ -package main - -// AvatarOutput represents a system that saves an avatar locally (in database or as a file, e.g.) -type AvatarOutput interface { - SaveAvatar(*Avatar) error -} diff --git a/jobs/avatars/FileSystem.go b/jobs/avatars/FileSystem.go deleted file mode 100644 index b97b1363..00000000 --- a/jobs/avatars/FileSystem.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "bytes" - "image" - "io/ioutil" - - "github.com/animenotifier/arn" -) - -var fileSystemLog = avatarLog.NewChannel("SSD") - -// FileSystem loads avatar from the local filesystem. -type FileSystem struct { - Directory string -} - -// GetAvatar returns the local image for the user. -func (source *FileSystem) GetAvatar(user *arn.User) *Avatar { - fullPath := arn.FindFileWithExtension(user.ID, source.Directory, arn.OriginalImageExtensions) - - if fullPath == "" { - fileSystemLog.Error(user.Nick, "Not found on file system") - return nil - } - - data, err := ioutil.ReadFile(fullPath) - - if err != nil { - fileSystemLog.Error(user.Nick, err) - return nil - } - - // Decode - img, format, decodeErr := image.Decode(bytes.NewReader(data)) - - if decodeErr != nil { - fileSystemLog.Error(user.Nick, decodeErr) - return nil - } - - return &Avatar{ - User: user, - Image: img, - Data: data, - Format: format, - } -} diff --git a/jobs/avatars/Gravatar.go b/jobs/avatars/Gravatar.go deleted file mode 100644 index 731d863f..00000000 --- a/jobs/avatars/Gravatar.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" - - "github.com/animenotifier/arn" - gravatar "github.com/ungerik/go-gravatar" -) - -var gravatarLog = avatarLog.NewChannel("GRA") - -// Gravatar - https://gravatar.com/ -type Gravatar struct { - Rating string - RequestLimiter *time.Ticker -} - -// GetAvatar returns the Gravatar image for a user (if available). -func (source *Gravatar) GetAvatar(user *arn.User) *Avatar { - // If the user has no Email registered we can't get a Gravatar. - if user.Email == "" { - gravatarLog.Error(user.Nick, "No Email") - return nil - } - - // Build URL - gravatarURL := gravatar.Url(user.Email) + "?s=" + fmt.Sprint(arn.AvatarMaxSize) + "&d=404&r=" + source.Rating - gravatarURL = strings.Replace(gravatarURL, "http://", "https://", 1) - - // Wait for request limiter to allow us to send a request - <-source.RequestLimiter.C - - // Download - return AvatarFromURL(gravatarURL, user) -} diff --git a/jobs/avatars/MyAnimeList.go b/jobs/avatars/MyAnimeList.go deleted file mode 100644 index e1a30b52..00000000 --- a/jobs/avatars/MyAnimeList.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "net/http" - "regexp" - "time" - - "github.com/animenotifier/arn" - "github.com/parnurzeal/gorequest" -) - -var userIDRegex = regexp.MustCompile(`(\d+)<\/user_id>`) -var malLog = avatarLog.NewChannel("MAL") - -// MyAnimeList - https://myanimelist.net/ -type MyAnimeList struct { - RequestLimiter *time.Ticker -} - -// GetAvatar returns the Gravatar image for a user (if available). -func (source *MyAnimeList) GetAvatar(user *arn.User) *Avatar { - malNick := user.Accounts.MyAnimeList.Nick - - // If the user has no username we can't get an avatar. - if malNick == "" { - malLog.Error(user.Nick, "No MAL nick") - return nil - } - - // Download user info - userInfoURL := "https://myanimelist.net/malappinfo.php?u=" + malNick - response, xml, networkErr := gorequest.New().Get(userInfoURL).End() - - if networkErr != nil { - malLog.Error(user.Nick, userInfoURL, networkErr) - return nil - } - - if response.StatusCode != http.StatusOK { - malLog.Error(user.Nick, userInfoURL, response.StatusCode) - return nil - } - - // Build URL - matches := userIDRegex.FindStringSubmatch(xml) - - if matches == nil || len(matches) < 2 { - malLog.Error(user.Nick, "Could not find user ID") - return nil - } - - malID := matches[1] - malAvatarURL := "https://myanimelist.cdn-dena.com/images/userimages/" + malID + ".jpg" - - // Wait for request limiter to allow us to send a request - <-source.RequestLimiter.C - - // Download - return AvatarFromURL(malAvatarURL, user) -} diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index 70eeb352..ac1757f1 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -15,6 +15,9 @@ import ( "github.com/aerogo/log" "github.com/animenotifier/arn" + "github.com/animenotifier/avatar" + "github.com/animenotifier/avatar/outputs" + "github.com/animenotifier/avatar/sources" "github.com/fatih/color" ) @@ -22,8 +25,8 @@ const ( webPQuality = 80 ) -var avatarSources []AvatarSource -var avatarOutputs []AvatarOutput +var avatarSources []avatar.Source +var avatarOutputs []avatar.Output var avatarLog = log.New() var wg sync.WaitGroup @@ -46,42 +49,42 @@ func main() { defer avatarLog.Flush() // Define the avatar sources - avatarSources = []AvatarSource{ - &Gravatar{ + avatarSources = []avatar.Source{ + &sources.Gravatar{ Rating: "pg", + RequestLimiter: time.NewTicker(100 * time.Millisecond), + }, + &sources.MyAnimeList{ RequestLimiter: time.NewTicker(250 * time.Millisecond), }, - &MyAnimeList{ - RequestLimiter: time.NewTicker(250 * time.Millisecond), - }, - &FileSystem{ + &sources.FileSystem{ Directory: "images/avatars/large/", }, } // Define the avatar outputs - avatarOutputs = []AvatarOutput{ + avatarOutputs = []avatar.Output{ // Original - Large - &AvatarOriginalFileOutput{ + &outputs.OriginalFile{ Directory: "images/avatars/large/", Size: arn.AvatarMaxSize, }, // Original - Small - &AvatarOriginalFileOutput{ + &outputs.OriginalFile{ Directory: "images/avatars/small/", Size: arn.AvatarSmallSize, }, // WebP - Large - &AvatarWebPFileOutput{ + &outputs.WebPFile{ Directory: "images/avatars/large/", Size: arn.AvatarMaxSize, Quality: webPQuality, }, // WebP - Small - &AvatarWebPFileOutput{ + &outputs.WebPFile{ Directory: "images/avatars/small/", Size: arn.AvatarSmallSize, Quality: webPQuality, @@ -126,7 +129,12 @@ func Work(user *arn.User) { user.Avatar.Extension = "" for _, source := range avatarSources { - avatar := source.GetAvatar(user) + avatar, err := source.GetAvatar(user) + + if err != nil { + avatarLog.Error(err) + continue + } if avatar == nil { // fmt.Println(color.RedString("✘"), reflect.TypeOf(source).Elem().Name(), user.Nick) diff --git a/jobs/jobs.go b/jobs/jobs.go index 2c635708..276f5f7c 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -23,8 +23,8 @@ var colorPool = []*color.Color{ } var jobs = map[string]time.Duration{ - "active-users": 1 * time.Minute, "forum-activity": 1 * time.Minute, + "active-users": 5 * time.Minute, "anime-ratings": 10 * time.Minute, "airing-anime": 10 * time.Minute, "statistics": 15 * time.Minute, diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 59a8c0a3..6c89d07f 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -117,10 +117,19 @@ component Settings(user *arn.User) select.widget-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change") option(value="") Automatic option(value="Gravatar") Gravatar + option(value="URL") Link + option(value="FileSystem") Upload - if "Gravatar" == "Gravatar" + if user.Settings().Avatar.Source == "Gravatar" || (user.Settings().Avatar.Source == "" && user.Avatar.Source == "Gravatar") .profile-image-container.avatar-preview img.profile-image(src=user.Gravatar(), alt="Gravatar") + + if user.Settings().Avatar.Source == "URL" + InputText("Avatar.SourceURL", user.Settings().Avatar.SourceURL, "Link", "Post the link to the image here") + + if user.Settings().Avatar.SourceURL != "" + .profile-image-container.avatar-preview + img.profile-image(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title From 5d0e7911d80eb81a28d403c29e0c3e0f17cad754 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Jul 2017 07:23:48 +0200 Subject: [PATCH 277/527] Updated avatar refresh --- bots/avatars/avatars.go | 54 ++++++++++++++-- jobs/avatars/avatars.go | 119 ++--------------------------------- jobs/avatars/shell.go | 5 +- pages/settings/settings.pixy | 6 +- 4 files changed, 59 insertions(+), 125 deletions(-) diff --git a/bots/avatars/avatars.go b/bots/avatars/avatars.go index 340308d0..8f27146a 100644 --- a/bots/avatars/avatars.go +++ b/bots/avatars/avatars.go @@ -1,14 +1,23 @@ package main import ( + "encoding/json" "flag" + "io" "log" "net/http" -) + "os" + "path" + "strings" -func refreshAvatar(w http.ResponseWriter, req *http.Request) { - w.Write([]byte("ok")) -} + "github.com/animenotifier/arn" + + _ "image/gif" + _ "image/jpeg" + _ "image/png" + + "github.com/animenotifier/avatar/lib" +) var port = "8001" @@ -18,6 +27,41 @@ func init() { } func main() { - http.HandleFunc("/", refreshAvatar) + // Switch to main directory + exe, err := os.Executable() + + if err != nil { + panic(err) + } + + root := path.Dir(exe) + os.Chdir(path.Join(root, "../../")) + + // Start server + http.HandleFunc("/", onRequest) log.Fatal(http.ListenAndServe(":"+port, nil)) } + +// onRequest handles requests and refreshes the requested avatar +func onRequest(w http.ResponseWriter, req *http.Request) { + userID := strings.TrimPrefix(req.URL.Path, "/") + user, err := arn.GetUser(userID) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, err.Error()) + return + } + + // Refresh + lib.RefreshAvatar(user) + + // Send JSON response + buffer, err := json.Marshal(user.Avatar) + + if err != nil { + io.WriteString(w, err.Error()) + } + + w.Write(buffer) +} diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index ac1757f1..97d73ac0 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -1,13 +1,10 @@ package main import ( - "fmt" "os" "path" - "reflect" "runtime" "sync" - "time" _ "image/gif" _ "image/jpeg" @@ -15,19 +12,10 @@ import ( "github.com/aerogo/log" "github.com/animenotifier/arn" - "github.com/animenotifier/avatar" - "github.com/animenotifier/avatar/outputs" - "github.com/animenotifier/avatar/sources" + "github.com/animenotifier/avatar/lib" "github.com/fatih/color" ) -const ( - webPQuality = 80 -) - -var avatarSources []avatar.Source -var avatarOutputs []avatar.Output -var avatarLog = log.New() var wg sync.WaitGroup // Main @@ -45,51 +33,8 @@ func main() { os.Chdir(path.Join(root, "../../")) // Log - avatarLog.AddOutput(log.File("logs/avatar.log")) - defer avatarLog.Flush() - - // Define the avatar sources - avatarSources = []avatar.Source{ - &sources.Gravatar{ - Rating: "pg", - RequestLimiter: time.NewTicker(100 * time.Millisecond), - }, - &sources.MyAnimeList{ - RequestLimiter: time.NewTicker(250 * time.Millisecond), - }, - &sources.FileSystem{ - Directory: "images/avatars/large/", - }, - } - - // Define the avatar outputs - avatarOutputs = []avatar.Output{ - // Original - Large - &outputs.OriginalFile{ - Directory: "images/avatars/large/", - Size: arn.AvatarMaxSize, - }, - - // Original - Small - &outputs.OriginalFile{ - Directory: "images/avatars/small/", - Size: arn.AvatarSmallSize, - }, - - // WebP - Large - &outputs.WebPFile{ - Directory: "images/avatars/large/", - Size: arn.AvatarMaxSize, - Quality: webPQuality, - }, - - // WebP - Small - &outputs.WebPFile{ - Directory: "images/avatars/small/", - Size: arn.AvatarSmallSize, - Quality: webPQuality, - }, - } + lib.Log.AddOutput(log.File("logs/avatar.log")) + defer lib.Log.Flush() if InvokeShellArgs() { return @@ -97,7 +42,7 @@ func main() { // Worker queue usersQueue := make(chan *arn.User, runtime.NumCPU()) - StartWorkers(usersQueue, Work) + StartWorkers(usersQueue, lib.RefreshAvatar) allUsers, _ := arn.AllUsers() @@ -123,59 +68,3 @@ func StartWorkers(queue chan *arn.User, work func(*arn.User)) { }() } } - -// Work handles a single user. -func Work(user *arn.User) { - user.Avatar.Extension = "" - - for _, source := range avatarSources { - avatar, err := source.GetAvatar(user) - - if err != nil { - avatarLog.Error(err) - continue - } - - if avatar == nil { - // fmt.Println(color.RedString("✘"), reflect.TypeOf(source).Elem().Name(), user.Nick) - continue - } - - // Name of source - user.Avatar.Source = reflect.TypeOf(source).Elem().Name() - - // Log - fmt.Println(color.GreenString("✔"), user.Avatar.Source, "|", user.Nick, "|", avatar) - - // Avoid JPG quality loss (if it's on the file system, we don't need to write it again) - if user.Avatar.Source == "FileSystem" { - user.Avatar.Extension = avatar.Extension() - break - } - - for _, writer := range avatarOutputs { - err := writer.SaveAvatar(avatar) - - if err != nil { - color.Red(err.Error()) - } - } - - break - } - - // Since this a very long running job, refresh user data before saving it. - avatarExt := user.Avatar.Extension - avatarSrc := user.Avatar.Source - user, err := arn.GetUser(user.ID) - - if err != nil { - avatarLog.Error("Can't refresh user info:", user.ID, user.Nick) - return - } - - // Save avatar data - user.Avatar.Extension = avatarExt - user.Avatar.Source = avatarSrc - user.Save() -} diff --git a/jobs/avatars/shell.go b/jobs/avatars/shell.go index ab957ac2..da798e98 100644 --- a/jobs/avatars/shell.go +++ b/jobs/avatars/shell.go @@ -4,6 +4,7 @@ import ( "flag" "github.com/animenotifier/arn" + "github.com/animenotifier/avatar/lib" ) // Shell parameters @@ -26,7 +27,7 @@ func InvokeShellArgs() bool { panic(err) } - Work(user) + lib.RefreshAvatar(user) return true } @@ -37,7 +38,7 @@ func InvokeShellArgs() bool { panic(err) } - Work(user) + lib.RefreshAvatar(user) return true } diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 6c89d07f..f396f52d 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -118,18 +118,18 @@ component Settings(user *arn.User) option(value="") Automatic option(value="Gravatar") Gravatar option(value="URL") Link - option(value="FileSystem") Upload + //- option(value="FileSystem") Upload if user.Settings().Avatar.Source == "Gravatar" || (user.Settings().Avatar.Source == "" && user.Avatar.Source == "Gravatar") .profile-image-container.avatar-preview - img.profile-image(src=user.Gravatar(), alt="Gravatar") + img.profile-image.mountable(src=user.Gravatar(), alt="Gravatar") if user.Settings().Avatar.Source == "URL" InputText("Avatar.SourceURL", user.Settings().Avatar.SourceURL, "Link", "Post the link to the image here") if user.Settings().Avatar.SourceURL != "" .profile-image-container.avatar-preview - img.profile-image(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") + img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title From d2ecff2d15bc32d0e0a84fe5fc9bc54c45fbd007 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Jul 2017 07:54:50 +0200 Subject: [PATCH 278/527] Configurable avatar link --- pages/settings/settings.pixy | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index f396f52d..13a86784 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -107,29 +107,29 @@ component Settings(user *arn.User) Icon("android") span Get the Android App - .widget.mountable + .widget.mountable(data-api="/api/settings/" + user.ID) h3.widget-title Icon("picture-o") span Avatar - - .widget-input(data-api="/api/settings/" + user.ID) + + .widget-input label(for="Avatar.Source") Source: select.widget-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change") - option(value="") Automatic + option(value="") Automatic option(value="Gravatar") Gravatar option(value="URL") Link //- option(value="FileSystem") Upload - - if user.Settings().Avatar.Source == "Gravatar" || (user.Settings().Avatar.Source == "" && user.Avatar.Source == "Gravatar") - .profile-image-container.avatar-preview - img.profile-image.mountable(src=user.Gravatar(), alt="Gravatar") - - if user.Settings().Avatar.Source == "URL" - InputText("Avatar.SourceURL", user.Settings().Avatar.SourceURL, "Link", "Post the link to the image here") - if user.Settings().Avatar.SourceURL != "" - .profile-image-container.avatar-preview - img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") + if user.Settings().Avatar.Source == "URL" + InputText("Avatar.SourceURL", user.Settings().Avatar.SourceURL, "Link", "Post the link to the image here") + + if user.Settings().Avatar.Source == "Gravatar" || (user.Settings().Avatar.Source == "" && user.Avatar.Source == "Gravatar") + .profile-image-container.avatar-preview + img.profile-image.mountable(src=user.Gravatar(), alt="Gravatar") + + if user.Settings().Avatar.Source == "URL" && user.Settings().Avatar.SourceURL != "" + .profile-image-container.avatar-preview + img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title From d895f6b89f7ec716ac17523c4e78388e4265f19f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Jul 2017 11:23:02 +0200 Subject: [PATCH 279/527] Fixed diff --- scripts/Diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index b1fe5318..3cdf5906 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -51,7 +51,7 @@ export class Diff { if(elemA.classList.contains("lazy")) { if(elemA.dataset.src !== elemB.dataset.src) { elemA.dataset.src = elemB.dataset.src - elemA.title = elemB.title + elemA.setAttribute("title", elemB.getAttribute("title")) } continue } From 36ee6619486daf1b66693dbf6210afa47fdbd9d7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Jul 2017 11:36:41 +0200 Subject: [PATCH 280/527] Fixed diffs --- scripts/Diff.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 3cdf5906..05518167 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -48,10 +48,10 @@ export class Diff { let elemB = b as HTMLElement // Ignore lazy images if they have the same source - if(elemA.classList.contains("lazy")) { + if(elemA.classList.contains("lazy") && elemB.classList.contains("lazy")) { if(elemA.dataset.src !== elemB.dataset.src) { elemA.dataset.src = elemB.dataset.src - elemA.setAttribute("title", elemB.getAttribute("title")) + elemA.title = elemB.title } continue } From 01dfb9636358be6dc65e4378c94adf542c63df79 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 02:36:52 +0200 Subject: [PATCH 281/527] Change statistics --- jobs/statistics/statistics.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index 53645ed0..d6861e36 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -42,7 +42,6 @@ func getUserStats() []*arn.PieChart { gender := stats{} os := stats{} notifications := stats{} - activity := stats{} avatar := stats{} for _, info := range analytics { @@ -53,6 +52,10 @@ func getUserStats() []*arn.PieChart { } for user := range arn.MustStreamUsers() { + if !user.IsActive() { + continue + } + if user.Gender != "" && user.Gender != "other" { gender[user.Gender]++ } @@ -79,12 +82,6 @@ func getUserStats() []*arn.PieChart { notifications["Disabled"]++ } - if user.IsActive() { - activity["Active last week"]++ - } else { - activity["Inactive"]++ - } - if user.Avatar.Source == "" { avatar["none"]++ } else { @@ -100,7 +97,6 @@ func getUserStats() []*arn.PieChart { arn.NewPieChart("Browser", browser), arn.NewPieChart("Country", country), arn.NewPieChart("Avatar", avatar), - arn.NewPieChart("Activity", activity), arn.NewPieChart("Notifications", notifications), arn.NewPieChart("Gender", gender), arn.NewPieChart("Pixel ratio", pixelRatio), From 48a792a1008b6ca54bdbba11247b5cbcb84fff22 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 03:22:50 +0200 Subject: [PATCH 282/527] Implemented pushsubscriptionchange --- main.go | 2 ++ pages/me/me.go | 17 +++++++++++++++++ sw/service-worker.ts | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 pages/me/me.go diff --git a/main.go b/main.go index 5f2a2019..52f7afc4 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ import ( "github.com/animenotifier/notify.moe/pages/listimport/listimportkitsu" "github.com/animenotifier/notify.moe/pages/listimport/listimportmyanimelist" "github.com/animenotifier/notify.moe/pages/login" + "github.com/animenotifier/notify.moe/pages/me" "github.com/animenotifier/notify.moe/pages/music" "github.com/animenotifier/notify.moe/pages/newsoundtrack" "github.com/animenotifier/notify.moe/pages/newthread" @@ -129,6 +130,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/extension/embed", embed.Get) // API + app.Get("/api/me", me.Get) app.Get("/api/test/notification", notifications.Test) // PayPal diff --git a/pages/me/me.go b/pages/me/me.go new file mode 100644 index 00000000..51a42c9b --- /dev/null +++ b/pages/me/me.go @@ -0,0 +1,17 @@ +package me + +import ( + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/utils" +) + +// Get ... +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.JSON(nil) + } + + return ctx.JSON(user) +} diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 2f843de4..d952a77b 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -157,7 +157,38 @@ self.addEventListener("push", (evt: PushEvent) => { }) self.addEventListener("pushsubscriptionchange", (evt: any) => { - console.log("pushsubscriptionchange", evt) + evt.waitUntil((self as any).registration.pushManager.subscribe(evt.oldSubscription.options) + .then(async subscription => { + console.log("Send subscription to server...") + + let rawKey = subscription.getKey("p256dh") + let key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : "" + + let rawSecret = subscription.getKey("auth") + let secret = rawSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawSecret))) : "" + + let endpoint = subscription.endpoint + + let pushSubscription = { + endpoint, + p256dh: key, + auth: secret, + platform: navigator.platform, + userAgent: navigator.userAgent, + screen: { + width: window.screen.width, + height: window.screen.height + } + } + + let user = await fetch("/api/me").then(response => response.json()) + + return fetch("/api/pushsubscriptions/" + user.id + "/add", { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(pushSubscription) + }) + })) }) self.addEventListener("notificationclick", (evt: NotificationEvent) => { From ab175e8dabd745467780ecfec9a21b5971d947e3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 03:36:55 +0200 Subject: [PATCH 283/527] Improved character layout --- pages/character/character.pixy | 4 +++- pages/character/character.scarlet | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 pages/character/character.scarlet diff --git a/pages/character/character.pixy b/pages/character/character.pixy index 0963d600..2c437e44 100644 --- a/pages/character/character.pixy +++ b/pages/character/character.pixy @@ -1,5 +1,7 @@ component CharacterDetails(character *arn.Character) h1= character.Name + p img(src=character.Image, alt=character.Name) - p= character.Description \ No newline at end of file + + p.character-description= character.Description \ No newline at end of file diff --git a/pages/character/character.scarlet b/pages/character/character.scarlet new file mode 100644 index 00000000..49043fb8 --- /dev/null +++ b/pages/character/character.scarlet @@ -0,0 +1,4 @@ +.character-description + max-width 800px + margin 0 auto + margin-top 1.6rem \ No newline at end of file From 6e46c8950ab03d3fc0eee3566f86f641e6ee307c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 03:45:30 +0200 Subject: [PATCH 284/527] Updated search index --- jobs/search-index/search-index.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/search-index/search-index.go b/jobs/search-index/search-index.go index 0edb8450..273e6807 100644 --- a/jobs/search-index/search-index.go +++ b/jobs/search-index/search-index.go @@ -77,7 +77,7 @@ func updateUserIndex() { } for user := range userStream { - if user.IsActive() && user.Nick != "" { + if user.HasNick() { userSearchIndex.TextToID[strings.ToLower(user.Nick)] = user.ID } } From 1d3d069766ede0e28e05fd23948f69d69ee057a0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 04:04:19 +0200 Subject: [PATCH 285/527] Fixed diffs --- scripts/Diff.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 05518167..93c162aa 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -21,6 +21,7 @@ export class Diff { for(let i = 0; i < numNodes; i++) { let a = aChild[i] + // Remove nodes at the end of a that do not exist in b if(i >= bChild.length) { aRoot.removeChild(a) continue @@ -28,34 +29,31 @@ export class Diff { let b = bChild[i] + // If a doesn't have that many nodes, simply append at the end of a if(i >= aChild.length) { aRoot.appendChild(b) continue } + // If it's a completely different HTML tag or node type, replace it if(a.nodeName !== b.nodeName || a.nodeType !== b.nodeType) { aRoot.replaceChild(b, a) continue } + // Text node: + // We don't need to check for b to be a text node as well because + // we eliminated different node types in the previous condition. if(a.nodeType === Node.TEXT_NODE) { a.textContent = b.textContent continue } + // HTML element: if(a.nodeType === Node.ELEMENT_NODE) { let elemA = a as HTMLElement let elemB = b as HTMLElement - // Ignore lazy images if they have the same source - if(elemA.classList.contains("lazy") && elemB.classList.contains("lazy")) { - if(elemA.dataset.src !== elemB.dataset.src) { - elemA.dataset.src = elemB.dataset.src - elemA.title = elemB.title - } - continue - } - // Skip iframes // This part needs to be executed AFTER lazy images check // to allow lazily loaded iframes to update their data src. @@ -84,7 +82,7 @@ export class Diff { if(attrib.specified) { // Skip mountables - if(attrib.name == "class" && elemA.classList.contains("mounted")) { + if(attrib.name == "class" && (elemA.classList.contains("mounted") || elemA.classList.contains("image-found"))) { continue } From 574ac9e8854ff8b471183ceafc07659b2e16b835 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 04:18:56 +0200 Subject: [PATCH 286/527] Improved diffs --- scripts/AnimeNotifier.ts | 4 ++++ scripts/Diff.ts | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 759772c5..fbdb4655 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -30,6 +30,10 @@ export class AnimeNotifier { this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found")) this.unmount = new MutationQueue(elem => elem.classList.remove("mounted")) + // These classes will never be removed on DOM diffs + Diff.persistentClasses.add("mounted") + Diff.persistentClasses.add("image-found") + if("IntersectionObserver" in window) { // Enable lazy load this.visibilityObserver = new IntersectionObserver( diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 93c162aa..974a0450 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -1,4 +1,6 @@ export class Diff { + static persistentClasses = new Set() + // Reuse container for diffs to avoid memory allocation static container: HTMLElement @@ -82,7 +84,22 @@ export class Diff { if(attrib.specified) { // Skip mountables - if(attrib.name == "class" && (elemA.classList.contains("mounted") || elemA.classList.contains("image-found"))) { + if(attrib.name == "class") { + let classesA = elemA.classList + let classesB = elemB.classList + + for(let className of classesA) { + if(!classesB.contains(className) && !Diff.persistentClasses.has(className)) { + classesA.remove(className) + } + } + + for(let className of classesB) { + if(!classesA.contains(className)) { + classesA.add(className) + } + } + continue } From 406306df37f031018e91c3fca1d61bdac8f78c92 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 04:24:42 +0200 Subject: [PATCH 287/527] Fixed profile links --- pages/profile/profile.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index e06736f5..7f9672e8 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -20,7 +20,7 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) if viewUser.Website != "" p.profile-field.website Icon("home") - a(href=viewUser.WebsiteURL(), target="_blank", rel="nofollow")= viewUser.Website + a(href=viewUser.WebsiteURL(), target="_blank", rel="nofollow")= viewUser.WebsiteShortURL() if viewUser.Accounts.Osu.Nick != "" && viewUser.Accounts.Osu.PP >= 1000 p.profile-field.osu(title="osu! Level " + toString(int(viewUser.Accounts.Osu.Level)) + " | Accuracy: " + fmt.Sprintf("%.1f", viewUser.Accounts.Osu.Accuracy) + "%") From 647aed0e76f6246740279887d0e1339ae0abd7c0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 04:47:32 +0200 Subject: [PATCH 288/527] Improved diff --- pages/animelist/animelist.pixy | 4 ++-- scripts/Application.ts | 2 +- scripts/Diff.ts | 42 +++++++++++++++++++--------------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 95ccb1c3..4b26c19e 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -43,8 +43,8 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical - if user != nil && item.Status == arn.AnimeListStatusWatching && item.Anime().EpisodeByNumber(item.Episodes + 1) != nil - td.anime-list-item-actions + td.anime-list-item-actions + if user != nil && item.Status == arn.AnimeListStatusWatching && item.Anime().EpisodeByNumber(item.Episodes + 1) != nil for _, link := range item.Anime().EpisodeByNumber(item.Episodes + 1).Links a(href=link, title="Watch episode " + toString(item.Episodes + 1) + " on twist.moe", target="_blank", rel="noopener") RawIcon("eye") diff --git a/scripts/Application.ts b/scripts/Application.ts index d1d30bd5..3e041285 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -152,7 +152,7 @@ export class Application { for(let i = 0; i < links.length; i++) { let link = links[i] as HTMLElement - link.classList.remove(this.ajaxClass) + // link.classList.remove(this.ajaxClass) let self = this link.onclick = function(e) { diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 974a0450..6fa223c0 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -82,29 +82,35 @@ export class Diff { for(let x = 0; x < elemB.attributes.length; x++) { let attrib = elemB.attributes[x] - if(attrib.specified) { - // Skip mountables - if(attrib.name == "class") { - let classesA = elemA.classList - let classesB = elemB.classList - - for(let className of classesA) { - if(!classesB.contains(className) && !Diff.persistentClasses.has(className)) { - classesA.remove(className) - } - } - - for(let className of classesB) { - if(!classesA.contains(className)) { - classesA.add(className) - } - } + if(!attrib.specified) { + continue + } + if(attrib.name === "class") { + // If the class is exactly the same, skip this attribute. + if(elemA.getAttribute("class") === attrib.value) { continue } - elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name)) + let classesA = elemA.classList + let classesB = elemB.classList + + for(let className of classesA) { + if(!classesB.contains(className) && !Diff.persistentClasses.has(className)) { + classesA.remove(className) + } + } + + for(let className of classesB) { + if(!classesA.contains(className)) { + classesA.add(className) + } + } + + continue } + + elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name)) } // Special case: Apply state of input elements From 197ef0197acef254463687cce629edfe3c68076c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 05:23:06 +0200 Subject: [PATCH 289/527] Implemented full page diffs --- scripts/AnimeNotifier.ts | 50 +++++++++++++++++++++++++++++++++------- scripts/Application.ts | 3 --- scripts/Diff.ts | 13 +++++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index fbdb4655..d44ce36c 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -108,9 +108,6 @@ export class AnimeNotifier { // Let"s start this.app.run() - // Service worker - this.registerServiceWorker() - // Push manager this.pushManager = new PushManager() } @@ -158,8 +155,13 @@ export class AnimeNotifier { } onIdle() { + // Service worker + this.registerServiceWorker() + + // Analytics this.pushAnalytics() + // Offline message if(navigator.onLine === false) { this.statusMessage.showError("You are viewing an offline version of the site now.") } @@ -170,6 +172,8 @@ export class AnimeNotifier { return } + console.log("register service worker") + navigator.serviceWorker.register("/service-worker").then(registration => { registration.update() }) @@ -178,7 +182,7 @@ export class AnimeNotifier { this.onServiceWorkerMessage(evt) }) - document.addEventListener("DOMContentLoaded", () => { + let sendContentLoadedEvent = () => { if(!navigator.serviceWorker.controller) { return } @@ -194,8 +198,18 @@ export class AnimeNotifier { message.url = window.location.href } + console.log("send loaded event to service worker") + navigator.serviceWorker.controller.postMessage(JSON.stringify(message)) - }) + } + + // For future loaded events + document.addEventListener("DOMContentLoaded", sendContentLoadedEvent) + + // If the page is loaded already, send the loaded event right now. + if(document.readyState !== "loading") { + sendContentLoadedEvent() + } } onServiceWorkerMessage(evt: ServiceWorkerMessageEvent) { @@ -307,7 +321,6 @@ export class AnimeNotifier { return Promise.reject("old request") } - this.app.eTag = response.headers.get("ETag") return Promise.resolve(response) }) .then(response => response.text()) @@ -316,7 +329,29 @@ export class AnimeNotifier { } reloadPage() { - location.reload() + console.log("reload page") + + let headers = new Headers() + headers.append("X-Reload", "true") + + let path = this.app.currentPath + + return fetch(path, { + credentials: "same-origin", + headers + }) + .then(response => { + if(this.app.currentPath !== path) { + return Promise.reject("old request") + } + + return Promise.resolve(response) + }) + .then(response => response.text()) + .then(html => { + Diff.root(document.documentElement, html) + }) + .then(() => this.app.emit("DOMContentLoaded")) } loading(isLoading: boolean) { @@ -477,7 +512,6 @@ export class AnimeNotifier { credentials: "same-origin" }) .then(response => { - this.app.eTag = response.headers.get("ETag") return response.text() }) diff --git a/scripts/Application.ts b/scripts/Application.ts index 3e041285..4f4057cd 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -13,7 +13,6 @@ export class Application { loading: HTMLElement currentPath: string originalPath: string - eTag: string lastRequest: XMLHttpRequest constructor() { @@ -46,8 +45,6 @@ export class Application { request.onerror = () => reject(new Error("You are either offline or the requested page doesn't exist.")) request.ontimeout = () => reject(new Error("The page took too much time to respond.")) request.onload = () => { - this.eTag = request.getResponseHeader("ETag") - if(request.status < 200 || request.status >= 400) reject(request.responseText) else diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 6fa223c0..e26ca35c 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -3,6 +3,7 @@ export class Diff { // Reuse container for diffs to avoid memory allocation static container: HTMLElement + static rootContainer: HTMLElement // innerHTML will diff the element with the given HTML string and apply DOM mutations. static innerHTML(aRoot: HTMLElement, html: string) { @@ -14,6 +15,18 @@ export class Diff { Diff.childNodes(aRoot, Diff.container) } + // root will diff the document root element with the given HTML string and apply DOM mutations. + static root(aRoot: HTMLElement, html: string) { + if(!Diff.rootContainer) { + Diff.rootContainer = document.createElement("html") + } + + Diff.rootContainer.innerHTML = html.replace("", "") + + console.log(Diff.rootContainer) + Diff.childNodes(aRoot, Diff.rootContainer) + } + // childNodes diffs the child nodes of 2 given elements and applies DOM mutations. static childNodes(aRoot: Node, bRoot: Node) { let aChild = [...aRoot.childNodes] From 70aeea3db16553ba6076138e95888b6856fa5016 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 05:27:37 +0200 Subject: [PATCH 290/527] Fixed incorrect loading state --- scripts/AnimeNotifier.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index d44ce36c..50c58e6e 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -352,6 +352,7 @@ export class AnimeNotifier { Diff.root(document.documentElement, html) }) .then(() => this.app.emit("DOMContentLoaded")) + .then(() => this.loading(false)) // Because our loading element gets reset due to full page diff } loading(isLoading: boolean) { From 582a3cace146c65043ed5107a42fa08db39a2387 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 05:29:18 +0200 Subject: [PATCH 291/527] Removed debug log --- scripts/Diff.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index e26ca35c..f15765ed 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -22,8 +22,6 @@ export class Diff { } Diff.rootContainer.innerHTML = html.replace("", "") - - console.log(Diff.rootContainer) Diff.childNodes(aRoot, Diff.rootContainer) } From 3ee21850c8b2f93d828351e4e458978bfbdcc5a6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 05:34:39 +0200 Subject: [PATCH 292/527] Changed icon --- mixins/Navigation.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 4ce84de4..500f525c 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -6,7 +6,7 @@ component Navigation(user *arn.User) component LoggedOutMenu nav#navigation.logged-out - NavigationButton("About", "/", "question-circle") + NavigationButton("About", "/", "home") NavigationButton("Explore", "/explore", "th") NavigationButton("Forum", "/forum", "comment") From d1603fad4029370b34618e9c4107fa621eaa8b86 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 06:32:31 +0200 Subject: [PATCH 293/527] Improved service worker --- scripts/AnimeNotifier.ts | 27 +++++++++++++++++---------- sw/service-worker.ts | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 50c58e6e..75df0498 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -16,6 +16,7 @@ export class AnimeNotifier { statusMessage: StatusMessage visibilityObserver: IntersectionObserver pushManager: PushManager + lastRequestURL: string imageFound: MutationQueue imageNotFound: MutationQueue @@ -176,12 +177,18 @@ export class AnimeNotifier { navigator.serviceWorker.register("/service-worker").then(registration => { registration.update() + // if("sync" in registration) { + // registration.sync.register("background sync") + // } else { + // console.log("background sync not supported") + // } }) navigator.serviceWorker.addEventListener("message", evt => { this.onServiceWorkerMessage(evt) }) + // This will send a message to the service worker that the DOM has been loaded let sendContentLoadedEvent = () => { if(!navigator.serviceWorker.controller) { return @@ -192,14 +199,14 @@ export class AnimeNotifier { url: "" } - if(this.app.lastRequest) { + if(this.lastRequestURL) { + message.url = this.lastRequestURL + } else if(this.app.lastRequest) { message.url = this.app.lastRequest.responseURL } else { message.url = window.location.href } - console.log("send loaded event to service worker") - navigator.serviceWorker.controller.postMessage(JSON.stringify(message)) } @@ -311,8 +318,9 @@ export class AnimeNotifier { headers.append("X-Reload", "true") let path = this.app.currentPath + this.lastRequestURL = location.origin + "/_" + path - return fetch("/_" + path, { + return fetch(this.lastRequestURL, { credentials: "same-origin", headers }) @@ -330,15 +338,11 @@ export class AnimeNotifier { reloadPage() { console.log("reload page") - - let headers = new Headers() - headers.append("X-Reload", "true") let path = this.app.currentPath return fetch(path, { - credentials: "same-origin", - headers + credentials: "same-origin" }) .then(response => { if(this.app.currentPath !== path) { @@ -509,7 +513,10 @@ export class AnimeNotifier { return Promise.reject(null) } - let request = fetch("/_" + url, { + let path = "/_" + url + this.lastRequestURL = location.origin + "/_" + path + + let request = fetch(path, { credentials: "same-origin" }) .then(response => { diff --git a/sw/service-worker.ts b/sw/service-worker.ts index d952a77b..42281ff8 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -43,11 +43,29 @@ self.addEventListener("activate", (evt: any) => { // controlling service worker self.addEventListener("message", (evt: any) => { let message = JSON.parse(evt.data) - + let url = message.url let refresh = RELOADS.get(url) let servedETag = ETAGS.get(url) + // If the user requests a sub-page we should prefetch the full page, too. + if(url.includes("/_/")) { + let fullPage = new Request(url.replace("/_/", "/")) + + fetch(fullPage, { + credentials: "same-origin" + }) + .then(response => { + // Save the new version of the resource in the cache + let cacheRefresh = caches.open(CACHE).then(cache => { + return cache.put(fullPage, response) + }) + + CACHEREFRESH.set(fullPage.url, cacheRefresh) + return cacheRefresh + }) + } + if(!refresh || !servedETag) { return } @@ -73,11 +91,6 @@ self.addEventListener("message", (evt: any) => { url } - // If a subpage has refreshed, refresh the main page cache, too. - // if(url.includes("/_/")) { - - // } - let cacheRefresh = CACHEREFRESH.get(url) if(!cacheRefresh) { @@ -91,6 +104,21 @@ self.addEventListener("message", (evt: any) => { ) }) +// self.addEventListener("sync", (evt: any) => { +// console.log(evt.tag) + +// let fetches = new Array>() + +// for(let url of BACKGROUNDFETCHES.keys()) { + +// } + +// console.log("background fetching:", BACKGROUNDFETCHES.keys()) +// BACKGROUNDFETCHES.clear() + +// evt.waitUntil(Promise.all(fetches)) +// }) + self.addEventListener("fetch", async (evt: FetchEvent) => { let request = evt.request as Request let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") From d5e1ce4e3a86f0bb41839eca9c55d5f7ce6b0c63 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 06:55:21 +0200 Subject: [PATCH 294/527] Improved caching --- scripts/AnimeNotifier.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 75df0498..d4082869 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -16,7 +16,7 @@ export class AnimeNotifier { statusMessage: StatusMessage visibilityObserver: IntersectionObserver pushManager: PushManager - lastRequestURL: string + mainPageLoaded: boolean imageFound: MutationQueue imageNotFound: MutationQueue @@ -199,14 +199,16 @@ export class AnimeNotifier { url: "" } - if(this.lastRequestURL) { - message.url = this.lastRequestURL - } else if(this.app.lastRequest) { - message.url = this.app.lastRequest.responseURL + // If mainPageLoaded is set, it means every single request is now an AJAX request for the /_/ prefixed page + if(this.mainPageLoaded) { + message.url = window.location.origin + "/_" + window.location.pathname } else { + this.mainPageLoaded = true message.url = window.location.href } + console.log("Loaded", message.url) + navigator.serviceWorker.controller.postMessage(JSON.stringify(message)) } @@ -318,9 +320,8 @@ export class AnimeNotifier { headers.append("X-Reload", "true") let path = this.app.currentPath - this.lastRequestURL = location.origin + "/_" + path - return fetch(this.lastRequestURL, { + return fetch("/_" + path, { credentials: "same-origin", headers }) @@ -514,7 +515,6 @@ export class AnimeNotifier { } let path = "/_" + url - this.lastRequestURL = location.origin + "/_" + path let request = fetch(path, { credentials: "same-origin" From 13978b7e8cc229bb60093ebc1bce3ea2b720d490 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 07:39:09 +0200 Subject: [PATCH 295/527] Improved service worker --- pages/dashboard/dashboard.pixy | 2 +- scripts/AnimeNotifier.ts | 23 ++++++++++------ sw/service-worker.ts | 48 ++++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index bb8f161b..eedd8f7e 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -3,7 +3,7 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound .widgets .widget.mountable - h3.widget-title Schedule + h3.widget-title Schedule 123 for i := 0; i <= 4; i++ if i < len(schedule) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index d4082869..19232864 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -17,6 +17,7 @@ export class AnimeNotifier { visibilityObserver: IntersectionObserver pushManager: PushManager mainPageLoaded: boolean + lastReloadContentPath: string imageFound: MutationQueue imageNotFound: MutationQueue @@ -176,12 +177,7 @@ export class AnimeNotifier { console.log("register service worker") navigator.serviceWorker.register("/service-worker").then(registration => { - registration.update() - // if("sync" in registration) { - // registration.sync.register("background sync") - // } else { - // console.log("background sync not supported") - // } + // registration.update() }) navigator.serviceWorker.addEventListener("message", evt => { @@ -194,6 +190,13 @@ export class AnimeNotifier { return } + // A reloadContent call should never trigger another reload + if(this.app.currentPath === this.lastReloadContentPath) { + console.log("reload finished.") + this.lastReloadContentPath = "" + return + } + let message = { type: "loaded", url: "" @@ -207,7 +210,7 @@ export class AnimeNotifier { message.url = window.location.href } - console.log("Loaded", message.url) + console.log("checking for updates:", message.url) navigator.serviceWorker.controller.postMessage(JSON.stringify(message)) } @@ -316,10 +319,13 @@ export class AnimeNotifier { } reloadContent() { + console.log("reload content", "/_" + this.app.currentPath) + let headers = new Headers() headers.append("X-Reload", "true") let path = this.app.currentPath + this.lastReloadContentPath = path return fetch("/_" + path, { credentials: "same-origin", @@ -338,9 +344,10 @@ export class AnimeNotifier { } reloadPage() { - console.log("reload page") + console.log("reload page", this.app.currentPath) let path = this.app.currentPath + this.lastReloadContentPath = path return fetch(path, { credentials: "same-origin" diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 42281ff8..ef88212d 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -4,9 +4,15 @@ const CACHE = "v-1" const RELOADS = new Map>() const ETAGS = new Map() const CACHEREFRESH = new Map>() +const EXCLUDECACHE = new Set([ + "/api/", + "/paypal/", + "/import/", + "chrome-extension" +]) self.addEventListener("install", (evt: InstallEvent) => { - console.log("Service worker install") + console.log("service worker install") evt.waitUntil( (self as any).skipWaiting().then(() => { @@ -16,7 +22,7 @@ self.addEventListener("install", (evt: InstallEvent) => { }) self.addEventListener("activate", (evt: any) => { - console.log("Service worker activate") + console.log("service worker activate") // Delete old cache let cacheWhitelist = [CACHE] @@ -94,35 +100,32 @@ self.addEventListener("message", (evt: any) => { let cacheRefresh = CACHEREFRESH.get(url) if(!cacheRefresh) { + console.log("forcing reload, cache refresh null") return evt.source.postMessage(JSON.stringify(message)) } return cacheRefresh.then(() => { + console.log("forcing reload after cache refresh") evt.source.postMessage(JSON.stringify(message)) }) }) ) }) -// self.addEventListener("sync", (evt: any) => { -// console.log(evt.tag) - -// let fetches = new Array>() - -// for(let url of BACKGROUNDFETCHES.keys()) { - -// } - -// console.log("background fetching:", BACKGROUNDFETCHES.keys()) -// BACKGROUNDFETCHES.clear() - -// evt.waitUntil(Promise.all(fetches)) -// }) - self.addEventListener("fetch", async (evt: FetchEvent) => { let request = evt.request as Request let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") - let ignoreCache = request.url.includes("/api/") || request.url.includes("/paypal/") || request.url.includes("chrome-extension") + let ignoreCache = false + + console.log("fetch:", request.url) + + // Exclude certain URLs from being cached + for(let pattern of EXCLUDECACHE.keys()) { + if(request.url.includes(pattern)) { + ignoreCache = true + break + } + } // Delete existing cache on authentication if(isAuth) { @@ -155,11 +158,16 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { // Forced reload if(request.headers.get("X-Reload") === "true") { - return evt.waitUntil(refresh) + return evt.waitUntil(refresh.then(response => { + servedETag = response.headers.get("ETag") + ETAGS.set(request.url, servedETag) + return response + })) } // Try to serve cache first and fall back to network response let networkOrCache = fromCache(request).then(response => { + console.log("served from cache:", request.url) servedETag = response.headers.get("ETag") ETAGS.set(request.url, servedETag) return response @@ -187,7 +195,7 @@ self.addEventListener("push", (evt: PushEvent) => { self.addEventListener("pushsubscriptionchange", (evt: any) => { evt.waitUntil((self as any).registration.pushManager.subscribe(evt.oldSubscription.options) .then(async subscription => { - console.log("Send subscription to server...") + console.log("send subscription to server...") let rawKey = subscription.getKey("p256dh") let key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : "" From 6b64cc62dd002cc685a6c5e774c99ac14d5ddab3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 07:40:31 +0200 Subject: [PATCH 296/527] Minor fix --- pages/dashboard/dashboard.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index eedd8f7e..bb8f161b 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -3,7 +3,7 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound .widgets .widget.mountable - h3.widget-title Schedule 123 + h3.widget-title Schedule for i := 0; i <= 4; i++ if i < len(schedule) From 1f3507ea9e8bafbf44c8a481b67cd8be79b016d6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 08:45:41 +0200 Subject: [PATCH 297/527] Fix double request --- scripts/AnimeNotifier.ts | 11 ++++++++--- sw/service-worker.ts | 11 +++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 19232864..95df5f08 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -232,7 +232,7 @@ export class AnimeNotifier { if(message.url.includes("/_/")) { // Content reload this.contentLoadedActions.then(() => { - this.reloadContent() + this.reloadContent(true) }) } else { // Full page reload @@ -318,11 +318,16 @@ export class AnimeNotifier { } } - reloadContent() { + reloadContent(cached?: boolean) { console.log("reload content", "/_" + this.app.currentPath) let headers = new Headers() - headers.append("X-Reload", "true") + + if(!cached) { + headers.append("X-Reload", "true") + } else { + headers.append("X-CacheOnly", "true") + } let path = this.app.currentPath this.lastReloadContentPath = path diff --git a/sw/service-worker.ts b/sw/service-worker.ts index ef88212d..0deb43c2 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -137,10 +137,17 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { return evt.waitUntil(evt.respondWith(fetch(request))) } + // Forced cache response? + if(request.headers.get("X-CacheOnly") === "true") { + console.log("forced cache response") + return evt.waitUntil(fromCache(request)) + } + let servedETag = undefined // Start fetching the request let refresh = fetch(request).then(response => { + console.log(response) let clone = response.clone() // Save the new version of the resource in the cache @@ -158,11 +165,11 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { // Forced reload if(request.headers.get("X-Reload") === "true") { - return evt.waitUntil(refresh.then(response => { + return evt.waitUntil(evt.respondWith(refresh.then(response => { servedETag = response.headers.get("ETag") ETAGS.set(request.url, servedETag) return response - })) + }))) } // Try to serve cache first and fall back to network response From 80264f91b87bdf26121dd6de872fee50b625da49 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 08:49:38 +0200 Subject: [PATCH 298/527] Reduced log messages --- scripts/AnimeNotifier.ts | 2 +- sw/service-worker.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 95df5f08..e79dbd11 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -319,7 +319,7 @@ export class AnimeNotifier { } reloadContent(cached?: boolean) { - console.log("reload content", "/_" + this.app.currentPath) + // console.log("reload content", "/_" + this.app.currentPath) let headers = new Headers() diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 0deb43c2..07d838f8 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -117,7 +117,7 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") let ignoreCache = false - console.log("fetch:", request.url) + // console.log("fetch:", request.url) // Exclude certain URLs from being cached for(let pattern of EXCLUDECACHE.keys()) { @@ -139,7 +139,7 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { // Forced cache response? if(request.headers.get("X-CacheOnly") === "true") { - console.log("forced cache response") + // console.log("forced cache response") return evt.waitUntil(fromCache(request)) } @@ -147,7 +147,7 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { // Start fetching the request let refresh = fetch(request).then(response => { - console.log(response) + // console.log(response) let clone = response.clone() // Save the new version of the resource in the cache @@ -174,7 +174,7 @@ self.addEventListener("fetch", async (evt: FetchEvent) => { // Try to serve cache first and fall back to network response let networkOrCache = fromCache(request).then(response => { - console.log("served from cache:", request.url) + // console.log("served from cache:", request.url) servedETag = response.headers.get("ETag") ETAGS.set(request.url, servedETag) return response From cd6641cc069582bf08e83ff7acd138d2ac4741ca Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 09:09:55 +0200 Subject: [PATCH 299/527] Heavily improved page reload --- scripts/AnimeNotifier.ts | 7 +++++-- scripts/Diff.ts | 7 +++++-- scripts/main.ts | 5 ++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e79dbd11..a22ae953 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -36,6 +36,9 @@ export class AnimeNotifier { Diff.persistentClasses.add("mounted") Diff.persistentClasses.add("image-found") + // Never remove src property on diffs + Diff.persistentAttributes.add("src") + if("IntersectionObserver" in window) { // Enable lazy load this.visibilityObserver = new IntersectionObserver( @@ -374,10 +377,10 @@ export class AnimeNotifier { loading(isLoading: boolean) { if(isLoading) { - document.body.style.cursor = "progress" + document.documentElement.style.cursor = "progress" this.app.loading.classList.remove(this.app.fadeOutClass) } else { - document.body.style.cursor = "auto" + document.documentElement.style.cursor = "auto" this.app.loading.classList.add(this.app.fadeOutClass) } } diff --git a/scripts/Diff.ts b/scripts/Diff.ts index f15765ed..d450c0d9 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -1,5 +1,6 @@ export class Diff { static persistentClasses = new Set() + static persistentAttributes = new Set() // Reuse container for diffs to avoid memory allocation static container: HTMLElement @@ -22,7 +23,9 @@ export class Diff { } Diff.rootContainer.innerHTML = html.replace("", "") - Diff.childNodes(aRoot, Diff.rootContainer) + console.log(aRoot.getElementsByTagName("body")[0]) + console.log(Diff.rootContainer.getElementsByTagName("body")[0]) + Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.rootContainer.getElementsByTagName("body")[0]) } // childNodes diffs the child nodes of 2 given elements and applies DOM mutations. @@ -80,7 +83,7 @@ export class Diff { let attrib = elemA.attributes[x] if(attrib.specified) { - if(!elemB.hasAttribute(attrib.name)) { + if(!elemB.hasAttribute(attrib.name) && !Diff.persistentAttributes.has(attrib.name)) { removeAttributes.push(attrib) } } diff --git a/scripts/main.ts b/scripts/main.ts index 8bea2009..4f1ab54e 100644 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -4,4 +4,7 @@ import { AnimeNotifier } from "./AnimeNotifier" let app = new Application() let arn = new AnimeNotifier(app) -arn.init() \ No newline at end of file +arn.init() + +// For debugging purposes +window["arn"] = arn \ No newline at end of file From e57e67610fe7b8a96d24ec8c16a0125649d89639 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 13:08:12 +0200 Subject: [PATCH 300/527] Added user lists --- jobs/active-users/active-users.go | 84 +++++++++++++++++++++++++++---- main.go | 5 +- pages/profile/profile.pixy | 2 +- pages/users/users.go | 39 ++++++++++++-- pages/users/users.pixy | 17 +++++++ 5 files changed, 131 insertions(+), 16 deletions(-) diff --git a/jobs/active-users/active-users.go b/jobs/active-users/active-users.go index b804ec03..4719f262 100644 --- a/jobs/active-users/active-users.go +++ b/jobs/active-users/active-users.go @@ -11,16 +11,13 @@ import ( func main() { color.Yellow("Caching list of active users") - cache := arn.ListOfIDs{} - // Filter out active users with an avatar users, err := arn.FilterUsers(func(user *arn.User) bool { return user.IsActive() && user.Avatar.Extension != "" }) + fmt.Println(len(users)) - if err != nil { - panic(err) - } + arn.PanicOnError(err) // Sort sort.Slice(users, func(i, j int) bool { @@ -36,17 +33,82 @@ func main() { }) // Add users to list - for _, user := range users { - cache.IDList = append(cache.IDList, user.ID) + SaveInCache("active users", users) + + // Sort by osu rank + osuUsers := users[:] + + sort.Slice(osuUsers, func(i, j int) bool { + return osuUsers[i].Accounts.Osu.PP > osuUsers[j].Accounts.Osu.PP + }) + + // Cut off users with 0 pp + for index, user := range osuUsers { + if user.Accounts.Osu.PP == 0 { + osuUsers = osuUsers[:index] + break + } } - fmt.Println(len(cache.IDList), "users") + // Save osu users + SaveInCache("active osu users", osuUsers) - err = arn.DB.Set("Cache", "active users", cache) + // Sort by role + staff := users[:] - if err != nil { - panic(err) + sort.Slice(staff, func(i, j int) bool { + if staff[i].Role == "" { + return false + } + + if staff[j].Role == "" { + return true + } + + return staff[i].Role == "admin" + }) + + // Cut off non-staff + for index, user := range staff { + if user.Role == "" { + staff = staff[:index] + break + } } + // Save staff users + SaveInCache("active staff users", staff) + + // Sort by anime watching list length + watching := users[:] + + sort.Slice(watching, func(i, j int) bool { + return len(watching[i].AnimeList().FilterStatus(arn.AnimeListStatusWatching).Items) > len(watching[j].AnimeList().FilterStatus(arn.AnimeListStatusWatching).Items) + }) + + // Save watching users + SaveInCache("active anime watching users", watching) + color.Green("Finished.") } + +// SaveInCache ... +func SaveInCache(key string, users []*arn.User) { + cache := arn.ListOfIDs{ + IDList: GenerateIDList(users), + } + + fmt.Println(len(cache.IDList), key) + arn.PanicOnError(arn.DB.Set("Cache", key, cache)) +} + +// GenerateIDList generates an ID list from a slice of users. +func GenerateIDList(users []*arn.User) []string { + list := []string{} + + for _, user := range users { + list = append(list, user.ID) + } + + return list +} diff --git a/main.go b/main.go index 52f7afc4..87bd3aa3 100644 --- a/main.go +++ b/main.go @@ -84,7 +84,10 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) app.Ajax("/soundtracks", music.Get) - app.Ajax("/users", users.Get) + app.Ajax("/users", users.Active) + app.Ajax("/users/osu", users.Osu) + app.Ajax("/users/staff", users.Staff) + app.Ajax("/users/anime/watching", users.AnimeWatching) app.Ajax("/login", login.Get) // User profiles diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 7f9672e8..ce329bf3 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -22,7 +22,7 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) Icon("home") a(href=viewUser.WebsiteURL(), target="_blank", rel="nofollow")= viewUser.WebsiteShortURL() - if viewUser.Accounts.Osu.Nick != "" && viewUser.Accounts.Osu.PP >= 1000 + if viewUser.Accounts.Osu.Nick != "" && viewUser.Accounts.Osu.PP >= 100 p.profile-field.osu(title="osu! Level " + toString(int(viewUser.Accounts.Osu.Level)) + " | Accuracy: " + fmt.Sprintf("%.1f", viewUser.Accounts.Osu.Accuracy) + "%") Icon("trophy") a(href="https://osu.ppy.sh/u/" + viewUser.Accounts.Osu.Nick, target="_blank", rel="noopener")= toString(int(viewUser.Accounts.Osu.PP)) + " pp" diff --git a/pages/users/users.go b/pages/users/users.go index a81e9f39..09a3b989 100644 --- a/pages/users/users.go +++ b/pages/users/users.go @@ -8,9 +8,42 @@ import ( "github.com/animenotifier/notify.moe/components" ) -// Get ... -func Get(ctx *aero.Context) string { - users, err := arn.GetActiveUsersCached() +// Active ... +func Active(ctx *aero.Context) string { + users, err := arn.GetListOfUsersCached("active users") + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) + } + + return ctx.HTML(components.Users(users)) +} + +// Osu ... +func Osu(ctx *aero.Context) string { + users, err := arn.GetListOfUsersCached("active osu users") + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) + } + + return ctx.HTML(components.Users(users)) +} + +// Staff ... +func Staff(ctx *aero.Context) string { + users, err := arn.GetListOfUsersCached("active staff users") + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) + } + + return ctx.HTML(components.Users(users)) +} + +// AnimeWatching ... +func AnimeWatching(ctx *aero.Context) string { + users, err := arn.GetListOfUsersCached("active anime watching users") if err != nil { return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) diff --git a/pages/users/users.pixy b/pages/users/users.pixy index 6a25e9cb..a1f4b0c2 100644 --- a/pages/users/users.pixy +++ b/pages/users/users.pixy @@ -1,6 +1,23 @@ component Users(users []*arn.User) h1.page-title Users + .buttons.tabs + a.button.tab.action(href="/users", data-action="diff", data-trigger="click") + Icon("users") + span Active + + a.button.tab.action(href="/users/anime/watching", data-action="diff", data-trigger="click") + Icon("tv") + span Watching + + a.button.tab.action(href="/users/osu", data-action="diff", data-trigger="click") + Icon("gamepad") + span Osu + + a.button.tab.action(href="/users/staff", data-action="diff", data-trigger="click") + Icon("user-secret") + span Staff + .user-avatars each user in users Avatar(user) \ No newline at end of file From 39e6853d7d1f928436d928a4b61f4c4cf2025396 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 13:21:01 +0200 Subject: [PATCH 301/527] Added effects to user lists --- pages/users/users.pixy | 3 ++- scripts/AnimeNotifier.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pages/users/users.pixy b/pages/users/users.pixy index a1f4b0c2..dc83223c 100644 --- a/pages/users/users.pixy +++ b/pages/users/users.pixy @@ -20,4 +20,5 @@ component Users(users []*arn.User) .user-avatars each user in users - Avatar(user) \ No newline at end of file + .mountable + Avatar(user) \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index a22ae953..2b8cafd1 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -468,19 +468,26 @@ export class AnimeNotifier { } modifyDelayed(className: string, func: (element: HTMLElement) => void) { - const delay = 20 const maxDelay = 1000 + const delay = 20 let time = 0 let start = Date.now() let maxTime = start + maxDelay - let collection = document.getElementsByClassName(className) let mutations = [] let mountableTypes = { general: start } + let collection = document.getElementsByClassName(className) + + if(collection.length === 0) { + return + } + + // let delay = Math.min(maxDelay / collection.length, 20) + for(let i = 0; i < collection.length; i++) { let element = collection.item(i) as HTMLElement let type = element.dataset.mountableType || "general" From 6fbe6a34ab12f97c4a4e05bad41d80926961e275 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 16:56:02 +0200 Subject: [PATCH 302/527] Mobile layout --- layout/layout.pixy | 24 ++++++++++++++++++++ mixins/Navigation.pixy | 46 +++++++++++++++++++++++++++++---------- pages/users/users.scarlet | 5 ++++- scripts/Actions.ts | 5 +++++ scripts/AnimeNotifier.ts | 5 +++++ scripts/Application.ts | 5 ++++- styles/navigation.scarlet | 6 +---- styles/sidebar.scarlet | 44 +++++++++++++++++++++++++++++++++++++ 8 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 styles/sidebar.scarlet diff --git a/layout/layout.pixy b/layout/layout.pixy index 313da78e..96d77b14 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -26,10 +26,34 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openG main#content.fade!= content LoadingAnimation StatusMessage + aside#sidebar + Sidebar(user) if user != nil #user(data-id=user.ID) script(src="/scripts") +component Sidebar(user *arn.User) + .user-image-container + if user != nil + Avatar(user) + else + img.user-image.lazy(data-src="/images/brand/64", alt="Anime Notifier") + + SidebarButton("Home", "/", "home") + SidebarButton("Forum", "/forum", "comment") + SidebarButton("Explore", "/explore", "th") + SidebarButton("Soundtracks", "/soundtracks", "headphones") + SidebarButton("Users", "/users", "globe") + + if user != nil + if user.Role != "" + SidebarButton("Statistics", "/statistics", "pie-chart") + + SidebarButton("Settings", "/settings", "cog") + SidebarButtonNoAJAX("Logout", "/logout", "sign-out") + else + SidebarButton("Login", "/login", "sign-in") + component StatusMessage #status-message.fade.fade-out #status-message-text diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 500f525c..046cdb67 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -6,16 +6,20 @@ component Navigation(user *arn.User) component LoggedOutMenu nav#navigation.logged-out - NavigationButton("About", "/", "home") - NavigationButton("Explore", "/explore", "th") - NavigationButton("Forum", "/forum", "comment") + .navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") + .navigation-button + Icon("bars") + span.navigation-text Menu + + //- NavigationButton("Explore", "/explore", "th") + //- NavigationButton("Forum", "/forum", "comment") FuzzySearch - .extra-navigation - NavigationButton("Users", "/users", "globe") + //- .extra-navigation + //- NavigationButton("Users", "/users", "globe") - NavigationButton("Soundtracks", "/soundtracks", "headphones") + //- NavigationButton("Soundtracks", "/soundtracks", "headphones") NavigationButton("Login", "/login", "sign-in") @@ -24,9 +28,16 @@ component LoggedInMenu(user *arn.User) .extension-navigation NavigationButton("Watching list", "/extension/embed", "home") - NavigationButton("Dash", "/", "dashboard") - NavigationButton("Profile", "/+", "user") - NavigationButton("Forum", "/forum", "comment") + .navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") + .navigation-button + Icon("bars") + span.navigation-text Menu + + .extra-navigation + NavigationButton("Profile", "/+", "user") + + .extra-navigation + NavigationButton("Forum", "/forum", "comment") .extra-navigation NavigationButton("Soundtracks", "/soundtracks", "headphones") @@ -36,7 +47,8 @@ component LoggedInMenu(user *arn.User) .extra-navigation NavigationButton("Users", "/users", "globe") - NavigationButton("Explore", "/explore", "th") + .extra-navigation + NavigationButton("Explore", "/explore", "th") //- .extra-navigation //- NavigationButton("Statistics", "/statistics", "pie-chart") @@ -55,8 +67,20 @@ component NavigationButton(name string, target string, icon string) Icon(icon) span.navigation-text= name +component SidebarButton(name string, target string, icon string) + a.sidebar-link.ajax(href=target, aria-label=name, title=name, data-bubble="true") + .sidebar-button + Icon(icon) + span.sidebar-text= name + component NavigationButtonNoAJAX(name string, target string, icon string) a.navigation-link(href=target, aria-label=name) .navigation-button Icon(icon) - span.navigation-text= name \ No newline at end of file + span.navigation-text= name + +component SidebarButtonNoAJAX(name string, target string, icon string) + a.sidebar-link(href=target, aria-label=name, data-bubble="true") + .sidebar-button + Icon(icon) + span.sidebar-text= name \ No newline at end of file diff --git a/pages/users/users.scarlet b/pages/users/users.scarlet index cb7c18ef..141ac59d 100644 --- a/pages/users/users.scarlet +++ b/pages/users/users.scarlet @@ -4,4 +4,7 @@ border-radius 3px .user-image - margin 0.4rem \ No newline at end of file + margin 0.4rem + +.user + display flex \ No newline at end of file diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 66bd6dde..5c77caf0 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -3,6 +3,11 @@ import { AnimeNotifier } from "./AnimeNotifier" import { Diff } from "./Diff" import { findAll } from "./Utils" +// Toggle sidebar +export function toggleSidebar(arn: AnimeNotifier) { + arn.app.find("sidebar").classList.toggle("sidebar-visible") +} + // Save new data from an input field export function save(arn: AnimeNotifier, input: HTMLElement) { arn.loading(true) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 2b8cafd1..63c9a9f9 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -115,6 +115,11 @@ export class AnimeNotifier { // Push manager this.pushManager = new PushManager() + + // Sidebar control + document.body.addEventListener("click", e => { + this.app.find("sidebar").classList.remove("sidebar-visible") + }) } async onContentLoaded() { diff --git a/scripts/Application.ts b/scripts/Application.ts index 4f4057cd..9f86505a 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -160,7 +160,10 @@ export class Application { let url = this.getAttribute("href") e.preventDefault() - e.stopPropagation() + + // if(this.dataset.bubble !== "true") { + // e.stopPropagation() + // } if(!url || url === self.currentPath) return diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index a2204384..cfbf44eb 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -43,7 +43,7 @@ display none #search - display none + flex 1 border-radius 0 background transparent border none @@ -75,10 +75,6 @@ #navigation justify-content flex-start - - #search - display block - flex 1 .extra-navigation display block diff --git a/styles/sidebar.scarlet b/styles/sidebar.scarlet new file mode 100644 index 00000000..6dbe64f2 --- /dev/null +++ b/styles/sidebar.scarlet @@ -0,0 +1,44 @@ +sidebar-spacing-y = 0.75rem + +#sidebar + vertical + position fixed + left 0 + top 0 + min-width 265px + height 100% + background ui-background + transform translateX(-100%) + overflow-x hidden + overflow-y auto + opacity 0 + pointer-events none + box-shadow shadow-medium + transition opacity transition-speed ease, transform transition-speed ease + will-change opacity transition + + .user-image-container + horizontal + justify-content center + margin 0.8rem 0 + +.sidebar-visible + transform translateX(0) !important + pointer-events all !important + opacity 1 !important + +.sidebar-link + // color text-color + &.active + .sidebar-button + background rgb(245, 245, 245) + +.sidebar-button + horizontal + align-items center + padding sidebar-spacing-y content-padding + // background ui-background + + .icon + font-size 1.3rem + margin-right content-padding \ No newline at end of file From 8fb371428a74bf6f7cc4476b7a5342591acb7766 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 17:02:56 +0200 Subject: [PATCH 303/527] Improved mobile layout --- styles/navigation.scarlet | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index cfbf44eb..0398473d 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -1,6 +1,5 @@ #navigation horizontal - padding 0 content-padding overflow hidden background-color nav-color justify-content center @@ -69,6 +68,10 @@ .navigation-button, #search font-size 1.3em +> 550px + #navigation + padding 0 content-padding + > 930px .navigation-button, #search font-size 1.2em From b6ea4d172756724fcf376378284886528f655183 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 17:12:06 +0200 Subject: [PATCH 304/527] Improved landscape mode --- mixins/Navigation.pixy | 8 ++++---- pages/profile/profile.scarlet | 2 +- styles/navigation.scarlet | 8 ++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 046cdb67..f3d74743 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -6,7 +6,7 @@ component Navigation(user *arn.User) component LoggedOutMenu nav#navigation.logged-out - .navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") + #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") .navigation-button Icon("bars") span.navigation-text Menu @@ -28,7 +28,7 @@ component LoggedInMenu(user *arn.User) .extension-navigation NavigationButton("Watching list", "/extension/embed", "home") - .navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") + #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") .navigation-button Icon("bars") span.navigation-text Menu @@ -39,7 +39,7 @@ component LoggedInMenu(user *arn.User) .extra-navigation NavigationButton("Forum", "/forum", "comment") - .extra-navigation + .extra-navigation.hide-landscape NavigationButton("Soundtracks", "/soundtracks", "headphones") FuzzySearch @@ -55,7 +55,7 @@ component LoggedInMenu(user *arn.User) NavigationButton("Settings", "/settings", "cog") - .extra-navigation + .extra-navigation.hide-landscape NavigationButtonNoAJAX("Logout", "/logout", "sign-out") component FuzzySearch diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 74ff58a2..25a4452f 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -23,7 +23,7 @@ profile-boot-duration = 2s a color white -< 600px +< 740px .profile vertical align-items center diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index 0398473d..d67ee9ba 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -82,7 +82,7 @@ .extra-navigation display block -< 380px height +< 400px height #navigation vertical height 100% @@ -92,7 +92,11 @@ horizontal .extra-navigation - display none + display block + + #sidebar-toggle, + .hide-landscape + display none !important #search display none \ No newline at end of file From a3b2c525f0d3dc5e3ab8493401cb15a0352de72f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 17:16:53 +0200 Subject: [PATCH 305/527] Fixed styling mistake --- mixins/Navigation.pixy | 5 +++-- pages/profile/profile.scarlet | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index f3d74743..5d8608cd 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -39,7 +39,7 @@ component LoggedInMenu(user *arn.User) .extra-navigation NavigationButton("Forum", "/forum", "comment") - .extra-navigation.hide-landscape + .extra-navigation NavigationButton("Soundtracks", "/soundtracks", "headphones") FuzzySearch @@ -53,7 +53,8 @@ component LoggedInMenu(user *arn.User) //- .extra-navigation //- NavigationButton("Statistics", "/statistics", "pie-chart") - NavigationButton("Settings", "/settings", "cog") + .hide-landscape + NavigationButton("Settings", "/settings", "cog") .extra-navigation.hide-landscape NavigationButtonNoAJAX("Logout", "/logout", "sign-out") diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 25a4452f..ff90fc30 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -18,7 +18,7 @@ profile-boot-duration = 2s overflow hidden .profile-field - text-align center + text-align left a color white @@ -27,6 +27,9 @@ profile-boot-duration = 2s .profile vertical align-items center + + .profile-field + text-align center .intro-container align-items center From 2945954dd1d7fd521df7cb633250a8743b52e812 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 18:11:15 +0200 Subject: [PATCH 306/527] Improved mobile layout --- styles/navigation.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index d67ee9ba..1820f459 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -82,7 +82,7 @@ .extra-navigation display block -< 400px height +@media screen and (max-device-height: 500px) #navigation vertical height 100% From 6329b7026df5bd075527c1bb3d55e67db2b0156d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 18:23:53 +0200 Subject: [PATCH 307/527] Improved tabs --- pages/statistics/statistics.pixy | 4 ++-- pages/users/users.pixy | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index e5ac3e19..ef8b42ad 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -17,11 +17,11 @@ component StatisticsHeader .buttons.tabs a.button.tab.action(href="/statistics", data-action="diff", data-trigger="click") Icon("user") - span Users + span.tab-text Users a.button.tab.action(href="/statistics/anime", data-action="diff", data-trigger="click") Icon("tv") - span Anime + span.tab-text Anime component PieChart(slices []*arn.PieChartSlice) svg.pie-chart(viewBox="-1.1 -1.1 2.2 2.2") diff --git a/pages/users/users.pixy b/pages/users/users.pixy index dc83223c..33f2f1fd 100644 --- a/pages/users/users.pixy +++ b/pages/users/users.pixy @@ -4,19 +4,19 @@ component Users(users []*arn.User) .buttons.tabs a.button.tab.action(href="/users", data-action="diff", data-trigger="click") Icon("users") - span Active + span.tab-text Active a.button.tab.action(href="/users/anime/watching", data-action="diff", data-trigger="click") Icon("tv") - span Watching + span.tab-text Watching a.button.tab.action(href="/users/osu", data-action="diff", data-trigger="click") Icon("gamepad") - span Osu + span.tab-text Osu a.button.tab.action(href="/users/staff", data-action="diff", data-trigger="click") Icon("user-secret") - span Staff + span.tab-text Staff .user-avatars each user in users From 49fef1e57b597c32f1f7fcb42f5150c895bf187f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 18:47:17 +0200 Subject: [PATCH 308/527] Added touch controller --- scripts/AnimeNotifier.ts | 11 +++++++- scripts/TouchController.ts | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 scripts/TouchController.ts diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 63c9a9f9..66c663a3 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -6,6 +6,7 @@ import { Diff } from "./Diff" import { MutationQueue } from "./MutationQueue" import { StatusMessage } from "./StatusMessage" import { PushManager } from "./PushManager" +import { TouchController } from "./TouchController" export class AnimeNotifier { app: Application @@ -16,6 +17,8 @@ export class AnimeNotifier { statusMessage: StatusMessage visibilityObserver: IntersectionObserver pushManager: PushManager + touchController: TouchController + sideBar: HTMLElement mainPageLoaded: boolean lastReloadContentPath: string @@ -117,9 +120,15 @@ export class AnimeNotifier { this.pushManager = new PushManager() // Sidebar control + this.sideBar = this.app.find("sidebar") + document.body.addEventListener("click", e => { - this.app.find("sidebar").classList.remove("sidebar-visible") + this.sideBar.classList.remove("sidebar-visible") }) + + this.touchController = new TouchController() + this.touchController.leftSwipe = () => this.sideBar.classList.remove("sidebar-visible") + this.touchController.rightSwipe = () => this.sideBar.classList.add("sidebar-visible") } async onContentLoaded() { diff --git a/scripts/TouchController.ts b/scripts/TouchController.ts new file mode 100644 index 00000000..9fd06bf7 --- /dev/null +++ b/scripts/TouchController.ts @@ -0,0 +1,53 @@ +export class TouchController { + x: number + y: number + + threshold: number + + leftSwipe: Function + rightSwipe: Function + upSwipe: Function + downSwipe: Function + + constructor() { + document.addEventListener("touchstart", evt => this.handleTouchStart(evt), false) + document.addEventListener("touchmove", evt => this.handleTouchMove(evt), false) + + this.downSwipe = this.upSwipe = this.rightSwipe = this.leftSwipe = () => null + this.threshold = 5 + } + + handleTouchStart(evt) { + this.x = evt.touches[0].clientX + this.y = evt.touches[0].clientY + } + + handleTouchMove(evt) { + if(!this.x || !this.y) { + return + } + + let xUp = evt.touches[0].clientX + let yUp = evt.touches[0].clientY + + let xDiff = this.x - xUp + let yDiff = this.y - yUp + + if(Math.abs(xDiff) > Math.abs(yDiff)) { + if(xDiff > this.threshold) { + this.leftSwipe() + } else if(xDiff < -this.threshold) { + this.rightSwipe() + } + } else { + if(yDiff > this.threshold) { + this.upSwipe() + } else if(yDiff < -this.threshold) { + this.downSwipe() + } + } + + this.x = undefined + this.y = undefined + } +} \ No newline at end of file From 983c9f4ed67859198366361020e5c31afcff266b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jul 2017 19:13:57 +0200 Subject: [PATCH 309/527] Updated badge --- sw/service-worker.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 07d838f8..6f800784 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -194,7 +194,8 @@ self.addEventListener("push", (evt: PushEvent) => { body: payload.message, icon: payload.icon, image: payload.image, - data: payload.link + data: payload.link, + badge: "https://notify.moe/brand/64" }) ) }) From 43a58b8f46edc36501b7c73d47222c03b3e9f108 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 20 Jul 2017 02:54:53 +0200 Subject: [PATCH 310/527] Added help to menu --- layout/layout.pixy | 42 +----------------------------------- mixins/LoadingAnimation.pixy | 11 ++++++++++ mixins/Sidebar.pixy | 25 +++++++++++++++++++++ mixins/StatusMessage.pixy | 5 +++++ 4 files changed, 42 insertions(+), 41 deletions(-) create mode 100644 mixins/LoadingAnimation.pixy create mode 100644 mixins/Sidebar.pixy create mode 100644 mixins/StatusMessage.pixy diff --git a/layout/layout.pixy b/layout/layout.pixy index 96d77b14..21ce0324 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -30,44 +30,4 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openG Sidebar(user) if user != nil #user(data-id=user.ID) - script(src="/scripts") - -component Sidebar(user *arn.User) - .user-image-container - if user != nil - Avatar(user) - else - img.user-image.lazy(data-src="/images/brand/64", alt="Anime Notifier") - - SidebarButton("Home", "/", "home") - SidebarButton("Forum", "/forum", "comment") - SidebarButton("Explore", "/explore", "th") - SidebarButton("Soundtracks", "/soundtracks", "headphones") - SidebarButton("Users", "/users", "globe") - - if user != nil - if user.Role != "" - SidebarButton("Statistics", "/statistics", "pie-chart") - - SidebarButton("Settings", "/settings", "cog") - SidebarButtonNoAJAX("Logout", "/logout", "sign-out") - else - SidebarButton("Login", "/login", "sign-in") - -component StatusMessage - #status-message.fade.fade-out - #status-message-text - a.status-message-action.action(href="#", data-trigger="click", data-action="closeStatusMessage", aria-label="Close status message") - RawIcon("close") - -component LoadingAnimation - #loading.sk-cube-grid.fade - .sk-cube.hide - .sk-cube - .sk-cube.hide - .sk-cube - .sk-cube.sk-cube-center - .sk-cube - .sk-cube.hide - .sk-cube - .sk-cube.hide \ No newline at end of file + script(src="/scripts") \ No newline at end of file diff --git a/mixins/LoadingAnimation.pixy b/mixins/LoadingAnimation.pixy new file mode 100644 index 00000000..2489c239 --- /dev/null +++ b/mixins/LoadingAnimation.pixy @@ -0,0 +1,11 @@ +component LoadingAnimation + #loading.sk-cube-grid.fade + .sk-cube.hide + .sk-cube + .sk-cube.hide + .sk-cube + .sk-cube.sk-cube-center + .sk-cube + .sk-cube.hide + .sk-cube + .sk-cube.hide \ No newline at end of file diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy new file mode 100644 index 00000000..b26deaee --- /dev/null +++ b/mixins/Sidebar.pixy @@ -0,0 +1,25 @@ +component Sidebar(user *arn.User) + .user-image-container + if user != nil + Avatar(user) + else + img.user-image.lazy(data-src="/images/brand/64", alt="Anime Notifier") + + SidebarButton("Home", "/", "home") + SidebarButton("Forum", "/forum", "comment") + SidebarButton("Explore", "/explore", "th") + SidebarButton("Soundtracks", "/soundtracks", "headphones") + SidebarButton("Users", "/users", "globe") + + if user != nil + if user.Role != "" + SidebarButton("Statistics", "/statistics", "pie-chart") + + SidebarButton("Settings", "/settings", "cog") + + SidebarButton("Help", "/thread/I3MMiOtzR", "question") + + if user != nil + SidebarButtonNoAJAX("Logout", "/logout", "sign-out") + else + SidebarButton("Login", "/login", "sign-in") \ No newline at end of file diff --git a/mixins/StatusMessage.pixy b/mixins/StatusMessage.pixy new file mode 100644 index 00000000..d5397a27 --- /dev/null +++ b/mixins/StatusMessage.pixy @@ -0,0 +1,5 @@ +component StatusMessage + #status-message.fade.fade-out + #status-message-text + a.status-message-action.action(href="#", data-trigger="click", data-action="closeStatusMessage", aria-label="Close status message") + RawIcon("close") \ No newline at end of file From 0e2fec0d1b5213f46f3a510f19298bcbfef4f763 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 20 Jul 2017 12:39:47 +0200 Subject: [PATCH 311/527] Added Firewall --- main.go | 1 + middleware/Firewall.go | 79 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 middleware/Firewall.go diff --git a/main.go b/main.go index 87bd3aa3..d1cdc12a 100644 --- a/main.go +++ b/main.go @@ -142,6 +142,7 @@ func configure(app *aero.Application) *aero.Application { app.Get("/api/paypal/payment/create", paypal.CreatePayment) // Middleware + app.Use(middleware.Firewall()) app.Use(middleware.Log()) app.Use(middleware.Session()) app.Use(middleware.UserInfo()) diff --git a/middleware/Firewall.go b/middleware/Firewall.go new file mode 100644 index 00000000..a92d9004 --- /dev/null +++ b/middleware/Firewall.go @@ -0,0 +1,79 @@ +package middleware + +import ( + "strings" + "time" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/utils" + cache "github.com/patrickmn/go-cache" +) + +const requestThreshold = 10 + +var ipToStats = cache.New(30*time.Minute, 15*time.Minute) + +// IPStats captures the statistics for a single IP. +type IPStats struct { + Requests []string +} + +// Firewall middleware detects malicious requests. +func Firewall() aero.Middleware { + return func(ctx *aero.Context, next func()) { + var stats *IPStats + + ip := ctx.RealIP() + + // Allow localhost + // if ip == "127.0.0.1" { + // next() + // return + // } + + statsObj, found := ipToStats.Get(ip) + + if found { + stats = statsObj.(*IPStats) + } else { + stats = &IPStats{ + Requests: []string{}, + } + + ipToStats.Set(ip, stats, cache.DefaultExpiration) + } + + // Add requested URI to the list of requests + stats.Requests = append(stats.Requests, ctx.URI()) + + if len(stats.Requests) > requestThreshold { + stats.Requests = stats.Requests[len(stats.Requests)-requestThreshold:] + + for _, uri := range stats.Requests { + // Allow request + if strings.Contains(uri, "/_/") || strings.Contains(uri, "/api/") || strings.Contains(uri, "/scripts") || strings.Contains(uri, "/service-worker") || strings.Contains(uri, "/favicon.ico") || strings.Contains(uri, "/extension/embed") { + next() + return + } + } + + // Allow logged in users + if ctx.HasSession() { + user := utils.GetUser(ctx) + + if user != nil { + // Allow request + next() + return + } + } + + // Disallow request + request.Error("[guest]", ip, "BLOCKED BY FIREWALL", ctx.URI()) + return + } + + // Allow the request if the number of requests done by the IP is below the threshold + next() + } +} From ecb9e4ad86919e12100d714b2859468cf36a8c74 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 20 Jul 2017 12:40:26 +0200 Subject: [PATCH 312/527] Allow localhost --- middleware/Firewall.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/middleware/Firewall.go b/middleware/Firewall.go index a92d9004..1929ec8a 100644 --- a/middleware/Firewall.go +++ b/middleware/Firewall.go @@ -26,10 +26,10 @@ func Firewall() aero.Middleware { ip := ctx.RealIP() // Allow localhost - // if ip == "127.0.0.1" { - // next() - // return - // } + if ip == "127.0.0.1" { + next() + return + } statsObj, found := ipToStats.Get(ip) From 5ea14e8fc1eb43be13c356c8572ff68d654c1365 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 20 Jul 2017 14:26:43 +0200 Subject: [PATCH 313/527] Improved search --- jobs/jobs.go | 2 +- jobs/search-index/search-index.go | 54 +++++++++++++++++++++++++------ middleware/Firewall.go | 2 +- pages/animelist/animelist.scarlet | 4 +-- pages/dashboard/dashboard.scarlet | 4 +-- pages/search/search.go | 6 ++-- pages/search/search.pixy | 23 +++++++++++-- pages/search/search.scarlet | 16 +++++++++ scripts/AnimeNotifier.ts | 46 ++++++++++++++------------ scripts/Diff.ts | 4 +-- 10 files changed, 115 insertions(+), 46 deletions(-) diff --git a/jobs/jobs.go b/jobs/jobs.go index 276f5f7c..9f3a86d1 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -31,12 +31,12 @@ var jobs = map[string]time.Duration{ "popular-anime": 20 * time.Minute, "avatars": 30 * time.Minute, "twist": 1 * time.Hour, + "search-index": 2 * time.Hour, "sync-shoboi": 8 * time.Hour, "refresh-episodes": 10 * time.Hour, "refresh-track-titles": 10 * time.Hour, "refresh-osu": 12 * time.Hour, "sync-anime": 12 * time.Hour, - "search-index": 12 * time.Hour, } func main() { diff --git a/jobs/search-index/search-index.go b/jobs/search-index/search-index.go index 273e6807..01cb9ada 100644 --- a/jobs/search-index/search-index.go +++ b/jobs/search-index/search-index.go @@ -12,7 +12,12 @@ import ( func main() { color.Yellow("Updating search index") - flow.Parallel(updateAnimeIndex, updateUserIndex) + flow.Parallel( + updateAnimeIndex, + updateUserIndex, + updatePostIndex, + updateThreadIndex, + ) color.Green("Finished.") } @@ -71,10 +76,7 @@ func updateUserIndex() { // Users userStream, err := arn.StreamUsers() - - if err != nil { - panic(err) - } + arn.PanicOnError(err) for user := range userStream { if user.HasNick() { @@ -86,8 +88,42 @@ func updateUserIndex() { // Save in database err = arn.DB.Set("SearchIndex", "User", userSearchIndex) - - if err != nil { - panic(err) - } + arn.PanicOnError(err) +} + +func updatePostIndex() { + postSearchIndex := arn.NewSearchIndex() + + // Users + postStream, err := arn.StreamPosts() + arn.PanicOnError(err) + + for post := range postStream { + postSearchIndex.TextToID[strings.ToLower(post.Text)] = post.ID + } + + fmt.Println(len(postSearchIndex.TextToID), "posts") + + // Save in database + err = arn.DB.Set("SearchIndex", "Post", postSearchIndex) + arn.PanicOnError(err) +} + +func updateThreadIndex() { + threadSearchIndex := arn.NewSearchIndex() + + // Users + threadStream, err := arn.StreamThreads() + arn.PanicOnError(err) + + for thread := range threadStream { + threadSearchIndex.TextToID[strings.ToLower(thread.Title)] = thread.ID + threadSearchIndex.TextToID[strings.ToLower(thread.Text)] = thread.ID + } + + fmt.Println(len(threadSearchIndex.TextToID)/2, "threads") + + // Save in database + err = arn.DB.Set("SearchIndex", "Thread", threadSearchIndex) + arn.PanicOnError(err) } diff --git a/middleware/Firewall.go b/middleware/Firewall.go index 1929ec8a..0594e57c 100644 --- a/middleware/Firewall.go +++ b/middleware/Firewall.go @@ -11,7 +11,7 @@ import ( const requestThreshold = 10 -var ipToStats = cache.New(30*time.Minute, 15*time.Minute) +var ipToStats = cache.New(15*time.Minute, 15*time.Minute) // IPStats captures the statistics for a single IP. type IPStats struct { diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 8d2d609a..e0a97e10 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -13,9 +13,7 @@ .anime-list-item-name flex 1 - white-space nowrap - text-overflow ellipsis - overflow hidden + clip-long-text .anime-list-item-episodes horizontal diff --git a/pages/dashboard/dashboard.scarlet b/pages/dashboard/dashboard.scarlet index 16566790..ca979fcf 100644 --- a/pages/dashboard/dashboard.scarlet +++ b/pages/dashboard/dashboard.scarlet @@ -1,8 +1,6 @@ .schedule-item-link, .schedule-item-title - white-space nowrap - text-overflow ellipsis - overflow hidden + clip-long-text .schedule-item-link horizontal diff --git a/pages/search/search.go b/pages/search/search.go index c86995f4..67298913 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -8,11 +8,13 @@ import ( const maxUsers = 6 * 6 const maxAnime = 5 * 6 +const maxPosts = 3 +const maxThreads = 3 // Get search page. func Get(ctx *aero.Context) string { term := ctx.Query("q") - userResults, animeResults := arn.Search(term, maxUsers, maxAnime) - return ctx.HTML(components.SearchResults(term, userResults, animeResults)) + userResults, animeResults, postResults, threadResults := arn.Search(term, maxUsers, maxAnime, maxPosts, maxThreads) + return ctx.HTML(components.SearchResults(term, userResults, animeResults, postResults, threadResults)) } diff --git a/pages/search/search.pixy b/pages/search/search.pixy index f7269231..ca32fef6 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -1,4 +1,4 @@ -component SearchResults(term string, users []*arn.User, animeResults []*arn.Anime) +component SearchResults(term string, users []*arn.User, animeResults []*arn.Anime, postResults []*arn.Post, threadResults []*arn.Thread) h1.page-title= "Search: " + term .widgets @@ -32,9 +32,26 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim .widget h3.widget-title Icon("comment") - span Forums + span Forum - p.no-search-results.mountable Forums search coming soon. + if len(postResults) == 0 && len(threadResults) == 0 + p.no-search-results.mountable No posts found. + else + each thread in threadResults + .mountable(data-mountable-type="forum") + .forum-search-result + a.forum-search-result-title.ajax(href=thread.Link())= thread.Title + if thread.Author().HasNick() + .forum-search-result-author= thread.Author().Nick + .forum-search-result-sample= thread.Text + + each post in postResults + .mountable(data-mountable-type="forum") + .forum-search-result + a.forum-search-result-title.ajax(href=post.Link(), data-mountable-type="forum")= post.Thread().Title + if post.Author().HasNick() + .forum-search-result-author= post.Author().Nick + .forum-search-result-sample= post.Text .widget h3.widget-title diff --git a/pages/search/search.scarlet b/pages/search/search.scarlet index 2f0d315d..9a609128 100644 --- a/pages/search/search.scarlet +++ b/pages/search/search.scarlet @@ -2,5 +2,21 @@ width 55px !important height 78px !important +.forum-search-result + horizontal + +.forum-search-result-title + flex 1 + clip-long-text + +.forum-search-result-author + text-align right + opacity 0.5 + +.forum-search-result-sample + clip-long-text + margin-bottom 1rem + opacity 0.8 + .no-search-results text-align left \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 66c663a3..316e6638 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -488,11 +488,9 @@ export class AnimeNotifier { let time = 0 let start = Date.now() let maxTime = start + maxDelay - let mutations = [] - let mountableTypes = { - general: start - } + let mountableTypes = new Map() + let mountableTypeMutations = new Map>() let collection = document.getElementsByClassName(className) @@ -506,43 +504,49 @@ export class AnimeNotifier { let element = collection.item(i) as HTMLElement let type = element.dataset.mountableType || "general" - if(type in mountableTypes) { - time = mountableTypes[type] += delay + if(mountableTypes.has(type)) { + time = mountableTypes.get(type) + delay + mountableTypes.set(type, time) } else { - time = mountableTypes[type] = start + time = start + mountableTypes.set(type, time) + mountableTypeMutations.set(type, []) } if(time > maxTime) { time = maxTime } - mutations.push({ + mountableTypeMutations.get(type).push({ element, time }) } - let mutationIndex = 0 + for(let mountableType of mountableTypeMutations.keys()) { + let mutations = mountableTypeMutations.get(mountableType) + let mutationIndex = 0 - let updateBatch = () => { - let now = Date.now() + let updateBatch = () => { + let now = Date.now() - for(; mutationIndex < mutations.length; mutationIndex++) { - let mutation = mutations[mutationIndex] + for(; mutationIndex < mutations.length; mutationIndex++) { + let mutation = mutations[mutationIndex] - if(mutation.time > now) { - break + if(mutation.time > now) { + break + } + + func(mutation.element) } - func(mutation.element) + if(mutationIndex < mutations.length) { + window.requestAnimationFrame(updateBatch) + } } - if(mutationIndex < mutations.length) { - window.requestAnimationFrame(updateBatch) - } + window.requestAnimationFrame(updateBatch) } - - window.requestAnimationFrame(updateBatch) } diff(url: string) { diff --git a/scripts/Diff.ts b/scripts/Diff.ts index d450c0d9..50afac15 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -23,9 +23,7 @@ export class Diff { } Diff.rootContainer.innerHTML = html.replace("", "") - console.log(aRoot.getElementsByTagName("body")[0]) - console.log(Diff.rootContainer.getElementsByTagName("body")[0]) - Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.rootContainer.getElementsByTagName("body")[0]) + Diff.childNodes(aRoot, Diff.rootContainer) } // childNodes diffs the child nodes of 2 given elements and applies DOM mutations. From b58434eb7d2ce2a79516419ae060d4e480c86f90 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 20 Jul 2017 14:34:46 +0200 Subject: [PATCH 314/527] Fixed page reloads --- scripts/Diff.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 50afac15..b0d01c2f 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -13,7 +13,7 @@ export class Diff { } Diff.container.innerHTML = html - Diff.childNodes(aRoot, Diff.container) + Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.container.getElementsByTagName("body")[0]) } // root will diff the document root element with the given HTML string and apply DOM mutations. From dd520faaf92fd3089b7010b9fab740d599044cc2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 20 Jul 2017 14:36:32 +0200 Subject: [PATCH 315/527] Fixed diff --- scripts/Diff.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index b0d01c2f..88cc90fc 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -13,7 +13,7 @@ export class Diff { } Diff.container.innerHTML = html - Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.container.getElementsByTagName("body")[0]) + Diff.childNodes(aRoot, Diff.container) } // root will diff the document root element with the given HTML string and apply DOM mutations. @@ -23,7 +23,7 @@ export class Diff { } Diff.rootContainer.innerHTML = html.replace("", "") - Diff.childNodes(aRoot, Diff.rootContainer) + Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.rootContainer.getElementsByTagName("body")[0]) } // childNodes diffs the child nodes of 2 given elements and applies DOM mutations. From aeb562b548998eeac194bd566f6916e72df6e1aa Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 05:09:47 +0200 Subject: [PATCH 316/527] Added anime cover images to lists --- config.json | 5 +++++ images/brand/144.png | Bin 0 -> 49182 bytes images/brand/144.webp | Bin 0 -> 13728 bytes pages/animelist/animelist.pixy | 4 ++++ pages/animelist/animelist.scarlet | 25 ++++++++++++++++++++----- styles/embedded.scarlet | 4 ++-- 6 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 images/brand/144.png create mode 100644 images/brand/144.webp diff --git a/config.json b/config.json index 470a98c4..6b208020 100644 --- a/config.json +++ b/config.json @@ -38,6 +38,11 @@ "src": "images/brand/64", "sizes": "64x64" }, + { + "src": "images/brand/144", + "sizes": "144x144", + "type": "image/png" + }, { "src": "images/brand/300", "sizes": "300x300" diff --git a/images/brand/144.png b/images/brand/144.png new file mode 100644 index 0000000000000000000000000000000000000000..1035677340cbbb65158a005259aeb400cd09f38c GIT binary patch literal 49182 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 >K@xoCg!bMxG7X;{lue>-B;@J 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

xcjw@r5Myt-N>+r(s>L7J2J%V}!g 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 B@yY-I literal 0 HcmV?d00001 diff --git a/images/brand/144.webp b/images/brand/144.webp new file mode 100644 index 0000000000000000000000000000000000000000..636af88e48ce2b92376f6bc493216af86054fe17 GIT binary patch literal 13728 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 literal 0 HcmV?d00001 diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 4b26c19e..8b4262dc 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -40,6 +40,10 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User tbody each item in animeList.Items tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) + td.anime-list-item-image-container + a.ajax(href=item.Anime().Link()) + img.anime-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + td.anime-list-item-name a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index e0a97e10..00e34f06 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -11,6 +11,18 @@ .anime-list-item horizontal + td + display flex + align-items center + +.anime-list-item-image-container + padding 0 + +.anime-list-item-image + width 27px + height 39px + border-radius 2px + .anime-list-item-name flex 1 clip-long-text @@ -20,7 +32,7 @@ justify-content flex-end text-align right white-space nowrap - flex-basis 120px + width 130px :hover .plus-episode @@ -48,11 +60,12 @@ .anime-list-item-rating text-align right - flex-basis 70px + justify-content flex-end + width 65px .anime-list-item-actions display none - flex-basis 30px + width 30px // // Beautify icon alignment // .raw-icon @@ -69,11 +82,13 @@ .anime-list-item-airing-date display block text-align right - flex-basis 150px + width 150px + opacity 0.8 + justify-content flex-end < 1100px .anime-list-item-rating - display none + display none !important .fill-screen min-height calc(100vh - nav-height - content-padding * 2 - 1rem - 43px - 23px) \ No newline at end of file diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet index c7bf7e9a..93726479 100644 --- a/styles/embedded.scarlet +++ b/styles/embedded.scarlet @@ -13,8 +13,8 @@ remove-margin = 1.1rem margin-top calc(remove-margin * -1) width calc(100% + remove-margin * 2) - td - padding 0.4rem 0.8rem + // td + // padding 0.4rem 0.8rem .anime-list-owner display none From 75631f261fc6b6c85af92f768f5f592f06f211e6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 05:51:11 +0200 Subject: [PATCH 317/527] Improved mobile list --- pages/animelist/animelist.pixy | 4 +--- pages/animelist/animelist.scarlet | 4 ++-- pages/threads/threads.pixy | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 8b4262dc..741f6bf9 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -64,11 +64,9 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User .action(contenteditable=utils.SameUser(user, viewUser), data-field="Episodes", data-type="number", data-trigger="focusout", data-action="save")= item.Episodes if item.Status == arn.AnimeListStatusWatching .plus-episode.action(data-action="increaseEpisode", data-trigger="click") + + .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(title="Overall rating") .action(contenteditable=utils.SameUser(user, viewUser), data-field="Rating.Overall", data-type="number", data-trigger="focusout", data-action="save")= utils.FormatRating(item.Rating.Overall) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 00e34f06..11ae5c7a 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -76,11 +76,11 @@ display block .anime-list-item-airing-date - display none + display none !important > 700px .anime-list-item-airing-date - display block + display flex !important text-align right width 150px opacity 0.8 diff --git a/pages/threads/threads.pixy b/pages/threads/threads.pixy index 2236af28..09120799 100644 --- a/pages/threads/threads.pixy +++ b/pages/threads/threads.pixy @@ -15,7 +15,7 @@ component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User) Avatar(user) .post-content - textarea#new-reply(placeholder="Reply...") + textarea#new-reply(placeholder="Reply...", aria-label="Reply") .buttons button.action(data-action="forumReply", data-trigger="click") From 06f6fd1e76434cc9744d6fed18c18369757d1293 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 07:01:04 +0200 Subject: [PATCH 318/527] Updated icons --- images/brand/144.png | Bin 49182 -> 52172 bytes images/brand/144.webp | Bin 13728 -> 13538 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/images/brand/144.png b/images/brand/144.png index 1035677340cbbb65158a005259aeb400cd09f38c..b04329006a85d12c44021a9e87acdc7a4a419288 100644 GIT binary patch literal 52172 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 z dLd^%>ha1)!t>L9 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
  • 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 a472)fe$NRJUd+J2z~JfX=d#Wzp$Py7Y}9@L literal 49182 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 >K@xoCg!bMxG7X;{lue>-B;@J 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
    xcjw@r5Myt-N>+r(s>L7J2J%V}!g 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 B@yY-I diff --git a/images/brand/144.webp b/images/brand/144.webp index 636af88e48ce2b92376f6bc493216af86054fe17..ed4dd62867d465442ae00efe97025370de3d35f5 100644 GIT binary patch literal 13538 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 GA*h`x%u?nwFj-rFK*|% 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 literal 13728 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 From 8a72b76d279e26a7eb29e0254ad622c51475001a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 08:09:22 +0200 Subject: [PATCH 319/527] New styles --- pages/animelist/animelist.scarlet | 3 +- pages/profile/profile.go | 18 ++++++++++++ pages/profile/profile.pixy | 5 ++++ pages/profile/profile.scarlet | 46 +++++++++++++++++++------------ scripts/TouchController.ts | 2 +- styles/embedded.scarlet | 3 ++ 6 files changed, 58 insertions(+), 19 deletions(-) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 11ae5c7a..f71df96d 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -19,9 +19,10 @@ padding 0 .anime-list-item-image - width 27px + width 39px height 39px border-radius 2px + object-fit cover .anime-list-item-name flex 1 diff --git a/pages/profile/profile.go b/pages/profile/profile.go index f809b69b..9950a9ec 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -45,5 +45,23 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { }) }) + openGraph := &arn.OpenGraph{ + Tags: map[string]string{ + "og:title": viewUser.Nick, + "og:image": viewUser.LargeAvatar(), + "og:url": "https://" + ctx.App.Config.Domain + viewUser.Link(), + "og:site_name": "notify.moe", + "og:description": viewUser.Tagline, + "og:type": "profile", + "profile:username": viewUser.Nick, + }, + Meta: map[string]string{ + "description": viewUser.Tagline, + "keywords": viewUser.Nick + ",profile", + }, + } + + ctx.Data = openGraph + return ctx.HTML(components.Profile(viewUser, user, animeList, threads, posts, tracks, ctx.URI())) } diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index ce329bf3..733bf09d 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -43,6 +43,11 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) p.profile-field.role Icon("rocket") span= arn.Capitalize(viewUser.Role) + + //- .profile-actions + //- button.action(data-action="followUser", data-trigger="click") + //- Icon("user-plus") + //- span Follow ProfileNavigation(viewUser, uri) diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index ff90fc30..3f34e932 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -1,7 +1,8 @@ profile-boot-duration = 2s .profile - horizontal + vertical + align-items center position relative left calc(content-padding * -1) @@ -18,23 +19,42 @@ profile-boot-duration = 2s overflow hidden .profile-field - text-align left + text-align center + a color white -< 740px +.intro-container + vertical + align-items center + margin-top calc(content-padding * 1.5) + +.profile-actions + horizontal + +> 740px .profile - vertical - align-items center + horizontal + align-items flex-start .profile-field - text-align center + text-align left .intro-container - align-items center - margin-top calc(content-padding * 1.5) - padding-left content-padding + align-items flex-start + margin-top 0 + padding content-padding + padding-top 0 + padding-left calc(content-padding * 2) + max-width 900px + + .profile-actions + vertical + position absolute + top 0 + right 0 + padding content-padding // animation appear // 0% @@ -78,14 +98,6 @@ profile-boot-duration = 2s border-radius 3px overflow hidden -.intro-container - vertical - align-items flex-start - padding content-padding - padding-top 0 - padding-left calc(content-padding * 2) - max-width 900px - #nick margin-bottom 1rem diff --git a/scripts/TouchController.ts b/scripts/TouchController.ts index 9fd06bf7..28805da9 100644 --- a/scripts/TouchController.ts +++ b/scripts/TouchController.ts @@ -14,7 +14,7 @@ export class TouchController { document.addEventListener("touchmove", evt => this.handleTouchMove(evt), false) this.downSwipe = this.upSwipe = this.rightSwipe = this.leftSwipe = () => null - this.threshold = 5 + this.threshold = 3 } handleTouchStart(evt) { diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet index 93726479..e0b1c3a1 100644 --- a/styles/embedded.scarlet +++ b/styles/embedded.scarlet @@ -18,6 +18,9 @@ remove-margin = 1.1rem .anime-list-owner display none + + .anime-list-item-image + width 27px #navigation font-size 0.9rem \ No newline at end of file From 524ab3fff9b3853771232bec3579126faf6be093 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 10:10:48 +0200 Subject: [PATCH 320/527] Added user follows --- mixins/Navigation.pixy | 8 +++--- pages/animelist/animelist.scarlet | 9 ++++--- pages/profile/profile.pixy | 20 +++++++++++--- pages/profile/profile.scarlet | 12 +++++++-- patches/add-follows/add-follows.go | 42 ++++++++++++++++++++++++++++++ scripts/Actions.ts | 16 ++++++++++++ scripts/AnimeNotifier.ts | 16 ++++++++++-- scripts/StatusMessage.ts | 9 +++++-- styles/status-message.scarlet | 6 ++++- tests.go | 6 +++++ 10 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 patches/add-follows/add-follows.go diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 5d8608cd..e612aae7 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -39,8 +39,8 @@ component LoggedInMenu(user *arn.User) .extra-navigation NavigationButton("Forum", "/forum", "comment") - .extra-navigation - NavigationButton("Soundtracks", "/soundtracks", "headphones") + //- .extra-navigation + //- NavigationButton("Soundtracks", "/soundtracks", "headphones") FuzzySearch @@ -56,8 +56,8 @@ component LoggedInMenu(user *arn.User) .hide-landscape NavigationButton("Settings", "/settings", "cog") - .extra-navigation.hide-landscape - NavigationButtonNoAJAX("Logout", "/logout", "sign-out") + //- .extra-navigation.hide-landscape + //- NavigationButtonNoAJAX("Logout", "/logout", "sign-out") component FuzzySearch input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: F") diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index f71df96d..475afdc5 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -65,8 +65,7 @@ width 65px .anime-list-item-actions - display none - width 30px + display none !important // // Beautify icon alignment // .raw-icon @@ -74,7 +73,11 @@ > 740px .anime-list-item-actions - display block + display flex !important + width 30px + + :empty + display none !important .anime-list-item-airing-date display none !important diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 733bf09d..06e529c5 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -44,10 +44,22 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) Icon("rocket") span= arn.Capitalize(viewUser.Role) - //- .profile-actions - //- button.action(data-action="followUser", data-trigger="click") - //- Icon("user-plus") - //- span Follow + if user != nil + .profile-actions + if user.ID != viewUser.ID + if !user.Follows().Contains(viewUser.ID) + button.profile-action.action(data-action="followUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/add", data-view-user-id=viewUser.ID) + Icon("user-plus") + span Follow + else + button.profile-action.action(data-action="unfollowUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/remove", data-view-user-id=viewUser.ID) + Icon("user-times") + span Unfollow + + if user.Role == "admin" || user.Role == "editor" + a.button.profile-action(href="/api/user/" + viewUser.ID) + Icon("search-plus") + span API ProfileNavigation(viewUser, uri) diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 3f34e932..1d377010 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -31,7 +31,15 @@ profile-boot-duration = 2s margin-top calc(content-padding * 1.5) .profile-actions - horizontal + vertical + margin-top content-padding + + :empty + display none + +.profile-action + margin-bottom 0.5rem + text-shadow none !important > 740px .profile @@ -50,11 +58,11 @@ profile-boot-duration = 2s max-width 900px .profile-actions - vertical position absolute top 0 right 0 padding content-padding + margin-top 0 // animation appear // 0% diff --git a/patches/add-follows/add-follows.go b/patches/add-follows/add-follows.go new file mode 100644 index 00000000..5b0923c2 --- /dev/null +++ b/patches/add-follows/add-follows.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Adding user follows to users who don't have one") + + // Get a stream of all users + allUsers, err := arn.StreamUsers() + + if err != nil { + panic(err) + } + + // Iterate over the stream + for user := range allUsers { + // exists, err := arn.DB.Exists("UserFollows", user.ID) + + // if err != nil || exists { + // continue + // } + + fmt.Println(user.Nick) + + follows := &arn.UserFollows{} + follows.UserID = user.ID + follows.Items = user.Following + + err = arn.DB.Set("UserFollows", follows.UserID, follows) + + if err != nil { + color.Red(err.Error()) + } + } + + color.Green("Finished.") +} diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 5c77caf0..901d7871 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -3,6 +3,22 @@ import { AnimeNotifier } from "./AnimeNotifier" import { Diff } from "./Diff" import { findAll } from "./Utils" +// Follow user +export function followUser(arn: AnimeNotifier, elem: HTMLElement) { + return arn.post(elem.dataset.api, elem.dataset.viewUserId) + .then(() => arn.reloadContent()) + .then(() => arn.statusMessage.showInfo("You are now following " + arn.app.find("nick").innerText + ".")) + .catch(err => arn.statusMessage.showError(err)) +} + +// Unfollow user +export function unfollowUser(arn: AnimeNotifier, elem: HTMLElement) { + return arn.post(elem.dataset.api, elem.dataset.viewUserId) + .then(() => arn.reloadContent()) + .then(() => arn.statusMessage.showInfo("You stopped following " + arn.app.find("nick").innerText + ".")) + .catch(err => arn.statusMessage.showError(err)) +} + // Toggle sidebar export function toggleSidebar(arn: AnimeNotifier) { arn.app.find("sidebar").classList.toggle("sidebar-visible") diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 316e6638..22563390 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -580,18 +580,30 @@ export class AnimeNotifier { }) } - post(url, obj) { + post(url, body) { + if(typeof body !== "string") { + body = JSON.stringify(body) + } + + this.loading(true) + return fetch(url, { method: "POST", - body: JSON.stringify(obj), + body, credentials: "same-origin" }) .then(response => response.text()) .then(body => { + this.loading(false) + if(body !== "ok") { throw body } }) + .catch(err => { + this.loading(false) + throw err + }) } scrollTo(target: HTMLElement) { diff --git a/scripts/StatusMessage.ts b/scripts/StatusMessage.ts index 0820b90e..126e30c1 100644 --- a/scripts/StatusMessage.ts +++ b/scripts/StatusMessage.ts @@ -9,7 +9,7 @@ export class StatusMessage { this.text = text } - show(message: string, duration?: number) { + show(message: string, duration: number) { let messageId = String(Date.now()) this.text.innerText = message @@ -27,10 +27,15 @@ export class StatusMessage { } showError(message: string, duration?: number) { - this.show(message, duration) + this.show(message, duration || 4000) this.container.classList.add("error-message") } + showInfo(message: string, duration?: number) { + this.show(message, duration || 2000) + this.container.classList.add("info-message") + } + close() { this.container.classList.add("fade-out") } diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet index e08f83f5..7b0aaf62 100644 --- a/styles/status-message.scarlet +++ b/styles/status-message.scarlet @@ -17,4 +17,8 @@ .error-message color white - background-color hsl(0, 75%, 50%) \ No newline at end of file + background-color hsl(0, 75%, 50%) + +.info-message + color white + background-color forum-tag-hover-color \ No newline at end of file diff --git a/tests.go b/tests.go index e6176bb8..fd13ff74 100644 --- a/tests.go +++ b/tests.go @@ -228,6 +228,12 @@ var interfaceImplementations = map[string][]reflect.Type{ "AnimeList": []reflect.Type{ collection, }, + "PushSubscriptions": []reflect.Type{ + collection, + }, + "UserFollows": []reflect.Type{ + collection, + }, } func init() { From f9fa926644b731dc526d8f036cd6ee8be32d3de4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 10:37:29 +0200 Subject: [PATCH 321/527] Improved follower UI --- pages/profile/profile.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 06e529c5..0b3cca2c 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -59,7 +59,7 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) if user.Role == "admin" || user.Role == "editor" a.button.profile-action(href="/api/user/" + viewUser.ID) Icon("search-plus") - span API + span JSON ProfileNavigation(viewUser, uri) From c0e324cefd55ff29a9c1678dfd0d84967e282025 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 11:01:34 +0200 Subject: [PATCH 322/527] Followers --- main.go | 1 + pages/profile/followers.go | 30 ++++++++++++++++++++++++++++++ pages/profile/followers.pixy | 11 +++++++++++ pages/profile/profile.pixy | 4 ++++ 4 files changed, 46 insertions(+) create mode 100644 pages/profile/followers.go create mode 100644 pages/profile/followers.pixy diff --git a/main.go b/main.go index d1cdc12a..534ed8b5 100644 --- a/main.go +++ b/main.go @@ -97,6 +97,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/posts", profile.GetPostsByUser) app.Ajax("/user/:nick/soundtracks", profile.GetSoundTracksByUser) app.Ajax("/user/:nick/stats", profile.GetStatsByUser) + app.Ajax("/user/:nick/followers", profile.GetFollowers) app.Ajax("/user/:nick/animelist", animelist.Get) app.Ajax("/user/:nick/animelist/watching", animelist.FilterByStatus(arn.AnimeListStatusWatching)) app.Ajax("/user/:nick/animelist/completed", animelist.FilterByStatus(arn.AnimeListStatusCompleted)) diff --git a/pages/profile/followers.go b/pages/profile/followers.go new file mode 100644 index 00000000..ef14813f --- /dev/null +++ b/pages/profile/followers.go @@ -0,0 +1,30 @@ +package profile + +import ( + "net/http" + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// GetFollowers shows the followers of a particular user. +func GetFollowers(ctx *aero.Context) string { + nick := ctx.Get("nick") + viewUser, err := arn.GetUserByNick(nick) + + if err != nil { + return ctx.Error(http.StatusNotFound, "User not found", err) + } + + followers := viewUser.Followers() + + sort.Slice(followers, func(i, j int) bool { + return followers[i].LastSeen > followers[j].LastSeen + }) + + return ctx.HTML(components.ProfileFollowers(followers, viewUser, utils.GetUser(ctx), ctx.URI())) + +} diff --git a/pages/profile/followers.pixy b/pages/profile/followers.pixy new file mode 100644 index 00000000..50e25b72 --- /dev/null +++ b/pages/profile/followers.pixy @@ -0,0 +1,11 @@ +component ProfileFollowers(followers []*arn.User, viewUser *arn.User, user *arn.User, uri string) + ProfileHeader(viewUser, user, uri) + + if len(followers) > 0 + .user-avatars + each user in followers + if user.Nick != "" + .mountable + Avatar(user) + else + p.no-data.mountable= viewUser.Nick + " doesn't have a follower yet." \ No newline at end of file diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 0b3cca2c..85aff0e2 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -88,6 +88,10 @@ component ProfileNavigation(viewUser *arn.User, uri string) a.button.tab.action(href="/+" + viewUser.Nick + "/stats", data-action="diff", data-trigger="click") Icon("area-chart") span.tab-text Stats + + a.button.tab.action(href="/+" + viewUser.Nick + "/followers", data-action="diff", data-trigger="click") + Icon("users") + span.tab-text Followers if strings.Contains(uri, "/animelist") hr From 22fe164f344fe7fd3b1ab2a0fffd7d90fc1f2cea Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 11:25:53 +0200 Subject: [PATCH 323/527] Show friends on anime --- pages/anime/anime.go | 20 +++++++++++++++++++- pages/anime/anime.pixy | 8 +++++++- pages/anime/anime.scarlet | 4 ++++ pages/dashboard/dashboard.go | 2 +- pages/profile/followers.go | 6 +----- pages/profile/followers.pixy | 18 +++++++++++++----- pages/profile/followers.scarlet | 2 ++ 7 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 pages/profile/followers.scarlet diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 9981e74a..f4939a59 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -40,6 +40,24 @@ func Get(ctx *aero.Context) string { } } + // Friends watching + var friends []*arn.User + + if user != nil { + friends = user.Follows().Users() + + deleted := 0 + for i := range friends { + j := i - deleted + if !friends[j].AnimeList().Contains(anime.ID) { + friends = friends[:j+copy(friends[j:], friends[j+1:])] + deleted++ + } + } + + arn.SortUsersLastSeen(friends) + } + // Open Graph description := anime.Summary @@ -70,5 +88,5 @@ func Get(ctx *aero.Context) string { ctx.Data = openGraph - return ctx.HTML(components.Anime(anime, tracks, user, episodesReversed)) + return ctx.HTML(components.Anime(anime, friends, tracks, user, episodesReversed)) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 63f68651..fd43436e 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,4 +1,4 @@ -component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, episodesReversed bool) +component Anime(anime *arn.Anime, friends []*arn.User, tracks []*arn.SoundTrack, user *arn.User, episodesReversed bool) .anime-header(data-id=anime.ID) if anime.Image.Small != "" .anime-image-container @@ -35,6 +35,12 @@ component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User, epis Icon("plus") span Add to collection + if len(friends) > 0 + h3.anime-section-name Friends + + .anime-friends + UserGrid(friends) + h3.anime-section-name Ratings .anime-rating-categories .anime-rating-category(title=toString(anime.Rating.Overall)) diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 00de8545..5f32a9e5 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -92,6 +92,10 @@ .anime-rating-categories vertical +.anime-friends + .user-avatars + justify-content flex-start + .footer font-size 0.8rem opacity 0.7 diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 825473b8..ae40dbc2 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -88,7 +88,7 @@ func dashboard(ctx *aero.Context) string { } followingList = userList.([]*arn.User) - followingList = arn.SortUsersLastSeen(followingList) + arn.SortUsersLastSeen(followingList) if len(followingList) > maxFollowing { followingList = followingList[:maxFollowing] diff --git a/pages/profile/followers.go b/pages/profile/followers.go index ef14813f..3155df4e 100644 --- a/pages/profile/followers.go +++ b/pages/profile/followers.go @@ -2,7 +2,6 @@ package profile import ( "net/http" - "sort" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -20,10 +19,7 @@ func GetFollowers(ctx *aero.Context) string { } followers := viewUser.Followers() - - sort.Slice(followers, func(i, j int) bool { - return followers[i].LastSeen > followers[j].LastSeen - }) + arn.SortUsersLastSeen(followers) return ctx.HTML(components.ProfileFollowers(followers, viewUser, utils.GetUser(ctx), ctx.URI())) diff --git a/pages/profile/followers.pixy b/pages/profile/followers.pixy index 50e25b72..63a2ccbd 100644 --- a/pages/profile/followers.pixy +++ b/pages/profile/followers.pixy @@ -2,10 +2,18 @@ component ProfileFollowers(followers []*arn.User, viewUser *arn.User, user *arn. ProfileHeader(viewUser, user, uri) if len(followers) > 0 - .user-avatars - each user in followers - if user.Nick != "" + UserGrid(followers) + else + p.no-data.mountable= viewUser.Nick + " doesn't have a follower yet." + +component UserGrid(users []*arn.User) + .user-avatars + each user in users + if user.Nick != "" + if user.IsActive() .mountable Avatar(user) - else - p.no-data.mountable= viewUser.Nick + " doesn't have a follower yet." \ No newline at end of file + else + .mountable + .inactive-user + Avatar(user) \ No newline at end of file diff --git a/pages/profile/followers.scarlet b/pages/profile/followers.scarlet new file mode 100644 index 00000000..2550fa2b --- /dev/null +++ b/pages/profile/followers.scarlet @@ -0,0 +1,2 @@ +.inactive-user + opacity 0.25 \ No newline at end of file From 0d9b6330ee750c2d9d9c7913f58005fe8a011a43 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 11:43:54 +0200 Subject: [PATCH 324/527] Show followers' anime list item --- mixins/Avatar.pixy | 5 ++++- pages/anime/anime.go | 10 ++++++++-- pages/anime/anime.pixy | 13 +++++++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/mixins/Avatar.pixy b/mixins/Avatar.pixy index 74bfd4d4..990191cf 100644 --- a/mixins/Avatar.pixy +++ b/mixins/Avatar.pixy @@ -1,5 +1,8 @@ component Avatar(user *arn.User) - a.user.ajax(href="/+" + user.Nick, title=user.Nick) + CustomAvatar(user, user.Link(), user.Nick) + +component CustomAvatar(user *arn.User, link string, title string) + a.user.ajax(href=link, title=title) AvatarNoLink(user) component AvatarNoLink(user *arn.User) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index f4939a59..ea537484 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -42,6 +42,7 @@ func Get(ctx *aero.Context) string { // Friends watching var friends []*arn.User + friendsAnimeListItems := map[*arn.User]*arn.AnimeListItem{} if user != nil { friends = user.Follows().Users() @@ -49,9 +50,14 @@ func Get(ctx *aero.Context) string { deleted := 0 for i := range friends { j := i - deleted - if !friends[j].AnimeList().Contains(anime.ID) { + friendAnimeList := friends[j].AnimeList() + obj, err := friendAnimeList.Get(anime.ID) + + if err != nil { friends = friends[:j+copy(friends[j:], friends[j+1:])] deleted++ + } else { + friendsAnimeListItems[friends[j]] = obj.(*arn.AnimeListItem) } } @@ -88,5 +94,5 @@ func Get(ctx *aero.Context) string { ctx.Data = openGraph - return ctx.HTML(components.Anime(anime, friends, tracks, user, episodesReversed)) + return ctx.HTML(components.Anime(anime, friends, friendsAnimeListItems, tracks, user, episodesReversed)) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index fd43436e..d0c51204 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,4 +1,4 @@ -component Anime(anime *arn.Anime, friends []*arn.User, tracks []*arn.SoundTrack, user *arn.User, episodesReversed bool) +component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, tracks []*arn.SoundTrack, user *arn.User, episodesReversed bool) .anime-header(data-id=anime.ID) if anime.Image.Small != "" .anime-image-container @@ -39,7 +39,16 @@ component Anime(anime *arn.Anime, friends []*arn.User, tracks []*arn.SoundTrack, h3.anime-section-name Friends .anime-friends - UserGrid(friends) + .user-avatars + each friend in friends + if friend.Nick != "" + if friend.IsActive() + .mountable + CustomAvatar(friend, listItems[friend].Link(friend.Nick), friend.Nick + " => " + listItems[friend].Status + " | " + toString(listItems[friend].Episodes) + " eps | " + fmt.Sprintf("%.1f", listItems[friend].Rating.Overall) + " rating") + else + .mountable + .inactive-user + Avatar(friend) h3.anime-section-name Ratings .anime-rating-categories From b7786b1d51cf32fce93e245f9ccf524b228b2284 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 11:46:49 +0200 Subject: [PATCH 325/527] Fixed dashboard --- pages/dashboard/dashboard.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index ae40dbc2..7f40c968 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -30,7 +30,6 @@ func Get(ctx *aero.Context) string { // Render the dashboard. func dashboard(ctx *aero.Context) string { var forumActivity []arn.Postable - var userList interface{} var followingList []*arn.User var soundTracks []*arn.SoundTrack var upcomingEpisodes []*arn.UpcomingEpisode @@ -80,14 +79,7 @@ func dashboard(ctx *aero.Context) string { soundTracks = soundTracks[:maxSoundTracks] } }, func() { - var err error - userList, err = arn.DB.GetMany("User", user.Following) - - if err != nil { - return - } - - followingList = userList.([]*arn.User) + followingList = user.Follows().Users() arn.SortUsersLastSeen(followingList) if len(followingList) > maxFollowing { From f30056363b9f834e4c771d24bb95fe4e546603d8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 11:49:28 +0200 Subject: [PATCH 326/527] Fixed inactive users --- pages/anime/anime.pixy | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index d0c51204..974f6968 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -44,11 +44,11 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* if friend.Nick != "" if friend.IsActive() .mountable - CustomAvatar(friend, listItems[friend].Link(friend.Nick), friend.Nick + " => " + listItems[friend].Status + " | " + toString(listItems[friend].Episodes) + " eps | " + fmt.Sprintf("%.1f", listItems[friend].Rating.Overall) + " rating") + FriendEntry(friend, listItems) else .mountable .inactive-user - Avatar(friend) + FriendEntry(friend, listItems) h3.anime-section-name Ratings .anime-rating-categories @@ -235,4 +235,7 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* //- ul.anime-synonyms //- li.anime-japanese-title= anime.title.japanese //- each synonym in anime.title.synonyms - //- li= synonym \ No newline at end of file + //- li= synonym + +component FriendEntry(friend *arn.User, listItems map[*arn.User]*arn.AnimeListItem) + CustomAvatar(friend, listItems[friend].Link(friend.Nick), friend.Nick + " => " + listItems[friend].Status + " | " + toString(listItems[friend].Episodes) + " eps | " + fmt.Sprintf("%.1f", listItems[friend].Rating.Overall) + " rating") From 1cf023e4762c6024c36b05b3530ac56f06558de4 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 12:55:36 +0200 Subject: [PATCH 327/527] Some fixes --- pages/profile/profile.pixy | 12 +++++++----- scripts/AnimeNotifier.ts | 5 ++++- scripts/Application.ts | 12 +++++++----- scripts/StatusMessage.ts | 7 +++++++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 85aff0e2..0226f6d9 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -55,11 +55,6 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) button.profile-action.action(data-action="unfollowUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/remove", data-view-user-id=viewUser.ID) Icon("user-times") span Unfollow - - if user.Role == "admin" || user.Role == "editor" - a.button.profile-action(href="/api/user/" + viewUser.ID) - Icon("search-plus") - span JSON ProfileNavigation(viewUser, uri) @@ -131,6 +126,13 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") img.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + if user != nil && (user.Role == "admin" || user.Role == "editor") + .footer + .buttons + a.button.profile-action(href="/api/user/" + viewUser.ID, target="_blank", rel="noopener") + Icon("search-plus") + span JSON + //- .profile-category.mountable //- h3 //- a.ajax(href="/+" + viewUser.Nick + "/threads", title="View all threads") Threads diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 22563390..db588825 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -68,6 +68,10 @@ export class AnimeNotifier { } init() { + // App init + this.app.init() + + // Event listeners document.addEventListener("readystatechange", this.onReadyStateChange.bind(this)) document.addEventListener("DOMContentLoaded", this.onContentLoaded.bind(this)) document.addEventListener("keydown", this.onKeyDown.bind(this), false) @@ -573,7 +577,6 @@ export class AnimeNotifier { return delay(300).then(() => { return request .then(html => this.app.setContent(html, true)) - .then(() => this.app.markActiveLinks()) .then(() => this.app.emit("DOMContentLoaded")) .then(() => this.loading(false)) .catch(console.error) diff --git a/scripts/Application.ts b/scripts/Application.ts index 9f86505a..3c16c134 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -23,9 +23,14 @@ export class Application { this.fadeOutClass = "fade-out" } + init() { + document.addEventListener("DOMContentLoaded", () => { + this.ajaxify() + this.markActiveLinks() + }) + } + run() { - this.ajaxify() - this.markActiveLinks() this.loading.classList.add(this.fadeOutClass) } @@ -118,9 +123,6 @@ export class Application { } else { this.content.innerHTML = html } - - this.ajaxify(this.content) - this.markActiveLinks(this.content) } markActiveLinks(element?: HTMLElement) { diff --git a/scripts/StatusMessage.ts b/scripts/StatusMessage.ts index 126e30c1..adc81497 100644 --- a/scripts/StatusMessage.ts +++ b/scripts/StatusMessage.ts @@ -26,12 +26,19 @@ export class StatusMessage { }) } + clearStyle() { + this.container.classList.remove("info-message") + this.container.classList.remove("error-message") + } + showError(message: string, duration?: number) { + this.clearStyle() this.show(message, duration || 4000) this.container.classList.add("error-message") } showInfo(message: string, duration?: number) { + this.clearStyle() this.show(message, duration || 2000) this.container.classList.add("info-message") } From e9e0aa89eb9effb19350ab4b25dd787463863d08 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 21 Jul 2017 16:33:29 +0200 Subject: [PATCH 328/527] Added Open Graph to soundtracks --- pages/tracks/tracks.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pages/tracks/tracks.go b/pages/tracks/tracks.go index dd698342..8702f344 100644 --- a/pages/tracks/tracks.go +++ b/pages/tracks/tracks.go @@ -17,5 +17,15 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Track not found", err) } + ctx.Data = &arn.OpenGraph{ + Tags: map[string]string{ + "og:title": track.Media[0].Title, + "og:image": track.Anime()[0].Image.Large, + "og:url": "https://" + ctx.App.Config.Domain + track.Link(), + "og:site_name": "notify.moe", + "og:type": "music.song", + }, + } + return ctx.HTML(components.Track(track)) } From 16cbd5167fc7ad55da3d2cdf69c84bce2bee8e9c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 22 Jul 2017 15:04:54 +0200 Subject: [PATCH 329/527] Redesign --- layout/layout.pixy | 17 ++-- main.go | 8 +- mixins/ForumTags.pixy | 4 +- mixins/Navigation.pixy | 20 ++--- mixins/Sidebar.pixy | 4 + mixins/StatusTabs.pixy | 21 +++++ pages/amvs/amvs.go | 10 +++ pages/anime/anime.scarlet | 2 +- pages/animelist/animelist.go | 2 +- pages/animelist/animelist.pixy | 13 +-- pages/animelist/animelist.scarlet | 2 +- pages/animelist/status.go | 2 +- pages/animelist/status.pixy | 7 +- pages/artworks/artworks.go | 10 +++ pages/dashboard/dashboard.go | 14 +-- pages/home/home.go | 31 +++++++ pages/profile/profile.pixy | 38 ++------ pages/statistics/statistics.pixy | 6 +- pages/users/users.pixy | 10 +-- scripts/AnimeNotifier.ts | 144 +++++++++++++++--------------- styles/base.scarlet | 6 +- styles/include/config.scarlet | 11 +-- styles/layout.scarlet | 6 +- styles/mountable.scarlet | 14 +-- styles/navigation.scarlet | 6 +- styles/sidebar.scarlet | 26 ++++-- styles/table.scarlet | 2 +- styles/tabs.scarlet | 16 +++- utils/ItemCSSClass.go | 14 --- 29 files changed, 262 insertions(+), 204 deletions(-) create mode 100644 mixins/StatusTabs.pixy create mode 100644 pages/amvs/amvs.go create mode 100644 pages/artworks/artworks.go create mode 100644 pages/home/home.go delete mode 100644 utils/ItemCSSClass.go diff --git a/layout/layout.pixy b/layout/layout.pixy index 21ce0324..b8241502 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -20,14 +20,15 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openG link(rel="manifest", href="/manifest.json") body #container(class=utils.GetContainerClass(ctx)) - #header - Navigation(user) - #content-container - main#content.fade!= content - LoadingAnimation - StatusMessage - aside#sidebar - Sidebar(user) + //- #header + //- Navigation(user) + #columns + aside#sidebar + Sidebar(user) + #content-container + main#content.fade!= content + LoadingAnimation + StatusMessage if user != nil #user(data-id=user.ID) script(src="/scripts") \ No newline at end of file diff --git a/main.go b/main.go index 534ed8b5..4cdb14ed 100644 --- a/main.go +++ b/main.go @@ -10,19 +10,21 @@ import ( "github.com/animenotifier/notify.moe/layout" "github.com/animenotifier/notify.moe/middleware" "github.com/animenotifier/notify.moe/pages/admin" + "github.com/animenotifier/notify.moe/pages/amvs" "github.com/animenotifier/notify.moe/pages/anime" "github.com/animenotifier/notify.moe/pages/animelist" "github.com/animenotifier/notify.moe/pages/animelistitem" "github.com/animenotifier/notify.moe/pages/apiview" + "github.com/animenotifier/notify.moe/pages/artworks" "github.com/animenotifier/notify.moe/pages/best" "github.com/animenotifier/notify.moe/pages/character" - "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/editor" "github.com/animenotifier/notify.moe/pages/embed" "github.com/animenotifier/notify.moe/pages/explore" "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" + "github.com/animenotifier/notify.moe/pages/home" "github.com/animenotifier/notify.moe/pages/listimport" "github.com/animenotifier/notify.moe/pages/listimport/listimportanilist" "github.com/animenotifier/notify.moe/pages/listimport/listimportkitsu" @@ -68,7 +70,7 @@ func configure(app *aero.Application) *aero.Application { app.Layout = layout.Render // Ajax routes - app.Ajax("/", dashboard.Get) + app.Ajax("/", home.Get) app.Ajax("/anime/:id", anime.Get) app.Ajax("/anime/:id/edit", editanime.Get) app.Ajax("/api", apiview.Get) @@ -84,6 +86,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) app.Ajax("/soundtracks", music.Get) + app.Ajax("/artworks", artworks.Get) + app.Ajax("/amvs", amvs.Get) app.Ajax("/users", users.Active) app.Ajax("/users/osu", users.Osu) app.Ajax("/users/staff", users.Staff) diff --git a/mixins/ForumTags.pixy b/mixins/ForumTags.pixy index 44f40672..d89a59be 100644 --- a/mixins/ForumTags.pixy +++ b/mixins/ForumTags.pixy @@ -1,5 +1,5 @@ component ForumTags - .buttons.tabs + .tabs ForumTag("All", "", "list") ForumTag("General", "general", "list") ForumTag("News", "news", "list") @@ -9,6 +9,6 @@ component ForumTags ForumTag("Bugs", "bug", "list") component ForumTag(title string, category string, icon string) - a.button.tab.action(href=strings.TrimSuffix("/forum/" + category, "/"), data-action="diff", data-trigger="click") + a.tab.action(href=strings.TrimSuffix("/forum/" + category, "/"), data-action="diff", data-trigger="click") Icon(arn.GetForumIcon(category)) span.tab-text= title \ No newline at end of file diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index e612aae7..c4548ab6 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -33,28 +33,28 @@ component LoggedInMenu(user *arn.User) Icon("bars") span.navigation-text Menu - .extra-navigation - NavigationButton("Profile", "/+", "user") + //- .extra-navigation + //- NavigationButton("Profile", "/+", "user") - .extra-navigation - NavigationButton("Forum", "/forum", "comment") + //- .extra-navigation + //- NavigationButton("Forum", "/forum", "comment") //- .extra-navigation //- NavigationButton("Soundtracks", "/soundtracks", "headphones") FuzzySearch - .extra-navigation - NavigationButton("Users", "/users", "globe") + //- .extra-navigation + //- NavigationButton("Users", "/users", "globe") - .extra-navigation - NavigationButton("Explore", "/explore", "th") + //- .extra-navigation + //- NavigationButton("Explore", "/explore", "th") //- .extra-navigation //- NavigationButton("Statistics", "/statistics", "pie-chart") - .hide-landscape - NavigationButton("Settings", "/settings", "cog") + //- .hide-landscape + //- NavigationButton("Settings", "/settings", "cog") //- .extra-navigation.hide-landscape //- NavigationButtonNoAJAX("Logout", "/logout", "sign-out") diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index b26deaee..7b1d0ee5 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -8,7 +8,9 @@ component Sidebar(user *arn.User) SidebarButton("Home", "/", "home") SidebarButton("Forum", "/forum", "comment") SidebarButton("Explore", "/explore", "th") + SidebarButton("Artworks", "/artworks", "paint-brush") SidebarButton("Soundtracks", "/soundtracks", "headphones") + SidebarButton("AMVs", "/amvs", "video-camera") SidebarButton("Users", "/users", "globe") if user != nil @@ -17,6 +19,8 @@ component Sidebar(user *arn.User) SidebarButton("Settings", "/settings", "cog") + .spacer + SidebarButton("Help", "/thread/I3MMiOtzR", "question") if user != nil diff --git a/mixins/StatusTabs.pixy b/mixins/StatusTabs.pixy new file mode 100644 index 00000000..a526a69b --- /dev/null +++ b/mixins/StatusTabs.pixy @@ -0,0 +1,21 @@ +component StatusTabs(urlPrefix string) + .tabs.status-tabs + a.tab.status-tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") + Icon("play") + span.tab-text Watching + + a.tab.status-tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") + Icon("check") + span.tab-text Completed + + a.tab.status-tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") + Icon("forward") + span.tab-text Planned + + a.tab.status-tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") + Icon("pause") + span.tab-text On Hold + + a.tab.status-tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") + Icon("stop") + span.tab-text Dropped \ No newline at end of file diff --git a/pages/amvs/amvs.go b/pages/amvs/amvs.go new file mode 100644 index 00000000..f90db340 --- /dev/null +++ b/pages/amvs/amvs.go @@ -0,0 +1,10 @@ +package amvs + +import ( + "github.com/aerogo/aero" +) + +// Get AMVs. +func Get(ctx *aero.Context) string { + return ctx.HTML("Coming soon™.") +} diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 5f32a9e5..cde6a43c 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -14,7 +14,7 @@ align-items flex-start .anime-cover-image - width 230px + width 142px height auto border-radius 3px diff --git a/pages/animelist/animelist.go b/pages/animelist/animelist.go index c5daf656..71198c7c 100644 --- a/pages/animelist/animelist.go +++ b/pages/animelist/animelist.go @@ -28,5 +28,5 @@ func Get(ctx *aero.Context) string { animeList.PrefetchAnime() animeList.Sort() - return ctx.HTML(components.AnimeLists(animeList.SplitByStatus(), animeList.User(), user, ctx.URI())) + return ctx.HTML(components.ProfileAnimeLists(animeList.SplitByStatus(), animeList.User(), user, ctx.URI())) } diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 741f6bf9..96de01db 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -1,8 +1,15 @@ -component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User, uri string) +component ProfileAnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User, uri string) ProfileHeader(viewUser, user, uri) h1.page-title.anime-list-owner= viewUser.Nick + "'s collection" + AnimeLists(animeLists, viewUser, user) + + //- for status, animeList := range animeLists + //- h3= status + //- AnimeList(animeList, user) + +component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, user *arn.User) if len(animeLists[arn.AnimeListStatusWatching].Items) == 0 && len(animeLists[arn.AnimeListStatusCompleted].Items) == 0 && len(animeLists[arn.AnimeListStatusPlanned].Items) == 0 && len(animeLists[arn.AnimeListStatusHold].Items) == 0 && len(animeLists[arn.AnimeListStatusDropped].Items) == 0 p.no-data.mountable= viewUser.Nick + " hasn't added any anime yet." else @@ -30,10 +37,6 @@ component AnimeLists(animeLists map[string]*arn.AnimeList, viewUser *arn.User, u .anime-list-container h3.status-name Dropped AnimeList(animeLists[arn.AnimeListStatusDropped], viewUser, user) - - //- for status, animeList := range animeLists - //- h3= status - //- AnimeList(animeList, user) component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User) table.anime-list diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 475afdc5..82ed74f2 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -2,7 +2,7 @@ vertical width 100% max-width table-width-normal - margin 0 auto + // margin 0 auto margin-bottom 1rem .anime-list diff --git a/pages/animelist/status.go b/pages/animelist/status.go index cf66fd4a..567fe1af 100644 --- a/pages/animelist/status.go +++ b/pages/animelist/status.go @@ -19,7 +19,7 @@ func FilterByStatus(status string) aero.Handle { return response } - return ctx.HTML(components.AnimeListFilteredByStatus(list, list.User(), user, status, ctx.URI())) + return ctx.HTML(components.ProfileAnimeListFilteredByStatus(list, list.User(), user, status, ctx.URI())) } } diff --git a/pages/animelist/status.pixy b/pages/animelist/status.pixy index 4b30fa3b..ff66a1c7 100644 --- a/pages/animelist/status.pixy +++ b/pages/animelist/status.pixy @@ -1,9 +1,12 @@ -component AnimeListFilteredByStatus(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User, status string, uri string) +component ProfileAnimeListFilteredByStatus(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User, status string, uri string) ProfileHeader(viewUser, user, uri) + AnimeListFilteredByStatus(animeList, viewUser, user, status) + +component AnimeListFilteredByStatus(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User, status string) if len(animeList.Items) == 0 p.no-data.mountable= viewUser.Nick + " hasn't added any anime to this list yet." else .anime-list-container.fill-screen - h3.status-name= arn.ListItemStatusName(status) + //- h3.status-name= arn.ListItemStatusName(status) AnimeList(animeList, viewUser, user) \ No newline at end of file diff --git a/pages/artworks/artworks.go b/pages/artworks/artworks.go new file mode 100644 index 00000000..58b0893e --- /dev/null +++ b/pages/artworks/artworks.go @@ -0,0 +1,10 @@ +package artworks + +import ( + "github.com/aerogo/aero" +) + +// Get artworks. +func Get(ctx *aero.Context) string { + return ctx.HTML("Coming soon™.") +} diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 7f40c968..fe0649ae 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -7,7 +7,6 @@ import ( "github.com/aerogo/flow" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" - "github.com/animenotifier/notify.moe/pages/frontpage" "github.com/animenotifier/notify.moe/utils" ) @@ -16,19 +15,8 @@ const maxFollowing = 5 const maxSoundTracks = 5 const maxScheduleItems = 5 -// Get the dashboard or the frontpage when logged out. +// Get the dashboard. func Get(ctx *aero.Context) string { - user := utils.GetUser(ctx) - - if user == nil { - return frontpage.Get(ctx) - } - - return dashboard(ctx) -} - -// Render the dashboard. -func dashboard(ctx *aero.Context) string { var forumActivity []arn.Postable var followingList []*arn.User var soundTracks []*arn.SoundTrack diff --git a/pages/home/home.go b/pages/home/home.go new file mode 100644 index 00000000..8de999c3 --- /dev/null +++ b/pages/home/home.go @@ -0,0 +1,31 @@ +package home + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/pages/frontpage" + "github.com/animenotifier/notify.moe/utils" +) + +// Get the dashboard or the frontpage when logged out. +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return frontpage.Get(ctx) + } + + viewUser := user + animeList := viewUser.AnimeList() + + if animeList == nil { + return ctx.Error(http.StatusNotFound, "Anime list not found", nil) + } + + animeList.PrefetchAnime() + animeList.Sort() + + return ctx.HTML(components.AnimeLists(animeList.SplitByStatus(), animeList.User(), user)) +} diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 0226f6d9..2ff3b5cc 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -59,60 +59,38 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) ProfileNavigation(viewUser, uri) component ProfileNavigation(viewUser *arn.User, uri string) - .buttons.tabs - a.button.tab.action(href="/+" + viewUser.Nick, data-action="diff", data-trigger="click") + .tabs + a.tab.action(href="/+" + viewUser.Nick, data-action="diff", data-trigger="click") Icon("th") span.tab-text Anime - a.button.tab.action(href="/+" + viewUser.Nick + "/animelist/watching", data-action="diff", data-trigger="click") + a.tab.action(href="/+" + viewUser.Nick + "/animelist/watching", data-action="diff", data-trigger="click") Icon("list") span.tab-text Collection - a.button.tab.action(href="/+" + viewUser.Nick + "/threads", data-action="diff", data-trigger="click") + a.tab.action(href="/+" + viewUser.Nick + "/threads", data-action="diff", data-trigger="click") Icon("comment") span.tab-text Threads - a.button.tab.action(href="/+" + viewUser.Nick + "/posts", data-action="diff", data-trigger="click") + a.tab.action(href="/+" + viewUser.Nick + "/posts", data-action="diff", data-trigger="click") Icon("comments") span.tab-text Posts - a.button.tab.action(href="/+" + viewUser.Nick + "/soundtracks", data-action="diff", data-trigger="click") + a.tab.action(href="/+" + viewUser.Nick + "/soundtracks", data-action="diff", data-trigger="click") Icon("music") span.tab-text Tracks - a.button.tab.action(href="/+" + viewUser.Nick + "/stats", data-action="diff", data-trigger="click") + a.tab.action(href="/+" + viewUser.Nick + "/stats", data-action="diff", data-trigger="click") Icon("area-chart") span.tab-text Stats - a.button.tab.action(href="/+" + viewUser.Nick + "/followers", data-action="diff", data-trigger="click") + a.tab.action(href="/+" + viewUser.Nick + "/followers", data-action="diff", data-trigger="click") Icon("users") span.tab-text Followers if strings.Contains(uri, "/animelist") hr StatusTabs("/+" + viewUser.Nick + "/animelist") - -component StatusTabs(urlPrefix string) - .buttons.tabs.status-tabs - a.button.tab.status-tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") - Icon("play") - span.tab-text Watching - - a.button.tab.status-tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") - Icon("check") - span.tab-text Completed - - a.button.tab.status-tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") - Icon("forward") - span.tab-text Planned - - a.button.tab.status-tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") - Icon("pause") - span.tab-text On Hold - - a.button.tab.status-tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") - Icon("stop") - span.tab-text Dropped component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack, uri string) ProfileHeader(viewUser, user, uri) diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index ef8b42ad..c9fad809 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -14,12 +14,12 @@ component Statistics(pieCharts ...*arn.PieChart) p Data is collected for statistical purposes only. We respect user privacy and we will never display or sell critical data to 3rd party services. component StatisticsHeader - .buttons.tabs - a.button.tab.action(href="/statistics", data-action="diff", data-trigger="click") + .tabs + a.tab.action(href="/statistics", data-action="diff", data-trigger="click") Icon("user") span.tab-text Users - a.button.tab.action(href="/statistics/anime", data-action="diff", data-trigger="click") + a.tab.action(href="/statistics/anime", data-action="diff", data-trigger="click") Icon("tv") span.tab-text Anime diff --git a/pages/users/users.pixy b/pages/users/users.pixy index 33f2f1fd..5dbfd80a 100644 --- a/pages/users/users.pixy +++ b/pages/users/users.pixy @@ -1,20 +1,20 @@ component Users(users []*arn.User) h1.page-title Users - .buttons.tabs - a.button.tab.action(href="/users", data-action="diff", data-trigger="click") + .tabs + a.tab.action(href="/users", data-action="diff", data-trigger="click") Icon("users") span.tab-text Active - a.button.tab.action(href="/users/anime/watching", data-action="diff", data-trigger="click") + a.tab.action(href="/users/anime/watching", data-action="diff", data-trigger="click") Icon("tv") span.tab-text Watching - a.button.tab.action(href="/users/osu", data-action="diff", data-trigger="click") + a.tab.action(href="/users/osu", data-action="diff", data-trigger="click") Icon("gamepad") span.tab-text Osu - a.button.tab.action(href="/users/staff", data-action="diff", data-trigger="click") + a.tab.action(href="/users/staff", data-action="diff", data-trigger="click") Icon("user-secret") span.tab-text Staff diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index db588825..5eb56c9d 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -24,7 +24,7 @@ export class AnimeNotifier { imageFound: MutationQueue imageNotFound: MutationQueue - unmount: MutationQueue + // unmount: MutationQueue constructor(app: Application) { this.app = app @@ -33,7 +33,7 @@ export class AnimeNotifier { this.imageFound = new MutationQueue(elem => elem.classList.add("image-found")) this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found")) - this.unmount = new MutationQueue(elem => elem.classList.remove("mounted")) + // this.unmount = new MutationQueue(elem => elem.classList.remove("mounted")) // These classes will never be removed on DOM diffs Diff.persistentClasses.add("mounted") @@ -140,7 +140,7 @@ export class AnimeNotifier { this.visibilityObserver.disconnect() this.contentLoadedActions = Promise.all([ - Promise.resolve().then(() => this.mountMountables()), + // Promise.resolve().then(() => this.mountMountables()), Promise.resolve().then(() => this.lazyLoadImages()), Promise.resolve().then(() => this.displayLocalDates()), Promise.resolve().then(() => this.setSelectBoxValue()), @@ -471,87 +471,87 @@ export class AnimeNotifier { this.visibilityObserver.observe(img) } - mountMountables() { - this.modifyDelayed("mountable", element => element.classList.add("mounted")) - } + // mountMountables() { + // this.modifyDelayed("mountable", element => element.classList.add("mounted")) + // } - unmountMountables() { - for(let element of findAll("mountable")) { - if(element.classList.contains("never-unmount")) { - continue - } + // unmountMountables() { + // for(let element of findAll("mountable")) { + // if(element.classList.contains("never-unmount")) { + // continue + // } - this.unmount.queue(element) - } - } + // this.unmount.queue(element) + // } + // } - modifyDelayed(className: string, func: (element: HTMLElement) => void) { - const maxDelay = 1000 - const delay = 20 + // modifyDelayed(className: string, func: (element: HTMLElement) => void) { + // const maxDelay = 1000 + // const delay = 20 - let time = 0 - let start = Date.now() - let maxTime = start + maxDelay + // let time = 0 + // let start = Date.now() + // let maxTime = start + maxDelay - let mountableTypes = new Map() - let mountableTypeMutations = new Map>() + // let mountableTypes = new Map() + // let mountableTypeMutations = new Map>() - let collection = document.getElementsByClassName(className) + // let collection = document.getElementsByClassName(className) - if(collection.length === 0) { - return - } + // if(collection.length === 0) { + // return + // } - // let delay = Math.min(maxDelay / collection.length, 20) + // // let delay = Math.min(maxDelay / collection.length, 20) - for(let i = 0; i < collection.length; i++) { - let element = collection.item(i) as HTMLElement - let type = element.dataset.mountableType || "general" + // for(let i = 0; i < collection.length; i++) { + // let element = collection.item(i) as HTMLElement + // let type = element.dataset.mountableType || "general" - if(mountableTypes.has(type)) { - time = mountableTypes.get(type) + delay - mountableTypes.set(type, time) - } else { - time = start - mountableTypes.set(type, time) - mountableTypeMutations.set(type, []) - } + // if(mountableTypes.has(type)) { + // time = mountableTypes.get(type) + delay + // mountableTypes.set(type, time) + // } else { + // time = start + // mountableTypes.set(type, time) + // mountableTypeMutations.set(type, []) + // } - if(time > maxTime) { - time = maxTime - } + // if(time > maxTime) { + // time = maxTime + // } - mountableTypeMutations.get(type).push({ - element, - time - }) - } + // mountableTypeMutations.get(type).push({ + // element, + // time + // }) + // } - for(let mountableType of mountableTypeMutations.keys()) { - let mutations = mountableTypeMutations.get(mountableType) - let mutationIndex = 0 + // for(let mountableType of mountableTypeMutations.keys()) { + // let mutations = mountableTypeMutations.get(mountableType) + // let mutationIndex = 0 - let updateBatch = () => { - let now = Date.now() + // let updateBatch = () => { + // let now = Date.now() - for(; mutationIndex < mutations.length; mutationIndex++) { - let mutation = mutations[mutationIndex] + // for(; mutationIndex < mutations.length; mutationIndex++) { + // let mutation = mutations[mutationIndex] - if(mutation.time > now) { - break - } + // if(mutation.time > now) { + // break + // } - func(mutation.element) - } + // func(mutation.element) + // } - if(mutationIndex < mutations.length) { - window.requestAnimationFrame(updateBatch) - } - } + // if(mutationIndex < mutations.length) { + // window.requestAnimationFrame(updateBatch) + // } + // } - window.requestAnimationFrame(updateBatch) - } - } + // window.requestAnimationFrame(updateBatch) + // } + // } diff(url: string) { if(url === this.app.currentPath) { @@ -570,17 +570,15 @@ export class AnimeNotifier { history.pushState(url, null, url) this.app.currentPath = url this.app.markActiveLinks() - this.unmountMountables() + // this.unmountMountables() this.loading(true) // Delay by transition-speed - return delay(300).then(() => { - return request - .then(html => this.app.setContent(html, true)) - .then(() => this.app.emit("DOMContentLoaded")) - .then(() => this.loading(false)) - .catch(console.error) - }) + return request + .then(html => this.app.setContent(html, true)) + .then(() => this.app.emit("DOMContentLoaded")) + .then(() => this.loading(false)) + .catch(console.error) } post(url, body) { diff --git a/styles/base.scarlet b/styles/base.scarlet index c2eb31d3..a2f45946 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -1,10 +1,7 @@ html height 100% font-family "Ubuntu", "Trebuchet MS", sans-serif - font-size 100% - -.osx - font-size 95% + font-size 105% body tab-size 4 @@ -12,7 +9,6 @@ body height 100% color text-color background-color bg-color - font-size 1.05rem a color link-color diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index e108721a..0bbe4ead 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -23,7 +23,8 @@ input-focus-border-color = rgb(248, 165, 130) // Button button-hover-color = link-hover-color -forum-tag-hover-color = rgb(46, 85, 160) +forum-tag-hover-color = #225db5 +// forum-tag-hover-color = rgb(46, 85, 160) // Forum forum-width = 830px @@ -42,7 +43,7 @@ nav-link-hover-slide-color = main-color // nav-link-hover-color = rgb(80, 80, 80) // Tables -table-width-normal = 1200px +table-width-normal = 900px // Loading animation loading-anim-color = nav-link-hover-slide-color @@ -65,6 +66,6 @@ nav-height = 3.11rem typography-margin = 0.4rem // Timings -fade-speed = 200ms -transition-speed = 250ms -mountable-transition-speed = 300ms +fade-speed = 1ms +transition-speed = 200ms +// mountable-transition-speed = 300ms diff --git a/styles/layout.scarlet b/styles/layout.scarlet index 6e425f76..34fd2637 100644 --- a/styles/layout.scarlet +++ b/styles/layout.scarlet @@ -7,4 +7,8 @@ overflow-x hidden overflow-y scroll position relative - // will-change transform \ No newline at end of file + // will-change transform + +#columns + horizontal + height 100% \ No newline at end of file diff --git a/styles/mountable.scarlet b/styles/mountable.scarlet index 78becc67..8a55f28d 100644 --- a/styles/mountable.scarlet +++ b/styles/mountable.scarlet @@ -1,8 +1,8 @@ -.mountable - opacity 0 - transform translateY(0.85rem) - transition opacity mountable-transition-speed ease, transform mountable-transition-speed ease +// .mountable +// opacity 0 +// transform translateY(0.85rem) +// transition opacity mountable-transition-speed ease, transform mountable-transition-speed ease -.mounted - opacity 1 !important - transform translateY(0) \ No newline at end of file +// .mounted +// opacity 1 !important +// transform translateY(0) \ No newline at end of file diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index 1820f459..bbfde471 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -68,9 +68,9 @@ .navigation-button, #search font-size 1.3em -> 550px - #navigation - padding 0 content-padding +// > 550px +// #navigation +// padding 0 content-padding > 930px .navigation-button, #search diff --git a/styles/sidebar.scarlet b/styles/sidebar.scarlet index 6dbe64f2..e15631ba 100644 --- a/styles/sidebar.scarlet +++ b/styles/sidebar.scarlet @@ -1,11 +1,12 @@ -sidebar-spacing-y = 0.75rem +sidebar-spacing-y = 0.7rem #sidebar vertical position fixed left 0 top 0 - min-width 265px + z-index 10 + min-width 200px height 100% background ui-background transform translateX(-100%) @@ -22,23 +23,34 @@ sidebar-spacing-y = 0.75rem justify-content center margin 0.8rem 0 +> 700px + #sidebar + opacity 1 + transform none + position static + pointer-events all + box-shadow none + border-right ui-border + .sidebar-visible transform translateX(0) !important pointer-events all !important opacity 1 !important .sidebar-link - // color text-color + color text-color &.active .sidebar-button - background rgb(245, 245, 245) + background forum-tag-hover-color + color white .sidebar-button horizontal align-items center - padding sidebar-spacing-y content-padding + padding sidebar-spacing-y 1rem + font-size 0.92rem // background ui-background .icon - font-size 1.3rem - margin-right content-padding \ No newline at end of file + font-size 1rem + margin-right 0.75rem \ No newline at end of file diff --git a/styles/table.scarlet b/styles/table.scarlet index 713df6bc..5379e938 100644 --- a/styles/table.scarlet +++ b/styles/table.scarlet @@ -1,7 +1,7 @@ table width 100% max-width table-width-normal - margin 0 auto + // margin 0 auto tr border-bottom-width 1px diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index 7881b8bf..e736750d 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -1,10 +1,13 @@ .tab color text-color !important + padding 0.5rem 1rem + border-right ui-border + background-color rgb(224, 224, 224) !important :hover, &.active - color white !important - background-color forum-tag-hover-color !important + background-color bg-color !important + transform none // color text-color !important // :hover @@ -22,6 +25,11 @@ display none .tabs + horizontal + margin-left calc(content-padding * -1) + margin-top calc(content-padding * -1) + margin-right calc(content-padding * -2) + border-bottom ui-border // justify-content flex-start !important - margin-bottom 1rem - margin-top -0.6rem \ No newline at end of file + // margin-bottom 1rem + // margin-top -0.6rem \ No newline at end of file diff --git a/utils/ItemCSSClass.go b/utils/ItemCSSClass.go deleted file mode 100644 index 0f175ea5..00000000 --- a/utils/ItemCSSClass.go +++ /dev/null @@ -1,14 +0,0 @@ -package utils - -import ( - "github.com/animenotifier/arn" -) - -// ItemCSSClass removes mountable class if the list has too many items. -func ItemCSSClass(list *arn.AnimeList, index int) string { - if index > 20 || len(list.Items) > 50 { - return "anime-list-item" - } - - return "anime-list-item mountable" -} From 5524edd705e2eadd13eda64f4faaf7864e190c1c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 22 Jul 2017 16:31:25 +0200 Subject: [PATCH 330/527] Design improvements --- images/elements/noise-light.png | Bin 0 -> 1320 bytes images/elements/noise-strong.png | Bin 0 -> 4297 bytes mixins/Navigation.pixy | 2 +- pages/animelist/animelist.scarlet | 2 +- pages/home/home.go | 4 ++-- pages/home/home.pixy | 3 +++ styles/base.scarlet | 3 ++- styles/include/mixins.scarlet | 6 ++++++ styles/sidebar.scarlet | 4 ++-- styles/table.scarlet | 2 +- styles/tabs.scarlet | 20 +++++++++++++++----- 11 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 images/elements/noise-light.png create mode 100644 images/elements/noise-strong.png create mode 100644 pages/home/home.pixy diff --git a/images/elements/noise-light.png b/images/elements/noise-light.png new file mode 100644 index 0000000000000000000000000000000000000000..b99b55fc651e3e1d4389d69fdadd6963c347c183 GIT binary patch literal 1320 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 literal 0 HcmV?d00001 diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index c4548ab6..9140747d 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -69,7 +69,7 @@ component NavigationButton(name string, target string, icon string) span.navigation-text= name component SidebarButton(name string, target string, icon string) - a.sidebar-link.ajax(href=target, aria-label=name, title=name, data-bubble="true") + a.sidebar-link.ajax(href=target, aria-label=name, data-bubble="true") .sidebar-button Icon(icon) span.sidebar-text= name diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 82ed74f2..475afdc5 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -2,7 +2,7 @@ vertical width 100% max-width table-width-normal - // margin 0 auto + margin 0 auto margin-bottom 1rem .anime-list diff --git a/pages/home/home.go b/pages/home/home.go index 8de999c3..411c8d98 100644 --- a/pages/home/home.go +++ b/pages/home/home.go @@ -9,7 +9,7 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -// Get the dashboard or the frontpage when logged out. +// Get the anime list or the frontpage when logged out. func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) @@ -27,5 +27,5 @@ func Get(ctx *aero.Context) string { animeList.PrefetchAnime() animeList.Sort() - return ctx.HTML(components.AnimeLists(animeList.SplitByStatus(), animeList.User(), user)) + return ctx.HTML(components.Home(animeList.Watching(), animeList.User(), user, "watching")) } diff --git a/pages/home/home.pixy b/pages/home/home.pixy new file mode 100644 index 00000000..499c5b50 --- /dev/null +++ b/pages/home/home.pixy @@ -0,0 +1,3 @@ +component Home(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User, status string) + StatusTabs("/animelist/") + AnimeListFilteredByStatus(animeList, viewUser, user, status) \ No newline at end of file diff --git a/styles/base.scarlet b/styles/base.scarlet index a2f45946..42ff40dd 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -1,7 +1,7 @@ html height 100% font-family "Ubuntu", "Trebuchet MS", sans-serif - font-size 105% + font-size 95% body tab-size 4 @@ -9,6 +9,7 @@ body height 100% color text-color background-color bg-color + noise-strong a color link-color diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 6bafeb75..0f8294ad 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -17,6 +17,12 @@ mixin vertical-wrap display flex flex-flow column wrap +mixin noise-light + background-image url("/images/elements/noise-light.png") + +mixin noise-strong + background-image url("/images/elements/noise-strong.png") + mixin ui-element border 1px solid ui-border-color background ui-background diff --git a/styles/sidebar.scarlet b/styles/sidebar.scarlet index e15631ba..7b1ffa08 100644 --- a/styles/sidebar.scarlet +++ b/styles/sidebar.scarlet @@ -23,7 +23,7 @@ sidebar-spacing-y = 0.7rem justify-content center margin 0.8rem 0 -> 700px +> 800px #sidebar opacity 1 transform none @@ -31,6 +31,7 @@ sidebar-spacing-y = 0.7rem pointer-events all box-shadow none border-right ui-border + background rgba(0, 0, 0, 0.03) .sidebar-visible transform translateX(0) !important @@ -48,7 +49,6 @@ sidebar-spacing-y = 0.7rem horizontal align-items center padding sidebar-spacing-y 1rem - font-size 0.92rem // background ui-background .icon diff --git a/styles/table.scarlet b/styles/table.scarlet index 5379e938..713df6bc 100644 --- a/styles/table.scarlet +++ b/styles/table.scarlet @@ -1,7 +1,7 @@ table width 100% max-width table-width-normal - // margin 0 auto + margin 0 auto tr border-bottom-width 1px diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index e736750d..c7682fff 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -1,13 +1,21 @@ .tab color text-color !important padding 0.5rem 1rem + background-color rgb(238, 238, 238) border-right ui-border - background-color rgb(224, 224, 224) !important + border-bottom ui-border + white-space nowrap :hover, &.active background-color bg-color !important transform none + + &.active + border-bottom-color transparent + + :first-child + border-left ui-border // color text-color !important // :hover @@ -26,10 +34,12 @@ .tabs horizontal - margin-left calc(content-padding * -1) - margin-top calc(content-padding * -1) - margin-right calc(content-padding * -2) - border-bottom ui-border + justify-content center + // margin-left calc(content-padding * -1) + // margin-top calc(content-padding * -1) + // margin-right calc(content-padding * -2) + margin-bottom content-padding + // background-color rgba(0, 0, 0, 0.02) // justify-content flex-start !important // margin-bottom 1rem // margin-top -0.6rem \ No newline at end of file From 1c5478c5e03f4d2047acd097b00ac7c572b3b37c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 23 Jul 2017 05:51:00 +0200 Subject: [PATCH 331/527] Design updates --- styles/tabs.scarlet | 21 ++++++++++++--------- styles/user.scarlet | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index c7682fff..e6c78694 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -1,18 +1,21 @@ .tab - color text-color !important + color text-color padding 0.5rem 1rem - background-color rgb(238, 238, 238) - border-right ui-border - border-bottom ui-border + background-color rgba(0, 0, 0, 0.02) + border ui-border + border-left none white-space nowrap - :hover, - &.active - background-color bg-color !important + :hover + color text-color + background-color bg-color + + :active transform none - + &.active - border-bottom-color transparent + background-color forum-tag-hover-color + color white :first-child border-left ui-border diff --git a/styles/user.scarlet b/styles/user.scarlet index 5a39867f..0991cb2d 100644 --- a/styles/user.scarlet +++ b/styles/user.scarlet @@ -3,6 +3,7 @@ width avatar-size height avatar-size border-radius 100% + box-shadow outline-shadow-medium object-fit cover default-transition :hover From 9acbddf1be33ae8ad8c631f3613a899db1fcc55f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 23 Jul 2017 05:57:33 +0200 Subject: [PATCH 332/527] Minor changes --- mixins/Sidebar.pixy | 2 +- pages/home/home.pixy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 7b1d0ee5..2b0afda3 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -21,7 +21,7 @@ component Sidebar(user *arn.User) .spacer - SidebarButton("Help", "/thread/I3MMiOtzR", "question") + SidebarButton("Help", "/thread/I3MMiOtzR", "question-circle") if user != nil SidebarButtonNoAJAX("Logout", "/logout", "sign-out") diff --git a/pages/home/home.pixy b/pages/home/home.pixy index 499c5b50..6ab77741 100644 --- a/pages/home/home.pixy +++ b/pages/home/home.pixy @@ -1,3 +1,3 @@ component Home(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User, status string) - StatusTabs("/animelist/") + StatusTabs("/animelist") AnimeListFilteredByStatus(animeList, viewUser, user, status) \ No newline at end of file From 9c900ec01b6d5435715dbaec5873f095256af42b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 22 Sep 2017 06:19:32 +0200 Subject: [PATCH 333/527] Restored mountables --- scripts/AnimeNotifier.ts | 134 +++++++++++++++++----------------- styles/base.scarlet | 2 +- styles/include/config.scarlet | 4 +- styles/mountable.scarlet | 14 ++-- 4 files changed, 77 insertions(+), 77 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 5eb56c9d..33769de5 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -24,7 +24,7 @@ export class AnimeNotifier { imageFound: MutationQueue imageNotFound: MutationQueue - // unmount: MutationQueue + unmount: MutationQueue constructor(app: Application) { this.app = app @@ -33,7 +33,7 @@ export class AnimeNotifier { this.imageFound = new MutationQueue(elem => elem.classList.add("image-found")) this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found")) - // this.unmount = new MutationQueue(elem => elem.classList.remove("mounted")) + this.unmount = new MutationQueue(elem => elem.classList.remove("mounted")) // These classes will never be removed on DOM diffs Diff.persistentClasses.add("mounted") @@ -140,7 +140,7 @@ export class AnimeNotifier { this.visibilityObserver.disconnect() this.contentLoadedActions = Promise.all([ - // Promise.resolve().then(() => this.mountMountables()), + Promise.resolve().then(() => this.mountMountables()), Promise.resolve().then(() => this.lazyLoadImages()), Promise.resolve().then(() => this.displayLocalDates()), Promise.resolve().then(() => this.setSelectBoxValue()), @@ -471,87 +471,87 @@ export class AnimeNotifier { this.visibilityObserver.observe(img) } - // mountMountables() { - // this.modifyDelayed("mountable", element => element.classList.add("mounted")) - // } + mountMountables() { + this.modifyDelayed("mountable", element => element.classList.add("mounted")) + } - // unmountMountables() { - // for(let element of findAll("mountable")) { - // if(element.classList.contains("never-unmount")) { - // continue - // } + unmountMountables() { + for(let element of findAll("mountable")) { + if(element.classList.contains("never-unmount")) { + continue + } - // this.unmount.queue(element) - // } - // } + this.unmount.queue(element) + } + } - // modifyDelayed(className: string, func: (element: HTMLElement) => void) { - // const maxDelay = 1000 - // const delay = 20 + modifyDelayed(className: string, func: (element: HTMLElement) => void) { + const maxDelay = 1000 + const delay = 20 - // let time = 0 - // let start = Date.now() - // let maxTime = start + maxDelay + let time = 0 + let start = Date.now() + let maxTime = start + maxDelay - // let mountableTypes = new Map() - // let mountableTypeMutations = new Map>() + let mountableTypes = new Map() + let mountableTypeMutations = new Map>() - // let collection = document.getElementsByClassName(className) + let collection = document.getElementsByClassName(className) - // if(collection.length === 0) { - // return - // } + if(collection.length === 0) { + return + } - // // let delay = Math.min(maxDelay / collection.length, 20) + // let delay = Math.min(maxDelay / collection.length, 20) - // for(let i = 0; i < collection.length; i++) { - // let element = collection.item(i) as HTMLElement - // let type = element.dataset.mountableType || "general" + for(let i = 0; i < collection.length; i++) { + let element = collection.item(i) as HTMLElement + let type = element.dataset.mountableType || "general" - // if(mountableTypes.has(type)) { - // time = mountableTypes.get(type) + delay - // mountableTypes.set(type, time) - // } else { - // time = start - // mountableTypes.set(type, time) - // mountableTypeMutations.set(type, []) - // } + if(mountableTypes.has(type)) { + time = mountableTypes.get(type) + delay + mountableTypes.set(type, time) + } else { + time = start + mountableTypes.set(type, time) + mountableTypeMutations.set(type, []) + } - // if(time > maxTime) { - // time = maxTime - // } + if(time > maxTime) { + time = maxTime + } - // mountableTypeMutations.get(type).push({ - // element, - // time - // }) - // } + mountableTypeMutations.get(type).push({ + element, + time + }) + } - // for(let mountableType of mountableTypeMutations.keys()) { - // let mutations = mountableTypeMutations.get(mountableType) - // let mutationIndex = 0 + for(let mountableType of mountableTypeMutations.keys()) { + let mutations = mountableTypeMutations.get(mountableType) + let mutationIndex = 0 - // let updateBatch = () => { - // let now = Date.now() + let updateBatch = () => { + let now = Date.now() - // for(; mutationIndex < mutations.length; mutationIndex++) { - // let mutation = mutations[mutationIndex] + for(; mutationIndex < mutations.length; mutationIndex++) { + let mutation = mutations[mutationIndex] - // if(mutation.time > now) { - // break - // } + if(mutation.time > now) { + break + } - // func(mutation.element) - // } + func(mutation.element) + } - // if(mutationIndex < mutations.length) { - // window.requestAnimationFrame(updateBatch) - // } - // } + if(mutationIndex < mutations.length) { + window.requestAnimationFrame(updateBatch) + } + } - // window.requestAnimationFrame(updateBatch) - // } - // } + window.requestAnimationFrame(updateBatch) + } + } diff(url: string) { if(url === this.app.currentPath) { @@ -570,11 +570,11 @@ export class AnimeNotifier { history.pushState(url, null, url) this.app.currentPath = url this.app.markActiveLinks() - // this.unmountMountables() + this.unmountMountables() this.loading(true) // Delay by transition-speed - return request + return delay(300).then(() => request) .then(html => this.app.setContent(html, true)) .then(() => this.app.emit("DOMContentLoaded")) .then(() => this.loading(false)) diff --git a/styles/base.scarlet b/styles/base.scarlet index 42ff40dd..0840795d 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -1,7 +1,7 @@ html height 100% font-family "Ubuntu", "Trebuchet MS", sans-serif - font-size 95% + font-size 100% body tab-size 4 diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 0bbe4ead..7b18e37c 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -66,6 +66,6 @@ nav-height = 3.11rem typography-margin = 0.4rem // Timings -fade-speed = 1ms +fade-speed = 300ms transition-speed = 200ms -// mountable-transition-speed = 300ms +mountable-transition-speed = 300ms diff --git a/styles/mountable.scarlet b/styles/mountable.scarlet index 8a55f28d..78becc67 100644 --- a/styles/mountable.scarlet +++ b/styles/mountable.scarlet @@ -1,8 +1,8 @@ -// .mountable -// opacity 0 -// transform translateY(0.85rem) -// transition opacity mountable-transition-speed ease, transform mountable-transition-speed ease +.mountable + opacity 0 + transform translateY(0.85rem) + transition opacity mountable-transition-speed ease, transform mountable-transition-speed ease -// .mounted -// opacity 1 !important -// transform translateY(0) \ No newline at end of file +.mounted + opacity 1 !important + transform translateY(0) \ No newline at end of file From 4ce0bed52c8c64d6bf1636538cc3198040765fda Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 22 Sep 2017 17:23:22 +0200 Subject: [PATCH 334/527] Implemented new frontpage --- main.go | 7 ++++++ pages/animelist/animelist.scarlet | 2 +- pages/home/animelist.go | 39 +++++++++++++++++++++++++++++++ pages/home/home.go | 15 ++---------- scripts/AnimeNotifier.ts | 5 ++++ styles/include/config.scarlet | 4 ++-- styles/navigation.scarlet | 28 +++++++++++----------- 7 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 pages/home/animelist.go diff --git a/main.go b/main.go index 4cdb14ed..f0dd2d35 100644 --- a/main.go +++ b/main.go @@ -110,6 +110,13 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/user/:nick/animelist/dropped", animelist.FilterByStatus(arn.AnimeListStatusDropped)) app.Ajax("/user/:nick/animelist/anime/:id", animelistitem.Get) + // Anime list + app.Ajax("/animelist/watching", home.FilterByStatus(arn.AnimeListStatusWatching)) + app.Ajax("/animelist/completed", home.FilterByStatus(arn.AnimeListStatusCompleted)) + app.Ajax("/animelist/planned", home.FilterByStatus(arn.AnimeListStatusPlanned)) + app.Ajax("/animelist/hold", home.FilterByStatus(arn.AnimeListStatusHold)) + app.Ajax("/animelist/dropped", home.FilterByStatus(arn.AnimeListStatusDropped)) + // Search app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 475afdc5..73104afe 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -95,4 +95,4 @@ display none !important .fill-screen - min-height calc(100vh - nav-height - content-padding * 2 - 1rem - 43px - 23px) \ No newline at end of file + min-height calc(100vh - content-padding * 2 - 1rem - 43px - 23px) \ No newline at end of file diff --git a/pages/home/animelist.go b/pages/home/animelist.go new file mode 100644 index 00000000..8cff7a18 --- /dev/null +++ b/pages/home/animelist.go @@ -0,0 +1,39 @@ +package home + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/pages/frontpage" + "github.com/animenotifier/notify.moe/utils" +) + +// FilterByStatus returns a handler for the given anime list item status. +func FilterByStatus(status string) aero.Handle { + return func(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return frontpage.Get(ctx) + } + + return AnimeList(ctx, user, status) + } +} + +// AnimeList sends the anime list with the given status for given user. +func AnimeList(ctx *aero.Context, user *arn.User, status string) string { + viewUser := user + animeList := viewUser.AnimeList() + + if animeList == nil { + return ctx.Error(http.StatusNotFound, "Anime list not found", nil) + } + + animeList.PrefetchAnime() + animeList.Sort() + + return ctx.HTML(components.Home(animeList.FilterStatus(status), viewUser, user, status)) +} diff --git a/pages/home/home.go b/pages/home/home.go index 411c8d98..1766a40d 100644 --- a/pages/home/home.go +++ b/pages/home/home.go @@ -1,10 +1,9 @@ package home import ( - "net/http" + "github.com/animenotifier/arn" "github.com/aerogo/aero" - "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/pages/frontpage" "github.com/animenotifier/notify.moe/utils" ) @@ -17,15 +16,5 @@ func Get(ctx *aero.Context) string { return frontpage.Get(ctx) } - viewUser := user - animeList := viewUser.AnimeList() - - if animeList == nil { - return ctx.Error(http.StatusNotFound, "Anime list not found", nil) - } - - animeList.PrefetchAnime() - animeList.Sort() - - return ctx.HTML(components.Home(animeList.Watching(), animeList.User(), user, "watching")) + return AnimeList(ctx, user, arn.AnimeListStatusWatching) } diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 33769de5..e1693928 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -619,6 +619,11 @@ export class AnimeNotifier { let newScroll = 0 let finalScroll = Math.max(target.offsetTop - contentPadding, 0) let scrollDistance = finalScroll - oldScroll + + if(scrollDistance > 0 && scrollDistance < 4) { + return + } + let timeStart = Date.now() let timeEnd = timeStart + duration diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 7b18e37c..a7c21a28 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -66,6 +66,6 @@ nav-height = 3.11rem typography-margin = 0.4rem // Timings -fade-speed = 300ms +fade-speed = 250ms transition-speed = 200ms -mountable-transition-speed = 300ms +mountable-transition-speed = 250ms diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index bbfde471..fedf1a17 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -82,21 +82,21 @@ .extra-navigation display block -@media screen and (max-device-height: 500px) - #navigation - vertical - height 100% - padding content-padding 0 +// @media screen and (max-device-height: 500px) +// #navigation +// vertical +// height 100% +// padding content-padding 0 - #container - horizontal +// #container +// horizontal - .extra-navigation - display block +// .extra-navigation +// display block - #sidebar-toggle, - .hide-landscape - display none !important +// #sidebar-toggle, +// .hide-landscape +// display none !important - #search - display none \ No newline at end of file +// #search +// display none \ No newline at end of file From 43f350f5d7716a0707e7fe1e23a4e859085de376 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 22 Sep 2017 20:10:37 +0200 Subject: [PATCH 335/527] Redirect frontpage --- mixins/Sidebar.pixy | 6 +++++- pages/home/home.go | 5 ++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 2b0afda3..eaea54a8 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -5,7 +5,11 @@ component Sidebar(user *arn.User) else img.user-image.lazy(data-src="/images/brand/64", alt="Anime Notifier") - SidebarButton("Home", "/", "home") + if user != nil + SidebarButton("Home", "/animelist/watching", "home") + else + SidebarButton("Home", "/", "home") + SidebarButton("Forum", "/forum", "comment") SidebarButton("Explore", "/explore", "th") SidebarButton("Artworks", "/artworks", "paint-brush") diff --git a/pages/home/home.go b/pages/home/home.go index 1766a40d..1a31d48a 100644 --- a/pages/home/home.go +++ b/pages/home/home.go @@ -1,8 +1,6 @@ package home import ( - "github.com/animenotifier/arn" - "github.com/aerogo/aero" "github.com/animenotifier/notify.moe/pages/frontpage" "github.com/animenotifier/notify.moe/utils" @@ -16,5 +14,6 @@ func Get(ctx *aero.Context) string { return frontpage.Get(ctx) } - return AnimeList(ctx, user, arn.AnimeListStatusWatching) + return ctx.Redirect("/animelist/watching") + //return AnimeList(ctx, user, arn.AnimeListStatusWatching) } From e99f8e36e52c98121e716baefb0f85fcfaf3a75f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 22 Sep 2017 23:36:43 +0200 Subject: [PATCH 336/527] Added search --- mixins/Sidebar.pixy | 11 +++++--- scripts/AnimeNotifier.ts | 8 +++--- styles/embedded.scarlet | 4 +-- styles/input.scarlet | 1 - styles/navigation.scarlet | 56 ++++++++++++++++++++++----------------- 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index eaea54a8..8ba102b8 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -15,16 +15,21 @@ component Sidebar(user *arn.User) SidebarButton("Artworks", "/artworks", "paint-brush") SidebarButton("Soundtracks", "/soundtracks", "headphones") SidebarButton("AMVs", "/amvs", "video-camera") + //- SidebarButton("Games", "/games", "gamepad") SidebarButton("Users", "/users", "globe") + //- SidebarButton("Search", "/search", "search") if user != nil - if user.Role != "" - SidebarButton("Statistics", "/statistics", "pie-chart") - + SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") .spacer + .sidebar-link(aria-label="Search") + .sidebar-button + Icon("search") + FuzzySearch + SidebarButton("Help", "/thread/I3MMiOtzR", "question-circle") if user != nil diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e1693928..63142e05 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -98,11 +98,6 @@ export class AnimeNotifier { } run() { - // Add "osx" class on macs so we can set a proper font-size - if(navigator.platform.includes("Mac")) { - document.documentElement.classList.add("osx") - } - // Check for WebP support this.webpEnabled = canUseWebP() @@ -127,6 +122,9 @@ export class AnimeNotifier { this.sideBar = this.app.find("sidebar") document.body.addEventListener("click", e => { + if(document.activeElement.id === "search") + return; + this.sideBar.classList.remove("sidebar-visible") }) diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet index e0b1c3a1..7a0fd9f8 100644 --- a/styles/embedded.scarlet +++ b/styles/embedded.scarlet @@ -4,8 +4,8 @@ remove-margin = 1.1rem // Put navigation to the bottom of the screen flex-direction column-reverse !important - .extension-navigation - display inline-block + // .extension-navigation + // display inline-block .anime-list // max-width 500px diff --git a/styles/input.scarlet b/styles/input.scarlet index e4cff2b0..f4e2beed 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -19,7 +19,6 @@ input, textarea, select ui-disabled input, select - width 100% padding 0.5rem 1rem input diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index fedf1a17..b8084275 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -42,45 +42,53 @@ display none #search - flex 1 - border-radius 0 background transparent border none - - color nav-link-hover-color font-size 1em - min-width 0 + padding 0 + width 0 + flex-grow 1 - ::placeholder - color nav-link-color +// #search +// flex 1 +// border-radius 0 +// background transparent +// border none + +// color nav-link-hover-color +// font-size 1em +// min-width 0 - :focus - border none - box-shadow none +// ::placeholder +// color nav-link-color -.extra-navigation - display none +// :focus +// border none +// box-shadow none -.extension-navigation - display none +// .extra-navigation +// display none -> 330px - .navigation-button, #search - font-size 1.3em +// .extension-navigation +// display none + +// > 330px +// .navigation-button, #search +// font-size 1.3em // > 550px // #navigation // padding 0 content-padding -> 930px - .navigation-button, #search - font-size 1.2em +// > 930px +// .navigation-button, #search +// font-size 1.2em - #navigation - justify-content flex-start +// #navigation +// justify-content flex-start - .extra-navigation - display block +// .extra-navigation +// display block // @media screen and (max-device-height: 500px) // #navigation From f2811ba648dbc4ce15911ca95c1f67547e760686 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 23 Sep 2017 18:04:57 +0200 Subject: [PATCH 337/527] Added activity filtering to statistics --- jobs/statistics/statistics.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index d6861e36..81d7a61e 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -45,6 +45,13 @@ func getUserStats() []*arn.PieChart { avatar := stats{} for _, info := range analytics { + user, err := arn.GetUser(info.UserID) + arn.PanicOnError(err) + + if !user.IsActive() { + continue + } + pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) From 7bce362eeb48f5b63a9efcc3d9ef02287fc87478 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 23 Sep 2017 18:55:33 +0200 Subject: [PATCH 338/527] Improved episode refresh --- jobs/refresh-episodes/refresh-episodes.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 3c000c48..bf0bae79 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -31,13 +31,13 @@ func main() { } } - color.Cyan("High priority queue:") + color.Cyan("High priority queue (%d):", len(highPriority)) refresh(highPriority) - color.Cyan("Medium priority queue:") + color.Cyan("Medium priority queue (%d):", len(mediumPriority)) refresh(mediumPriority) - color.Cyan("Low priority queue:") + color.Cyan("Low priority queue (%d):", len(lowPriority)) refresh(lowPriority) color.Green("Finished.") @@ -45,6 +45,8 @@ func main() { func refresh(queue []*arn.Anime) { for _, anime := range queue { + fmt.Println(anime.ID, "|", anime.Title.Canonical, "|", anime.GetMapping("shoboi/anime")) + episodeCount := len(anime.Episodes().Items) availableEpisodeCount := anime.Episodes().AvailableCount() @@ -57,7 +59,7 @@ func refresh(queue []*arn.Anime) { color.Red(err.Error()) } else { - fmt.Println(anime.ID, "|", anime.Title.Canonical, "|", "+"+strconv.Itoa(len(anime.Episodes().Items)-episodeCount)+" airing", "|", "+"+strconv.Itoa(anime.Episodes().AvailableCount()-availableEpisodeCount)+" available") + fmt.Println("+"+strconv.Itoa(len(anime.Episodes().Items)-episodeCount)+" airing", "|", "+"+strconv.Itoa(anime.Episodes().AvailableCount()-availableEpisodeCount)+" available") } } } From eb4b4c7b54eeb954f2004140b757c25ec3a2fc03 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 24 Sep 2017 03:39:26 +0200 Subject: [PATCH 339/527] Can now refresh a single anime in refresh-episodes --- jobs/refresh-episodes/refresh-episodes.go | 48 +++++++++++++---------- jobs/refresh-episodes/shell.go | 32 +++++++++++++++ 2 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 jobs/refresh-episodes/shell.go diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index bf0bae79..b6e466b3 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -12,6 +12,10 @@ import ( func main() { color.Yellow("Refreshing episode information for each anime.") + if InvokeShellArgs() { + return + } + highPriority := []*arn.Anime{} mediumPriority := []*arn.Anime{} lowPriority := []*arn.Anime{} @@ -32,34 +36,38 @@ func main() { } color.Cyan("High priority queue (%d):", len(highPriority)) - refresh(highPriority) + refreshQueue(highPriority) color.Cyan("Medium priority queue (%d):", len(mediumPriority)) - refresh(mediumPriority) + refreshQueue(mediumPriority) color.Cyan("Low priority queue (%d):", len(lowPriority)) - refresh(lowPriority) + refreshQueue(lowPriority) color.Green("Finished.") } -func refresh(queue []*arn.Anime) { +func refreshQueue(queue []*arn.Anime) { for _, anime := range queue { - fmt.Println(anime.ID, "|", anime.Title.Canonical, "|", anime.GetMapping("shoboi/anime")) - - episodeCount := len(anime.Episodes().Items) - availableEpisodeCount := anime.Episodes().AvailableCount() - - err := anime.RefreshEpisodes() - - if err != nil { - if strings.Contains(err.Error(), "missing a Shoboi ID") { - continue - } - - color.Red(err.Error()) - } else { - fmt.Println("+"+strconv.Itoa(len(anime.Episodes().Items)-episodeCount)+" airing", "|", "+"+strconv.Itoa(anime.Episodes().AvailableCount()-availableEpisodeCount)+" available") - } + refresh(anime) + } +} + +func refresh(anime *arn.Anime) { + fmt.Println(anime.ID, "|", anime.Title.Canonical, "|", anime.GetMapping("shoboi/anime")) + + episodeCount := len(anime.Episodes().Items) + availableEpisodeCount := anime.Episodes().AvailableCount() + + err := anime.RefreshEpisodes() + + if err != nil { + if strings.Contains(err.Error(), "missing a Shoboi ID") { + return + } + + color.Red(err.Error()) + } else { + fmt.Println("+"+strconv.Itoa(len(anime.Episodes().Items)-episodeCount)+" airing", "|", "+"+strconv.Itoa(anime.Episodes().AvailableCount()-availableEpisodeCount)+" available") } } diff --git a/jobs/refresh-episodes/shell.go b/jobs/refresh-episodes/shell.go new file mode 100644 index 00000000..a804ad45 --- /dev/null +++ b/jobs/refresh-episodes/shell.go @@ -0,0 +1,32 @@ +package main + +import ( + "flag" + + "github.com/animenotifier/arn" +) + +// Shell parameters +var animeID string + +// Shell flags +func init() { + flag.StringVar(&animeID, "id", "", "ID of the anime you want to refresh") + flag.Parse() +} + +// InvokeShellArgs ... +func InvokeShellArgs() bool { + if animeID != "" { + anime, err := arn.GetAnime(animeID) + + if err != nil { + panic(err) + } + + refresh(anime) + return true + } + + return false +} From bd38aca4c0c0ca970f9bf181928a4b889e60d133 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 24 Sep 2017 05:01:15 +0200 Subject: [PATCH 340/527] Improved episode refresh --- jobs/refresh-episodes/refresh-episodes.go | 12 ++++++++++-- jobs/sync-shoboi/sync-shoboi.go | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index b6e466b3..0f294e73 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -2,8 +2,8 @@ package main import ( "fmt" - "strconv" "strings" + "time" "github.com/animenotifier/arn" "github.com/fatih/color" @@ -50,6 +50,9 @@ func main() { func refreshQueue(queue []*arn.Anime) { for _, anime := range queue { refresh(anime) + + // Lower the request interval + time.Sleep(5 * time.Second) } } @@ -68,6 +71,11 @@ func refresh(anime *arn.Anime) { color.Red(err.Error()) } else { - fmt.Println("+"+strconv.Itoa(len(anime.Episodes().Items)-episodeCount)+" airing", "|", "+"+strconv.Itoa(anime.Episodes().AvailableCount()-availableEpisodeCount)+" available") + faint := color.New(color.Faint).SprintFunc() + episodes := anime.Episodes() + + fmt.Println(faint(episodes)) + fmt.Printf("+%d airing | +%d available (%d total)\n", len(episodes.Items), len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount) + println() } } diff --git a/jobs/sync-shoboi/sync-shoboi.go b/jobs/sync-shoboi/sync-shoboi.go index 341eb451..e0addec4 100644 --- a/jobs/sync-shoboi/sync-shoboi.go +++ b/jobs/sync-shoboi/sync-shoboi.go @@ -20,6 +20,9 @@ func main() { if sync(anime) { count++ } + + // Lower the request interval + time.Sleep(2 * time.Second) } // Log @@ -53,7 +56,6 @@ func sync(anime *arn.Anime) bool { // Did we get the ID? if anime.GetMapping("shoboi/anime") != "" { println(color.GreenString("✔")) - time.Sleep(2 * time.Second) return true } From df762bc6e9f0ada5bc7469a60644fc9f9b5522fc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 25 Sep 2017 17:24:21 +0200 Subject: [PATCH 341/527] Removed delay on refresh episodes --- jobs/refresh-episodes/refresh-episodes.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 0f294e73..e3be83cd 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -3,7 +3,6 @@ package main import ( "fmt" "strings" - "time" "github.com/animenotifier/arn" "github.com/fatih/color" @@ -50,9 +49,6 @@ func main() { func refreshQueue(queue []*arn.Anime) { for _, anime := range queue { refresh(anime) - - // Lower the request interval - time.Sleep(5 * time.Second) } } From 025a7c431dae864873e0f2f117d109a7115f8746 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 30 Sep 2017 12:18:21 +0200 Subject: [PATCH 342/527] Fixed discord bot --- bots/discord/discord.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/bots/discord/discord.go b/bots/discord/discord.go index 359a2cca..f656a56f 100644 --- a/bots/discord/discord.go +++ b/bots/discord/discord.go @@ -97,19 +97,27 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { if strings.HasPrefix(m.Content, "!s ") { term := m.Content[len("!s "):] - userResults, animeResults := arn.Search(term, 3, 3) + users, animes, posts, threads := arn.Search(term, 3, 3, 3, 3) message := "" - for _, user := range userResults { + for _, user := range users { message += "https://notify.moe" + user.Link() + "\n" } - for _, anime := range animeResults { + for _, anime := range animes { message += "https://notify.moe" + anime.Link() + "\n" } - if len(userResults) == 0 && len(animeResults) == 0 { - message = "Sorry, I couldn't find any anime or users with that term." + for _, post := range posts { + message += "https://notify.moe" + post.Link() + "\n" + } + + for _, thread := range threads { + message += "https://notify.moe" + thread.Link() + "\n" + } + + if len(users) == 0 && len(animes) == 0 && len(posts) == 0 && len(threads) == 0 { + message = "Sorry, I couldn't find anything using that term." } s.ChannelMessageSend(m.ChannelID, message) From 3caad5cb0d45cdfc85518c5139f5d36ea2c63002 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 30 Sep 2017 15:05:25 +0200 Subject: [PATCH 343/527] Removed horizontal line in profile --- pages/profile/profile.pixy | 1 - 1 file changed, 1 deletion(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 2ff3b5cc..2ccb0985 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -89,7 +89,6 @@ component ProfileNavigation(viewUser *arn.User, uri string) span.tab-text Followers if strings.Contains(uri, "/animelist") - hr StatusTabs("/+" + viewUser.Nick + "/animelist") component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, threads []*arn.Thread, posts []*arn.Post, tracks []*arn.SoundTrack, uri string) From 831c6118d9f4a7dff39298cda36b6a642a11808a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 30 Sep 2017 16:04:13 +0200 Subject: [PATCH 344/527] Added anime popularity --- jobs/airing-anime/airing-anime.go | 28 ++++++++++++++---- jobs/anime-ratings/anime-ratings.go | 37 +++++++++++++++++++++++- jobs/sync-anime/sync-anime.go | 5 ++++ mixins/AnimeGrid.pixy | 2 +- pages/anime/anime.pixy | 18 ++++++++++++ patches/add-popularity/add-popularity.go | 16 ++++++++++ 6 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 patches/add-popularity/add-popularity.go diff --git a/jobs/airing-anime/airing-anime.go b/jobs/airing-anime/airing-anime.go index 09de8914..ef944efa 100644 --- a/jobs/airing-anime/airing-anime.go +++ b/jobs/airing-anime/airing-anime.go @@ -7,7 +7,12 @@ import ( "github.com/fatih/color" ) -const currentlyAiringBonus = 4.0 +const ( + currentlyAiringBonus = 4.0 + popularityThreshold = 5 + popularityPenalty = 4.0 + watchingPopularityWeight = 0.1 +) func main() { color.Yellow("Caching airing anime") @@ -21,17 +26,30 @@ func main() { } sort.Slice(animeList, func(i, j int) bool { - scoreA := animeList[i].Rating.Overall - scoreB := animeList[j].Rating.Overall + a := animeList[i] + b := animeList[j] + scoreA := a.Rating.Overall + scoreB := b.Rating.Overall - if animeList[i].Status == "current" { + if a.Status == "current" { scoreA += currentlyAiringBonus } - if animeList[j].Status == "current" { + if b.Status == "current" { scoreB += currentlyAiringBonus } + if a.Popularity.Total() < popularityThreshold { + scoreA -= popularityPenalty + } + + if b.Popularity.Total() < popularityThreshold { + scoreB -= popularityPenalty + } + + scoreA += float64(a.Popularity.Watching) * watchingPopularityWeight + scoreB += float64(b.Popularity.Watching) * watchingPopularityWeight + return scoreA > scoreB }) diff --git a/jobs/anime-ratings/anime-ratings.go b/jobs/anime-ratings/anime-ratings.go index 35f053e7..99c020e9 100644 --- a/jobs/anime-ratings/anime-ratings.go +++ b/jobs/anime-ratings/anime-ratings.go @@ -7,6 +7,7 @@ import ( var ratings = map[string][]*arn.AnimeRating{} var finalRating = map[string]*arn.AnimeRating{} +var popularity = map[string]*arn.AnimePopularity{} // Note this is using the airing-anime as a template with modfications // made to it. @@ -18,9 +19,10 @@ func main() { for _, animeList := range allAnimeLists { extractRatings(animeList) + extractPopularity(animeList) } - // Calculate + // Calculate rating for animeID := range finalRating { overall := []float64{} story := []float64{} @@ -59,6 +61,14 @@ func main() { arn.PanicOnError(anime.Save()) } + // Save popularity + for animeID := range popularity { + anime, err := arn.GetAnime(animeID) + arn.PanicOnError(err) + anime.Popularity = popularity[animeID] + arn.PanicOnError(anime.Save()) + } + color.Green("Finished.") } @@ -92,3 +102,28 @@ func extractRatings(animeList *arn.AnimeList) { ratings[item.AnimeID] = append(ratings[item.AnimeID], item.Rating) } } + +func extractPopularity(animeList *arn.AnimeList) { + for _, item := range animeList.Items { + _, found := popularity[item.AnimeID] + + if !found { + popularity[item.AnimeID] = &arn.AnimePopularity{} + } + + counter := popularity[item.AnimeID] + + switch item.Status { + case arn.AnimeListStatusWatching: + counter.Watching++ + case arn.AnimeListStatusCompleted: + counter.Completed++ + case arn.AnimeListStatusPlanned: + counter.Planned++ + case arn.AnimeListStatusHold: + counter.Hold++ + case arn.AnimeListStatusDropped: + counter.Dropped++ + } + } +} diff --git a/jobs/sync-anime/sync-anime.go b/jobs/sync-anime/sync-anime.go index b531802e..2b97a418 100644 --- a/jobs/sync-anime/sync-anime.go +++ b/jobs/sync-anime/sync-anime.go @@ -111,6 +111,11 @@ func sync(data *kitsu.Anime) *arn.Anime { anime.Rating.Reset() } + // Popularity + if anime.Popularity == nil { + anime.Popularity = &arn.AnimePopularity{} + } + // Trailers anime.Trailers = []*arn.ExternalMedia{} diff --git a/mixins/AnimeGrid.pixy b/mixins/AnimeGrid.pixy index 43537328..b95e06ef 100644 --- a/mixins/AnimeGrid.pixy +++ b/mixins/AnimeGrid.pixy @@ -2,4 +2,4 @@ component AnimeGrid(animeList []*arn.Anime) .anime-grid each anime in animeList a.anime-grid-cell.ajax(href="/anime/" + toString(anime.ID)) - img.anime-grid-image.lazy(data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")") \ No newline at end of file + img.anime-grid-image.lazy(data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji) \ No newline at end of file diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 974f6968..93235ddc 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -169,6 +169,24 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* if character.Character() != nil Character(character.Character()) + h3.anime-section-name Popularity + .anime-rating-categories + .anime-rating-category + .anime-rating-category-name Watching + .anime-rating= anime.Popularity.Watching + .anime-rating-category + .anime-rating-category-name Completed + .anime-rating= anime.Popularity.Completed + .anime-rating-category + .anime-rating-category-name Planned + .anime-rating= anime.Popularity.Planned + .anime-rating-category + .anime-rating-category-name Hold + .anime-rating= anime.Popularity.Hold + .anime-rating-category + .anime-rating-category-name Dropped + .anime-rating= anime.Popularity.Dropped + if len(anime.Episodes().Items) > 0 if episodesReversed h3.anime-section-name Latest episodes diff --git a/patches/add-popularity/add-popularity.go b/patches/add-popularity/add-popularity.go new file mode 100644 index 00000000..c799eaca --- /dev/null +++ b/patches/add-popularity/add-popularity.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + for anime := range arn.MustStreamAnime() { + if anime.Popularity != nil { + continue + } + + anime.Popularity = &arn.AnimePopularity{} + arn.PanicOnError(anime.Save()) + } +} From 5fc07782b9819bdddea6fbd9589d00f83258b8de Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 30 Sep 2017 16:26:01 +0200 Subject: [PATCH 345/527] Increased weight of watching popularity --- jobs/airing-anime/airing-anime.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/airing-anime/airing-anime.go b/jobs/airing-anime/airing-anime.go index ef944efa..7b81a063 100644 --- a/jobs/airing-anime/airing-anime.go +++ b/jobs/airing-anime/airing-anime.go @@ -11,7 +11,7 @@ const ( currentlyAiringBonus = 4.0 popularityThreshold = 5 popularityPenalty = 4.0 - watchingPopularityWeight = 0.1 + watchingPopularityWeight = 0.2 ) func main() { From e646410fc22c6d68ac1d2b8d4c45ff5a0f99b89b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 1 Oct 2017 07:24:56 +0200 Subject: [PATCH 346/527] Added new route tests --- rewrite.go | 3 ++ tests.go | 88 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/rewrite.go b/rewrite.go index edc0a7be..78d45abf 100644 --- a/rewrite.go +++ b/rewrite.go @@ -14,6 +14,7 @@ func init() { app.Rewrite(func(ctx *aero.RewriteContext) { requestURI := ctx.URI() + // User profiles if strings.HasPrefix(requestURI, plusRoute) { newURI := "/user/" userName := requestURI[2:] @@ -28,6 +29,7 @@ func init() { return } + // Search if strings.HasPrefix(requestURI, "/search/") { searchTerm := requestURI[len("/search/"):] ctx.Request.URL.RawQuery = "q=" + searchTerm @@ -42,6 +44,7 @@ func init() { return } + // Analytics if requestURI == "/dark-flame-master" { ctx.SetURI("/api/new/analytics") return diff --git a/tests.go b/tests.go index fd13ff74..8d86176c 100644 --- a/tests.go +++ b/tests.go @@ -26,6 +26,14 @@ var routeTests = map[string][]string{ "/+Akyoto/soundtracks", }, + "/user/:nick/followers": []string{ + "/+Akyoto/followers", + }, + + "/user/:nick/stats": []string{ + "/+Akyoto/stats", + }, + "/user/:nick/animelist": []string{ "/+Akyoto/animelist", }, @@ -79,6 +87,10 @@ var routeTests = map[string][]string{ "/soundtrack/h0ac8sKkg", }, + "/character/:id": []string{ + "/character/6556", + }, + // API "/api/anime/:id": []string{ "/api/anime/1", @@ -148,6 +160,34 @@ var routeTests = map[string][]string{ "/api/youtubetosoundtrack/hU2wqJuOIp4", }, + "/api/userfollows/:id": []string{ + "/api/userfollows/4J6qpK1ve", + }, + + "/api/anilisttoanime/:id": []string{ + "/api/anilisttoanime/527", + }, + + "/api/animecharacters/:id": []string{ + "/api/animecharacters/323", + }, + + "/api/animeepisodes/:id": []string{ + "/api/animeepisodes/323", + }, + + "/api/character/:id": []string{ + "/api/character/6556", + }, + + "/api/pushsubscriptions/:id": []string{ + "/api/pushsubscriptions/4J6qpK1ve", + }, + + "/api/myanimelisttoanime/:id": []string{ + "/api/myanimelisttoanime/527", + }, + // Images "/images/avatars/large/:file": []string{ "/images/avatars/large/4J6qpK1ve.webp", @@ -174,28 +214,32 @@ var routeTests = map[string][]string{ }, // Disable these tests because they require authorization - "/auth/google": nil, - "/auth/google/callback": nil, - "/auth/facebook": nil, - "/auth/facebook/callback": nil, - "/import": nil, - "/import/anilist/animelist": nil, - "/import/anilist/animelist/finish": nil, - "/import/myanimelist/animelist": nil, - "/import/myanimelist/animelist/finish": nil, - "/import/kitsu/animelist": nil, - "/import/kitsu/animelist/finish": nil, - "/api/test/notification": nil, - "/api/paypal/payment/create": nil, - "/paypal/success": nil, - "/paypal/cancel": nil, - "/anime/:id/edit": nil, - "/new/thread": nil, - "/new/soundtrack": nil, - "/editor": nil, - "/user": nil, - "/settings": nil, - "/extension/embed": nil, + "/auth/google": nil, + "/auth/google/callback": nil, + "/auth/facebook": nil, + "/auth/facebook/callback": nil, + "/import": nil, + "/import/anilist/animelist": nil, + "/import/anilist/animelist/finish": nil, + "/import/myanimelist/animelist": nil, + "/import/myanimelist/animelist/finish": nil, + "/import/kitsu/animelist": nil, + "/import/kitsu/animelist/finish": nil, + "/api/test/notification": nil, + "/api/paypal/payment/create": nil, + "/api/userfollows/:id/get/:item": nil, + "/api/userfollows/:id/get/:item/:property": nil, + "/api/pushsubscriptions/:id/get/:item": nil, + "/api/pushsubscriptions/:id/get/:item/:property": nil, + "/paypal/success": nil, + "/paypal/cancel": nil, + "/anime/:id/edit": nil, + "/new/thread": nil, + "/new/soundtrack": nil, + "/editor": nil, + "/user": nil, + "/settings": nil, + "/extension/embed": nil, } // API interfaces From c8acdc0d11a2c53cd366d1a8be7078cc68cb1008 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 1 Oct 2017 07:50:53 +0200 Subject: [PATCH 347/527] Cleanup --- scripts/Analytics.ts | 26 ++++++++++++++++++++++++++ scripts/AnimeNotifier.ts | 38 ++++++++------------------------------ scripts/Diff.ts | 7 ------- 3 files changed, 34 insertions(+), 37 deletions(-) create mode 100644 scripts/Analytics.ts diff --git a/scripts/Analytics.ts b/scripts/Analytics.ts new file mode 100644 index 00000000..f4c91fab --- /dev/null +++ b/scripts/Analytics.ts @@ -0,0 +1,26 @@ +export class Analytics { + push() { + let analytics = { + general: { + timezoneOffset: new Date().getTimezoneOffset() + }, + screen: { + width: screen.width, + height: screen.height, + availableWidth: screen.availWidth, + availableHeight: screen.availHeight, + pixelRatio: window.devicePixelRatio + }, + system: { + cpuCount: navigator.hardwareConcurrency, + platform: navigator.platform + } + } + + fetch("/dark-flame-master", { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(analytics) + }) + } +} \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 63142e05..7cb57f1b 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -7,9 +7,11 @@ import { MutationQueue } from "./MutationQueue" import { StatusMessage } from "./StatusMessage" import { PushManager } from "./PushManager" import { TouchController } from "./TouchController" +import { Analytics } from "./Analytics" export class AnimeNotifier { app: Application + analytics: Analytics user: HTMLElement title: string webpEnabled: boolean @@ -118,6 +120,9 @@ export class AnimeNotifier { // Push manager this.pushManager = new PushManager() + // Analytics + this.analytics = new Analytics() + // Sidebar control this.sideBar = this.app.find("sidebar") @@ -180,7 +185,9 @@ export class AnimeNotifier { this.registerServiceWorker() // Analytics - this.pushAnalytics() + if(this.user) { + this.analytics.push() + } // Offline message if(navigator.onLine === false) { @@ -290,35 +297,6 @@ export class AnimeNotifier { } } - pushAnalytics() { - if(!this.user) { - return - } - - let analytics = { - general: { - timezoneOffset: new Date().getTimezoneOffset() - }, - screen: { - width: screen.width, - height: screen.height, - availableWidth: screen.availWidth, - availableHeight: screen.availHeight, - pixelRatio: window.devicePixelRatio - }, - system: { - cpuCount: navigator.hardwareConcurrency, - platform: navigator.platform - } - } - - fetch("/dark-flame-master", { - method: "POST", - credentials: "same-origin", - body: JSON.stringify(analytics) - }) - } - setSelectBoxValue() { for(let element of document.getElementsByTagName("select")) { element.value = element.getAttribute("value") diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 88cc90fc..0fc7ffcb 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -68,13 +68,6 @@ export class Diff { let elemA = a as HTMLElement let elemB = b as HTMLElement - // Skip iframes - // This part needs to be executed AFTER lazy images check - // to allow lazily loaded iframes to update their data src. - if(elemA.tagName === "IFRAME") { - continue - } - let removeAttributes: Attr[] = [] for(let x = 0; x < elemA.attributes.length; x++) { From 1ced81352f657375ee09d40df1d368905e8d3e35 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 1 Oct 2017 07:56:30 +0200 Subject: [PATCH 348/527] Minor fix --- scripts/AnimeNotifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 7cb57f1b..8d5d5e36 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -138,7 +138,7 @@ export class AnimeNotifier { this.touchController.rightSwipe = () => this.sideBar.classList.add("sidebar-visible") } - async onContentLoaded() { + onContentLoaded() { // Stop watching all the objects from the previous page. this.visibilityObserver.disconnect() From b45af0eaf90c3463fff699e887185c87e42fa42d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 1 Oct 2017 08:06:43 +0200 Subject: [PATCH 349/527] Added desktop app link to settings --- pages/settings/settings.pixy | 6 ++++++ scripts/Actions.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 13a86784..e3c78740 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -101,6 +101,12 @@ component Settings(user *arn.User) Icon("chrome") span Get the Chrome Extension + .widget-input + label Desktop App: + button.action(data-action="installApp", data-trigger="click") + Icon("desktop") + span Get the Desktop App + .widget-input label Android App: a.button(href="https://www.youtube.com/watch?v=opyt4cw0ep8", target="_blank", rel="noopener") diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 901d7871..aa1e925a 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -306,4 +306,9 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen export function installExtension(arn: AnimeNotifier, button: HTMLElement) { let browser: any = window["chrome"] browser.webstore.install() +} + +// Desktop app installation +export function installApp() { + alert("Open your browser menu > 'More tools' > 'Add to desktop' and enable 'Open as window'.") } \ No newline at end of file From 1101dafc90c24800ea053384f5ad7497e7450c4a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 00:01:01 +0200 Subject: [PATCH 350/527] Improved testing --- benchmarks/DB_AnimeList_test.go | 2 +- main_test.go | 2 ++ makefile | 2 +- pages/anime/anime.pixy | 6 ++++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/benchmarks/DB_AnimeList_test.go b/benchmarks/DB_AnimeList_test.go index 8d79a521..d16c0da7 100644 --- a/benchmarks/DB_AnimeList_test.go +++ b/benchmarks/DB_AnimeList_test.go @@ -14,7 +14,7 @@ func BenchmarkDBAnimeListGetMap(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { - animeList, _ := arn.GetAnimeList(user) + animeList, _ := arn.GetAnimeList(user.ID) noop(animeList) } }) diff --git a/main_test.go b/main_test.go index 7598d804..4daa0c38 100644 --- a/main_test.go +++ b/main_test.go @@ -24,6 +24,8 @@ func TestRoutes(t *testing.T) { if status := responseRecorder.Code; status != http.StatusOK { t.Errorf("%s | Wrong status code | %v instead of %v", example, status, http.StatusOK) + } else { + t.Logf("%s | Correct status code | %v == %v", example, status, http.StatusOK) } } } diff --git a/makefile b/makefile index 9c2a429c..419a9886 100644 --- a/makefile +++ b/makefile @@ -23,7 +23,7 @@ js: install: $(GOINSTALL) test: - $(GOTEST) + $(GOTEST) github.com/animenotifier/... -v bench: $(GOTEST) -bench . tools: diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 93235ddc..8adf86b2 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -210,8 +210,10 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* RawIcon("eye") //- a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") //- RawIcon("google") - td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() - + if episode.AiringDate.IsValid() + td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() + else + td.episode-airing-date-start //- h3.anime-section-name Reviews //- p Coming soon. From 504993861babb0a4c4afaab561a920782de9d3cd Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 00:31:44 +0200 Subject: [PATCH 351/527] Cleanup --- assets.go | 15 +++++------ main.go | 8 ++++++ main_test.go | 57 +++++++++++++++++++++++++++++++++++++--- rewrite.go | 73 ++++++++++++++++++++++++---------------------------- tests.go | 62 -------------------------------------------- 5 files changed, 103 insertions(+), 112 deletions(-) diff --git a/assets.go b/assets.go index 48f3b1eb..9c0946b8 100644 --- a/assets.go +++ b/assets.go @@ -9,8 +9,10 @@ import ( ) func init() { - // Scripts - scripts := js.Bundle() + // Script bundle + scriptBundle := js.Bundle() + + // Service worker serviceWorkerBytes, err := ioutil.ReadFile("sw/service-worker.js") serviceWorker := string(serviceWorkerBytes) @@ -19,18 +21,15 @@ func init() { } app.Get("/scripts", func(ctx *aero.Context) string { - ctx.SetResponseHeader("Content-Type", "application/javascript") - return scripts + return ctx.JavaScript(scriptBundle) }) app.Get("/scripts.js", func(ctx *aero.Context) string { - ctx.SetResponseHeader("Content-Type", "application/javascript") - return scripts + return ctx.JavaScript(scriptBundle) }) app.Get("/service-worker", func(ctx *aero.Context) string { - ctx.SetResponseHeader("Content-Type", "application/javascript") - return serviceWorker + return ctx.JavaScript(serviceWorker) }) // Web manifest diff --git a/main.go b/main.go index f0dd2d35..12c0c193 100644 --- a/main.go +++ b/main.go @@ -153,6 +153,9 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/paypal/cancel", paypal.Cancel) app.Get("/api/paypal/payment/create", paypal.CreatePayment) + // Rewrite + app.Rewrite(Rewrite) + // Middleware app.Use(middleware.Firewall()) app.Use(middleware.Log()) @@ -173,5 +176,10 @@ func configure(app *aero.Application) *aero.Application { // Authentication auth.Install(app) + // Specify test routes + for route, examples := range routeTests { + app.Test(route, examples) + } + return app } diff --git a/main_test.go b/main_test.go index 4daa0c38..80f99509 100644 --- a/main_test.go +++ b/main_test.go @@ -1,11 +1,16 @@ package main import ( + "errors" "net/http" "net/http/httptest" + "reflect" "testing" "github.com/aerogo/aero" + "github.com/aerogo/api" + "github.com/animenotifier/arn" + "github.com/fatih/color" ) func TestRoutes(t *testing.T) { @@ -23,9 +28,55 @@ func TestRoutes(t *testing.T) { 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) - } else { - t.Logf("%s | Correct status code | %v == %v", example, status, http.StatusOK) + color.Red("%s | Wrong status code | %v instead of %v", example, status, http.StatusOK) + } + } + } +} + +func TestInterfaceImplementations(t *testing.T) { + // API interfaces + var creatable = reflect.TypeOf((*api.Creatable)(nil)).Elem() + var updatable = reflect.TypeOf((*api.Updatable)(nil)).Elem() + var actionable = reflect.TypeOf((*api.Actionable)(nil)).Elem() + var collection = reflect.TypeOf((*api.Collection)(nil)).Elem() + + // Required interface implementations + var interfaceImplementations = map[string][]reflect.Type{ + "User": []reflect.Type{ + updatable, + }, + "Thread": []reflect.Type{ + creatable, + updatable, + actionable, + }, + "Post": []reflect.Type{ + creatable, + updatable, + actionable, + }, + "SoundTrack": []reflect.Type{ + creatable, + }, + "Analytics": []reflect.Type{ + creatable, + }, + "AnimeList": []reflect.Type{ + collection, + }, + "PushSubscriptions": []reflect.Type{ + collection, + }, + "UserFollows": []reflect.Type{ + collection, + }, + } + + for typeName, interfaces := range interfaceImplementations { + for _, requiredInterface := range interfaces { + if !reflect.PtrTo(arn.DB.Type(typeName)).Implements(requiredInterface) { + panic(errors.New(typeName + " does not implement interface " + requiredInterface.Name())) } } } diff --git a/rewrite.go b/rewrite.go index 78d45abf..60096ead 100644 --- a/rewrite.go +++ b/rewrite.go @@ -6,48 +6,43 @@ import ( "github.com/aerogo/aero" ) -func init() { - plusRoute := "/+" - plusRouteAjax := "/_/+" +// Rewrite will rewrite certain routes +func Rewrite(ctx *aero.RewriteContext) { + requestURI := ctx.URI() - // This will rewrite /+UserName requests to /user/UserName - app.Rewrite(func(ctx *aero.RewriteContext) { - requestURI := ctx.URI() + // User profiles + if strings.HasPrefix(requestURI, "/+") { + newURI := "/user/" + userName := requestURI[2:] + ctx.SetURI(newURI + userName) + return + } - // User profiles - if strings.HasPrefix(requestURI, plusRoute) { - newURI := "/user/" - userName := requestURI[2:] - ctx.SetURI(newURI + userName) - return - } + if strings.HasPrefix(requestURI, "/_/+") { + newURI := "/_/user/" + userName := requestURI[4:] + ctx.SetURI(newURI + userName) + return + } - if strings.HasPrefix(requestURI, plusRouteAjax) { - newURI := "/_/user/" - userName := requestURI[4:] - ctx.SetURI(newURI + userName) - return - } + // Search + if strings.HasPrefix(requestURI, "/search/") { + searchTerm := requestURI[len("/search/"):] + ctx.Request.URL.RawQuery = "q=" + searchTerm + ctx.SetURI("/search") + return + } - // Search - if strings.HasPrefix(requestURI, "/search/") { - searchTerm := requestURI[len("/search/"):] - ctx.Request.URL.RawQuery = "q=" + searchTerm - ctx.SetURI("/search") - return - } + if strings.HasPrefix(requestURI, "/_/search/") { + searchTerm := requestURI[len("/_/search/"):] + ctx.Request.URL.RawQuery = "q=" + searchTerm + ctx.SetURI("/_/search") + return + } - if strings.HasPrefix(requestURI, "/_/search/") { - searchTerm := requestURI[len("/_/search/"):] - ctx.Request.URL.RawQuery = "q=" + searchTerm - ctx.SetURI("/_/search") - return - } - - // Analytics - if requestURI == "/dark-flame-master" { - ctx.SetURI("/api/new/analytics") - return - } - }) + // Analytics + if requestURI == "/dark-flame-master" { + ctx.SetURI("/api/new/analytics") + return + } } diff --git a/tests.go b/tests.go index 8d86176c..ff152fdc 100644 --- a/tests.go +++ b/tests.go @@ -1,13 +1,5 @@ package main -import ( - "errors" - "reflect" - - "github.com/aerogo/api" - "github.com/animenotifier/arn" -) - var routeTests = map[string][]string{ // User "/user/:nick": []string{ @@ -241,57 +233,3 @@ var routeTests = map[string][]string{ "/settings": nil, "/extension/embed": nil, } - -// API interfaces -var creatable = reflect.TypeOf((*api.Creatable)(nil)).Elem() -var updatable = reflect.TypeOf((*api.Updatable)(nil)).Elem() -var actionable = reflect.TypeOf((*api.Actionable)(nil)).Elem() -var collection = reflect.TypeOf((*api.Collection)(nil)).Elem() - -// Required interface implementations -var interfaceImplementations = map[string][]reflect.Type{ - "User": []reflect.Type{ - updatable, - }, - "Thread": []reflect.Type{ - creatable, - updatable, - actionable, - }, - "Post": []reflect.Type{ - creatable, - updatable, - actionable, - }, - "SoundTrack": []reflect.Type{ - creatable, - }, - "Analytics": []reflect.Type{ - creatable, - }, - "AnimeList": []reflect.Type{ - collection, - }, - "PushSubscriptions": []reflect.Type{ - collection, - }, - "UserFollows": []reflect.Type{ - collection, - }, -} - -func init() { - // Specify test routes - for route, examples := range routeTests { - app.Test(route, examples) - } - - // Check interface implementations - for typeName, interfaces := range interfaceImplementations { - for _, requiredInterface := range interfaces { - if !reflect.PtrTo(arn.DB.Type(typeName)).Implements(requiredInterface) { - panic(errors.New(typeName + " does not implement interface " + requiredInterface.Name())) - } - } - } -} From b0c70fc735e30e849a86d58c4eea1870b2a795cd Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 00:55:37 +0200 Subject: [PATCH 352/527] Improved assets --- assets.go | 3 +-- config.json | 8 ++++---- images/elements/noise-strong.png | Bin 4297 -> 3157 bytes mixins/Sidebar.pixy | 2 +- mixins/StatusTabs.pixy | 27 +++++++++------------------ pages/frontpage/frontpage.go | 2 +- pages/notifications/notifications.go | 2 +- 7 files changed, 17 insertions(+), 27 deletions(-) diff --git a/assets.go b/assets.go index 9c0946b8..e4bfa579 100644 --- a/assets.go +++ b/assets.go @@ -44,8 +44,7 @@ func init() { // Brand icons app.Get("/images/brand/:file", func(ctx *aero.Context) string { - file := strings.TrimSuffix(ctx.Get("file"), ".webp") - return ctx.TryWebP("images/brand/"+file, ".png") + return ctx.File("images/brand/" + ctx.Get("file")) }) // Cover image diff --git a/config.json b/config.json index 6b208020..c3bbc092 100644 --- a/config.json +++ b/config.json @@ -35,20 +35,20 @@ "background_color": "#ffffff", "icons": [ { - "src": "images/brand/64", + "src": "images/brand/64.png", "sizes": "64x64" }, { - "src": "images/brand/144", + "src": "images/brand/144.png", "sizes": "144x144", "type": "image/png" }, { - "src": "images/brand/300", + "src": "images/brand/300.png", "sizes": "300x300" }, { - "src": "images/brand/600", + "src": "images/brand/600.png", "sizes": "600x600" } ] diff --git a/images/elements/noise-strong.png b/images/elements/noise-strong.png index 3672d82fcd8dc48bbd97293d769682106e5297e9..836dbceaaabe5fc726441b69d61dffefa411cff3 100644 GIT binary patch delta 3153 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 JWt~$(69DogQR@Hz literal 4297 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 diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 8ba102b8..00ea9327 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -3,7 +3,7 @@ component Sidebar(user *arn.User) if user != nil Avatar(user) else - img.user-image.lazy(data-src="/images/brand/64", alt="Anime Notifier") + img.user-image.lazy(data-src="/images/brand/64.png", data-webp="true", alt="Anime Notifier") if user != nil SidebarButton("Home", "/animelist/watching", "home") diff --git a/mixins/StatusTabs.pixy b/mixins/StatusTabs.pixy index a526a69b..c714b0ab 100644 --- a/mixins/StatusTabs.pixy +++ b/mixins/StatusTabs.pixy @@ -1,21 +1,12 @@ component StatusTabs(urlPrefix string) .tabs.status-tabs - a.tab.status-tab.action(href=urlPrefix + "/watching", data-action="diff", data-trigger="click") - Icon("play") - span.tab-text Watching - - a.tab.status-tab.action(href=urlPrefix + "/completed", data-action="diff", data-trigger="click") - Icon("check") - span.tab-text Completed + StatusTab("Watching", "play", urlPrefix + "/watching") + StatusTab("Completed", "check", urlPrefix + "/completed") + StatusTab("Planned", "forward", urlPrefix + "/planned") + StatusTab("On Hold", "pause", urlPrefix + "/hold") + StatusTab("Dropped", "stop", urlPrefix + "/dropped") - a.tab.status-tab.action(href=urlPrefix + "/planned", data-action="diff", data-trigger="click") - Icon("forward") - span.tab-text Planned - - a.tab.status-tab.action(href=urlPrefix + "/hold", data-action="diff", data-trigger="click") - Icon("pause") - span.tab-text On Hold - - a.tab.status-tab.action(href=urlPrefix + "/dropped", data-action="diff", data-trigger="click") - Icon("stop") - span.tab-text Dropped \ No newline at end of file +component StatusTab(label string, icon string, url string) + a.tab.status-tab.action(href=url, data-action="diff", data-trigger="click", aria-label=label) + Icon(icon) + span.tab-text= label \ No newline at end of file diff --git a/pages/frontpage/frontpage.go b/pages/frontpage/frontpage.go index 81cbf7d0..d8a69afb 100644 --- a/pages/frontpage/frontpage.go +++ b/pages/frontpage/frontpage.go @@ -16,7 +16,7 @@ func Get(ctx *aero.Context) string { "og:description": description, "og:type": "website", "og:url": "https://" + ctx.App.Config.Domain, - "og:image": "https://" + ctx.App.Config.Domain + "/images/brand/600", + "og:image": "https://" + ctx.App.Config.Domain + "/images/brand/600.png", }, Meta: map[string]string{ "description": description, diff --git a/pages/notifications/notifications.go b/pages/notifications/notifications.go index 0ff4fc4d..787c0716 100644 --- a/pages/notifications/notifications.go +++ b/pages/notifications/notifications.go @@ -19,7 +19,7 @@ func Test(ctx *aero.Context) string { notification := &arn.Notification{ Title: "Anime Notifier", Message: "Yay, it works!", - Icon: "https://" + ctx.App.Config.Domain + "/images/brand/300", + Icon: "https://" + ctx.App.Config.Domain + "/images/brand/300.png", } user.SendNotification(notification) From 531a502721b7a841c6b6ed27d0693a21ec98a077 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 01:30:50 +0200 Subject: [PATCH 353/527] New weighting for the explore page --- jobs/airing-anime/airing-anime.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/jobs/airing-anime/airing-anime.go b/jobs/airing-anime/airing-anime.go index 7b81a063..96d41214 100644 --- a/jobs/airing-anime/airing-anime.go +++ b/jobs/airing-anime/airing-anime.go @@ -8,10 +8,11 @@ import ( ) const ( - currentlyAiringBonus = 4.0 + currentlyAiringBonus = 5.0 popularityThreshold = 5 - popularityPenalty = 4.0 - watchingPopularityWeight = 0.2 + popularityPenalty = 8.0 + watchingPopularityWeight = 0.3 + plannedPopularityWeight = 0.2 ) func main() { @@ -50,6 +51,9 @@ func main() { scoreA += float64(a.Popularity.Watching) * watchingPopularityWeight scoreB += float64(b.Popularity.Watching) * watchingPopularityWeight + scoreA += float64(a.Popularity.Planned) * plannedPopularityWeight + scoreB += float64(b.Popularity.Planned) * plannedPopularityWeight + return scoreA > scoreB }) From 6adcdffa33656c7ad6f44f3aeaee697a1cc9a144 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 02:02:07 +0200 Subject: [PATCH 354/527] Sidebar singleton --- scripts/AnimeNotifier.ts | 20 +++++--------------- scripts/SideBar.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 scripts/SideBar.ts diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 8d5d5e36..b9a78533 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -8,6 +8,7 @@ import { StatusMessage } from "./StatusMessage" import { PushManager } from "./PushManager" import { TouchController } from "./TouchController" import { Analytics } from "./Analytics" +import { SideBar } from "./SideBar" export class AnimeNotifier { app: Application @@ -20,7 +21,7 @@ export class AnimeNotifier { visibilityObserver: IntersectionObserver pushManager: PushManager touchController: TouchController - sideBar: HTMLElement + sideBar: SideBar mainPageLoaded: boolean lastReloadContentPath: string @@ -114,9 +115,6 @@ export class AnimeNotifier { this.app.find("status-message-text") ) - // Let"s start - this.app.run() - // Push manager this.pushManager = new PushManager() @@ -124,18 +122,10 @@ export class AnimeNotifier { this.analytics = new Analytics() // Sidebar control - this.sideBar = this.app.find("sidebar") + this.sideBar = new SideBar(this.app.find("sidebar")) - document.body.addEventListener("click", e => { - if(document.activeElement.id === "search") - return; - - this.sideBar.classList.remove("sidebar-visible") - }) - - this.touchController = new TouchController() - this.touchController.leftSwipe = () => this.sideBar.classList.remove("sidebar-visible") - this.touchController.rightSwipe = () => this.sideBar.classList.add("sidebar-visible") + // Let"s start + this.app.run() } onContentLoaded() { diff --git a/scripts/SideBar.ts b/scripts/SideBar.ts new file mode 100644 index 00000000..aa3e7d02 --- /dev/null +++ b/scripts/SideBar.ts @@ -0,0 +1,29 @@ +import { TouchController } from "./TouchController" + +export class SideBar { + element: HTMLElement + touchController: TouchController + + constructor(element) { + this.element = element + + document.body.addEventListener("click", e => { + if(document.activeElement.id === "search") + return; + + this.hide() + }) + + this.touchController = new TouchController() + this.touchController.leftSwipe = () => this.hide() + this.touchController.rightSwipe = () => this.show() + } + + show() { + this.element.classList.add("sidebar-visible") + } + + hide() { + this.element.classList.remove("sidebar-visible") + } +} \ No newline at end of file From f1da605c3d1e3ebde2f8a54d39cd714cd1a9b389 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 02:03:13 +0200 Subject: [PATCH 355/527] Cleanup --- scripts/APIObject.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 scripts/APIObject.ts diff --git a/scripts/APIObject.ts b/scripts/APIObject.ts deleted file mode 100644 index 481cb4de..00000000 --- a/scripts/APIObject.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Save new data from an input field -// export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) { - // let apiObject: HTMLElement - // let parent = input as HTMLElement - - // while(parent = parent.parentElement) { - // if(parent.classList.contains("api-object")) { - // apiObject = parent - // break - // } - // } - - // if(!apiObject) { - // throw "API object not found" - // } - - // let request = apiObject["api-fetch"] - - // request.then(obj => { - // obj[input.id] = input.value - // console.log(obj) - // }) -// } - -// updateAPIObjects() { -// for(let element of findAll(".api-object")) { -// let apiObject = element - -// apiObject["api-fetch"] = fetch(element.dataset.api).then(response => response.json()) -// } -// } \ No newline at end of file From 083ef6474693427731b0937cdeba0ce6861b535c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 02:06:59 +0200 Subject: [PATCH 356/527] Test fix --- tests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.go b/tests.go index ff152fdc..3151cd85 100644 --- a/tests.go +++ b/tests.go @@ -31,7 +31,7 @@ var routeTests = map[string][]string{ }, "/user/:nick/animelist/anime/:id": []string{ - "/+Akyoto/animelist/7929", + "/+Akyoto/animelist/anime/7929", }, "/user/:nick/animelist/watching": []string{ From 4e5472260d7602b3d972bcb0c129d2392371443c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 02:09:05 +0200 Subject: [PATCH 357/527] Pass test --- assets.go | 3 ++- main.go | 5 ++++- rewrite.go | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/assets.go b/assets.go index e4bfa579..916b240e 100644 --- a/assets.go +++ b/assets.go @@ -8,7 +8,8 @@ import ( "github.com/animenotifier/notify.moe/components/js" ) -func init() { +// configureAssets adds all the routes used for media assets. +func configureAssets(app *aero.Application) { // Script bundle scriptBundle := js.Bundle() diff --git a/main.go b/main.go index 12c0c193..53e32519 100644 --- a/main.go +++ b/main.go @@ -153,8 +153,11 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/paypal/cancel", paypal.Cancel) app.Get("/api/paypal/payment/create", paypal.CreatePayment) + // Assets + configureAssets(app) + // Rewrite - app.Rewrite(Rewrite) + app.Rewrite(rewrite) // Middleware app.Use(middleware.Firewall()) diff --git a/rewrite.go b/rewrite.go index 60096ead..3cf40ce4 100644 --- a/rewrite.go +++ b/rewrite.go @@ -6,8 +6,8 @@ import ( "github.com/aerogo/aero" ) -// Rewrite will rewrite certain routes -func Rewrite(ctx *aero.RewriteContext) { +// rewrite will rewrite certain routes +func rewrite(ctx *aero.RewriteContext) { requestURI := ctx.URI() // User profiles From 93ed7378ae2dcb946476d52e8d473e362cdba1b8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 06:29:58 +0200 Subject: [PATCH 358/527] Cleanup --- main.go | 10 ++++++---- mixins/Sidebar.pixy | 3 +++ pages/dashboard/dashboard.go | 5 +++++ pages/dashboard/dashboard.pixy | 3 ++- pages/{tracks/tracks.go => soundtrack/soundtrack.go} | 2 +- .../{tracks/tracks.pixy => soundtrack/soundtrack.pixy} | 0 pages/{music/music.go => soundtracks/soundtracks.go} | 2 +- .../{music/music.pixy => soundtracks/soundtracks.pixy} | 0 .../music.scarlet => soundtracks/soundtracks.scarlet} | 0 9 files changed, 18 insertions(+), 7 deletions(-) rename pages/{tracks/tracks.go => soundtrack/soundtrack.go} (97%) rename pages/{tracks/tracks.pixy => soundtrack/soundtrack.pixy} (100%) rename pages/{music/music.go => soundtracks/soundtracks.go} (96%) rename pages/{music/music.pixy => soundtracks/soundtracks.pixy} (100%) rename pages/{music/music.scarlet => soundtracks/soundtracks.scarlet} (100%) diff --git a/main.go b/main.go index 53e32519..68db0cf7 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/animenotifier/notify.moe/pages/artworks" "github.com/animenotifier/notify.moe/pages/best" "github.com/animenotifier/notify.moe/pages/character" + "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/editor" "github.com/animenotifier/notify.moe/pages/embed" @@ -31,7 +32,6 @@ import ( "github.com/animenotifier/notify.moe/pages/listimport/listimportmyanimelist" "github.com/animenotifier/notify.moe/pages/login" "github.com/animenotifier/notify.moe/pages/me" - "github.com/animenotifier/notify.moe/pages/music" "github.com/animenotifier/notify.moe/pages/newsoundtrack" "github.com/animenotifier/notify.moe/pages/newthread" "github.com/animenotifier/notify.moe/pages/notifications" @@ -40,9 +40,10 @@ import ( "github.com/animenotifier/notify.moe/pages/profile" "github.com/animenotifier/notify.moe/pages/search" "github.com/animenotifier/notify.moe/pages/settings" + "github.com/animenotifier/notify.moe/pages/soundtrack" + "github.com/animenotifier/notify.moe/pages/soundtracks" "github.com/animenotifier/notify.moe/pages/statistics" "github.com/animenotifier/notify.moe/pages/threads" - "github.com/animenotifier/notify.moe/pages/tracks" "github.com/animenotifier/notify.moe/pages/user" "github.com/animenotifier/notify.moe/pages/users" "github.com/animenotifier/notify.moe/pages/webdev" @@ -71,6 +72,7 @@ func configure(app *aero.Application) *aero.Application { // Ajax routes app.Ajax("/", home.Get) + app.Ajax("/dashboard", dashboard.Get) app.Ajax("/anime/:id", anime.Get) app.Ajax("/anime/:id/edit", editanime.Get) app.Ajax("/api", apiview.Get) @@ -80,12 +82,12 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/forum/:tag", forum.Get) app.Ajax("/thread/:id", threads.Get) app.Ajax("/post/:id", posts.Get) - app.Ajax("/soundtrack/:id", tracks.Get) + app.Ajax("/soundtrack/:id", soundtrack.Get) app.Ajax("/character/:id", character.Get) app.Ajax("/new/thread", newthread.Get) app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) - app.Ajax("/soundtracks", music.Get) + app.Ajax("/soundtracks", soundtracks.Get) app.Ajax("/artworks", artworks.Get) app.Ajax("/amvs", amvs.Get) app.Ajax("/users", users.Active) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 00ea9327..ab70f539 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -23,6 +23,9 @@ component Sidebar(user *arn.User) SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") + if user.Role == "admin" + SidebarButton("Admin", "/admin", "wrench") + .spacer .sidebar-link(aria-label="Search") diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index fe0649ae..ae7bfae0 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -1,6 +1,7 @@ package dashboard import ( + "net/http" "sort" "github.com/aerogo/aero" @@ -24,6 +25,10 @@ func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + flow.Parallel(func() { forumActivity, _ = arn.GetForumActivityCached() }, func() { diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index bb8f161b..8a0f3230 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -122,7 +122,8 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound //- .widget-element-text //- Icon("github") //- span GitHub - + +component Footer .footer.text-center span.footer-element Anime Notifier diff --git a/pages/tracks/tracks.go b/pages/soundtrack/soundtrack.go similarity index 97% rename from pages/tracks/tracks.go rename to pages/soundtrack/soundtrack.go index 8702f344..f1f13266 100644 --- a/pages/tracks/tracks.go +++ b/pages/soundtrack/soundtrack.go @@ -1,4 +1,4 @@ -package tracks +package soundtrack import ( "net/http" diff --git a/pages/tracks/tracks.pixy b/pages/soundtrack/soundtrack.pixy similarity index 100% rename from pages/tracks/tracks.pixy rename to pages/soundtrack/soundtrack.pixy diff --git a/pages/music/music.go b/pages/soundtracks/soundtracks.go similarity index 96% rename from pages/music/music.go rename to pages/soundtracks/soundtracks.go index da00dd90..1d1f6c3b 100644 --- a/pages/music/music.go +++ b/pages/soundtracks/soundtracks.go @@ -1,4 +1,4 @@ -package music +package soundtracks import ( "net/http" diff --git a/pages/music/music.pixy b/pages/soundtracks/soundtracks.pixy similarity index 100% rename from pages/music/music.pixy rename to pages/soundtracks/soundtracks.pixy diff --git a/pages/music/music.scarlet b/pages/soundtracks/soundtracks.scarlet similarity index 100% rename from pages/music/music.scarlet rename to pages/soundtracks/soundtracks.scarlet From 38fd739009d9397b8d108396fee9b91aa1a1b7d1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 06:59:29 +0200 Subject: [PATCH 359/527] Improved test command --- go-test-color.sh | 114 +++++++++++++++++++++++++++++++++++++++++++++++ makefile | 4 +- tests.go | 1 + 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100755 go-test-color.sh diff --git a/go-test-color.sh b/go-test-color.sh new file mode 100755 index 00000000..e184ba1c --- /dev/null +++ b/go-test-color.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +# Colorizing Go test output: +# This is meant to be used in place of `go test`. Provided this script is in +# your PATH, calling `color-go-test` will call through to `go test` and then +# colorize and reformat the output. + +RED=$(tput setaf 1) +GREEN=$(tput setaf 2) +YELLOW=$(tput setaf 3) +COLOR_RESET=$(tput sgr0) +BOLD=$(tput bold) + +previous_line_fail=false +verbose_output=false +verbose_flag_prefix="-v " +pass_count=0 +fail_count=0 +errors=() + +echo_last_line() { + local time_string=$1 + local color=$GREEN + if [ $verbose_output = false ]; then + echo -e "\n" + if [ ${#errors[@]} -gt 0 ]; then + for error in "${errors[@]}"; do + echo -e "$error" + done + fi + fi + if [ $fail_count -gt 0 ]; then + color=$RED + fi + local num_tests=$((pass_count + fail_count)) + echo -e "\n${color}${BOLD}$num_tests tests, $fail_count failure, run time ($time_string)${COLOR_RESET}" +} + +colorize_output() { + while read line; do + if echo $line | grep --quiet '^FAIL$'; then + continue + + elif echo $line | grep --quiet '^PASS$'; then + continue + + elif echo $line | grep --quiet '^=== RUN'; then + continue + + elif echo $line | grep --quiet '^exit status 1$'; then + continue + + elif echo $line | grep --quiet 'FAIL'; then + if echo $line | grep --quiet "\-\-\- FAIL:"; then + fail_count=$((fail_count + 1)) + error_message="${RED}$(echo $line | sed 's/--- FAIL:/✗/')${COLOR_RESET}" + + if [ $verbose_output = true ]; then + echo $error_message + else + errors+=("$error_message") + printf "${RED}.${COLOR_RESET}" + fi + previous_line_fail=true + else + local test_run_time=$(echo $line | grep -o '[0-9]*\.[0-9]*s$') + echo_last_line $test_run_time + previous_line_fail=false + fi + + elif [ $previous_line_fail = true ]; then + error_message=" ${YELLOW}➝ $line${COLOR_RESET}" + if [ $verbose_output = true ]; then + echo -e "$error_message" + else + errors+=("$error_message") + fi + previous_line_fail=false + + elif echo $line | grep --quiet 'PASS'; then + if echo $line | grep --quiet "\-\-\- PASS:"; then + if [ $verbose_output = true ]; then + echo "${GREEN}$(echo $line | sed 's/--- PASS:/✔/')${COLOR_RESET}" + else + printf "${GREEN}.${COLOR_RESET}" + fi + pass_count=$((pass_count + 1)) + else + local test_run_time=$(echo $line | grep -o '[0-9]*\.[0-9]*s$') + echo_last_line $test_run_time + fi + + previous_line_fail=false + + elif echo $line | grep --quiet '^ok '; then + local test_run_time=$(echo $line | grep -o '[0-9]*\.[0-9]*s$') + echo_last_line $test_run_time + previous_line_fail=false + + else + echo $line + previous_line_fail=false + fi + done +} + +for flag in $@; do + if [ "$flag" = "-v" ]; then + verbose_output=true + verbose_flag_prefix="" + fi +done + +go test ${verbose_flag_prefix}$@ | colorize_output diff --git a/makefile b/makefile index 419a9886..cb0dda73 100644 --- a/makefile +++ b/makefile @@ -3,7 +3,7 @@ GOCMD=@go GOBUILD=$(GOCMD) build GOINSTALL=$(GOCMD) install -GOTEST=$(GOCMD) test +GOTEST=@./go-test-color.sh BUILDJOBS=@./jobs/build.sh BUILDPATCHES=@./patches/build.sh BUILDBOTS=@./bots/build.sh @@ -23,7 +23,7 @@ js: install: $(GOINSTALL) test: - $(GOTEST) github.com/animenotifier/... -v + $(GOTEST) github.com/animenotifier/... -v -cover bench: $(GOTEST) -bench . tools: diff --git a/tests.go b/tests.go index 3151cd85..d4079096 100644 --- a/tests.go +++ b/tests.go @@ -210,6 +210,7 @@ var routeTests = map[string][]string{ "/auth/google/callback": nil, "/auth/facebook": nil, "/auth/facebook/callback": nil, + "/dashboard": nil, "/import": nil, "/import/anilist/animelist": nil, "/import/anilist/animelist/finish": nil, From 28d307201dc67696dbba71c0690890488dc1c7c0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 08:46:10 +0200 Subject: [PATCH 360/527] Started working on automatic tests --- jobs/test/test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 jobs/test/test.go diff --git a/jobs/test/test.go b/jobs/test/test.go new file mode 100644 index 00000000..c7c4cae5 --- /dev/null +++ b/jobs/test/test.go @@ -0,0 +1,20 @@ +package main + +import ( + "os" + "os/exec" +) + +func main() { + cmd := exec.Command("go", "test", "github.com/animenotifier/notify.moe") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Start() + + if err != nil { + panic(err) + } + + cmd.Wait() +} From bbbe311ac5287047a06345b3c93558d200bf94d0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 11:22:17 +0200 Subject: [PATCH 361/527] Added test job --- jobs/jobs.go | 1 + jobs/test/test.go | 58 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/jobs/jobs.go b/jobs/jobs.go index 9f3a86d1..edfb0049 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -30,6 +30,7 @@ var jobs = map[string]time.Duration{ "statistics": 15 * time.Minute, "popular-anime": 20 * time.Minute, "avatars": 30 * time.Minute, + "test": 1 * time.Hour, "twist": 1 * time.Hour, "search-index": 2 * time.Hour, "sync-shoboi": 8 * time.Hour, diff --git a/jobs/test/test.go b/jobs/test/test.go index c7c4cae5..0fc58c8d 100644 --- a/jobs/test/test.go +++ b/jobs/test/test.go @@ -1,14 +1,46 @@ package main import ( - "os" "os/exec" + "sync" + + "github.com/animenotifier/arn" + + "github.com/fatih/color" ) +var packages = []string{ + "github.com/animenotifier/notify.moe", + "github.com/animenotifier/arn", + "github.com/animenotifier/kitsu", + "github.com/animenotifier/anilist", + "github.com/animenotifier/mal", + "github.com/animenotifier/shoboi", + "github.com/animenotifier/twist", + "github.com/animenotifier/avatar", + "github.com/animenotifier/japanese", + // "github.com/animenotifier/osu", +} + func main() { - cmd := exec.Command("go", "test", "github.com/animenotifier/notify.moe") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + wg := sync.WaitGroup{} + + for _, pkg := range packages { + wg.Add(1) + + go func(pkgLocal string) { + testPackage(pkgLocal) + wg.Done() + }(pkg) + } + + wg.Wait() +} + +func testPackage(pkg string) { + cmd := exec.Command("go", "test", pkg+"/...") + // cmd.Stdout = os.Stdout + // cmd.Stderr = os.Stderr err := cmd.Start() @@ -16,5 +48,21 @@ func main() { panic(err) } - cmd.Wait() + err = cmd.Wait() + + if err != nil { + color.Red("%s", pkg) + + // Send notification to the admin + admin, _ := arn.GetUser("4J6qpK1ve") + admin.SendNotification(&arn.Notification{ + Title: pkg, + Message: "Test failed", + Link: "https://" + pkg, + Icon: "https://notify.moe/images/brand/300.png", + }) + return + } + + color.Green("%s", pkg) } From 5824ea4c8bcd49b9ae80fe9ba6a15d9eb65d6a80 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 12:10:55 +0200 Subject: [PATCH 362/527] Minor fix --- pages/anime/anime.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 8adf86b2..4b06618d 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -210,7 +210,7 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* RawIcon("eye") //- a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") //- RawIcon("google") - if episode.AiringDate.IsValid() + if validator.IsValidDate(episode.AiringDate.Start) td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() else td.episode-airing-date-start From ffdc015f716c875c21631a222479c732d7f7df0e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 12:21:17 +0200 Subject: [PATCH 363/527] Changed avatar update time --- jobs/jobs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/jobs.go b/jobs/jobs.go index edfb0049..5e0dca87 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -29,7 +29,7 @@ var jobs = map[string]time.Duration{ "airing-anime": 10 * time.Minute, "statistics": 15 * time.Minute, "popular-anime": 20 * time.Minute, - "avatars": 30 * time.Minute, + "avatars": 1 * time.Hour, "test": 1 * time.Hour, "twist": 1 * time.Hour, "search-index": 2 * time.Hour, From 8ace9a6538ab723b72bea80145dcc6d601ff1ca7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 13:18:27 +0200 Subject: [PATCH 364/527] Fixed status messages --- styles/status-message.scarlet | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet index 7b0aaf62..f7938bc8 100644 --- a/styles/status-message.scarlet +++ b/styles/status-message.scarlet @@ -6,6 +6,7 @@ width 100% padding calc(content-padding / 2) content-padding pointer-events none + z-index 1000 #status-message-text flex 1 From b0de2045d7fca4ed5379baff6d270f5db1af8e5a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 14:28:10 +0200 Subject: [PATCH 365/527] Improved admin page --- pages/admin/admin.go | 51 ++++++++++++++++++++++++++- pages/admin/admin.pixy | 78 +++++++++++++++++++++++++++++++++--------- sw/service-worker.ts | 1 + 3 files changed, 112 insertions(+), 18 deletions(-) diff --git a/pages/admin/admin.go b/pages/admin/admin.go index 9ef619e3..94912e64 100644 --- a/pages/admin/admin.go +++ b/pages/admin/admin.go @@ -1,9 +1,16 @@ package admin import ( + "time" + "github.com/aerogo/aero" + "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/mem" ) // Get admin page. @@ -14,5 +21,47 @@ func Get(ctx *aero.Context) string { return ctx.Redirect("/") } - return ctx.HTML(components.Admin(user)) + // CPU + cpuUsage := 0.0 + cpuUsages, err := cpu.Percent(1*time.Second, false) + + if err == nil { + cpuUsage = cpuUsages[0] + } + + // Memory + memUsage := 0.0 + memInfo, _ := mem.VirtualMemory() + + if err == nil { + memUsage = memInfo.UsedPercent + } + + // Disk + diskUsage := 0.0 + diskInfo, err := disk.Usage("/") + + if err == nil { + diskUsage = diskInfo.UsedPercent + } + + // Host + platform, family, platformVersion, _ := host.PlatformInformation() + kernelVersion, err := host.KernelVersion() + + return ctx.HTML(components.Admin(user, cpuUsage, memUsage, diskUsage, platform, family, platformVersion, kernelVersion)) +} + +func average(floatSlice []float64) float64 { + if len(floatSlice) == 0 { + return arn.DefaultAverageRating + } + + var sum float64 + + for _, value := range floatSlice { + sum += value + } + + return sum / float64(len(floatSlice)) } diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index e4d08109..77d8b121 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -1,19 +1,63 @@ -component Admin(user *arn.User) +component Admin(user *arn.User, cpuUsage, memUsage, diskUsage float64, platform, family, platformVersion, kernelVersion string) h1.page-title Admin Panel - h3 Server - table - //- thead - //- tr - //- th Metric - //- th Value - tbody - tr - td CPU count: - td= runtime.NumCPU() - tr - td Goroutines: - td= runtime.NumGoroutine() - tr - td Go version: - td= runtime.Version() \ No newline at end of file + .widgets + .widget.mountable + h3.widget-title Usage + + table + tbody + tr + td CPU usage: + td + span= int(cpuUsage + 0.5) + span % + tr + td Memory usage: + td + span= int(memUsage + 0.5) + span % + tr + td Disk usage: + td + span= int(diskUsage + 0.5) + span % + + .widget.mountable + h3.widget-title OS + + table + tbody + tr + td Platform: + td= platform + tr + td Family: + td= family + tr + td Version: + td= platformVersion + tr + td Kernel: + td= kernelVersion + + .widget.mountable + h3.widget-title Hardware + + table + tbody + tr + td CPUs: + td= runtime.NumCPU() + + .widget.mountable + h3.widget-title Go + + table + tbody + tr + td Version: + td= runtime.Version() + tr + td Goroutines: + td= runtime.NumGoroutine() \ No newline at end of file diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 6f800784..da79784a 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -6,6 +6,7 @@ const ETAGS = new Map() const CACHEREFRESH = new Map>() const EXCLUDECACHE = new Set([ "/api/", + "/admin/", "/paypal/", "/import/", "chrome-extension" From cc2418cd96faf4792276a42cdf404c773ca9d793 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 14:56:51 +0200 Subject: [PATCH 366/527] Improved admin panel --- main.go | 10 ++++------ mixins/Sidebar.pixy | 2 +- mixins/StatusTabs.pixy | 17 ++++++----------- mixins/Tab.pixy | 4 ++++ pages/admin/admin.go | 2 +- pages/admin/admin.pixy | 8 ++++++++ pages/{editor/editor.go => admin/anilist.go} | 6 +++--- .../{editor/editor.pixy => admin/anilist.pixy} | 4 +++- pages/{webdev => admin}/webdev.go | 6 +++--- pages/{webdev => admin}/webdev.pixy | 2 ++ pages/profile/watching.scarlet | 16 +--------------- tests.go | 2 +- 12 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 mixins/Tab.pixy rename pages/{editor/editor.go => admin/anilist.go} (88%) rename pages/{editor/editor.pixy => admin/anilist.pixy} (87%) rename pages/{webdev => admin}/webdev.go (65%) rename pages/{webdev => admin}/webdev.pixy (98%) diff --git a/main.go b/main.go index 68db0cf7..8a0224aa 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,6 @@ import ( "github.com/animenotifier/notify.moe/pages/character" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" - "github.com/animenotifier/notify.moe/pages/editor" "github.com/animenotifier/notify.moe/pages/embed" "github.com/animenotifier/notify.moe/pages/explore" "github.com/animenotifier/notify.moe/pages/forum" @@ -46,7 +45,6 @@ import ( "github.com/animenotifier/notify.moe/pages/threads" "github.com/animenotifier/notify.moe/pages/user" "github.com/animenotifier/notify.moe/pages/users" - "github.com/animenotifier/notify.moe/pages/webdev" ) var app = aero.New() @@ -94,6 +92,8 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/users/osu", users.Osu) app.Ajax("/users/staff", users.Staff) app.Ajax("/users/anime/watching", users.AnimeWatching) + app.Ajax("/statistics", statistics.Get) + app.Ajax("/statistics/anime", statistics.Anime) app.Ajax("/login", login.Get) // User profiles @@ -125,10 +125,8 @@ func configure(app *aero.Application) *aero.Application { // Admin app.Ajax("/admin", admin.Get) - app.Ajax("/editor", editor.Get) - app.Ajax("/statistics", statistics.Get) - app.Ajax("/statistics/anime", statistics.Anime) - app.Ajax("/webdev", webdev.Get) + app.Ajax("/admin/anilist", admin.AniList) + app.Ajax("/admin/webdev", admin.WebDev) // Import app.Ajax("/import", listimport.Get) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index ab70f539..b6b66e8e 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -23,7 +23,7 @@ component Sidebar(user *arn.User) SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") - if user.Role == "admin" + if user.Role == "admin" || user.Role == "editor" SidebarButton("Admin", "/admin", "wrench") .spacer diff --git a/mixins/StatusTabs.pixy b/mixins/StatusTabs.pixy index c714b0ab..7732ab3b 100644 --- a/mixins/StatusTabs.pixy +++ b/mixins/StatusTabs.pixy @@ -1,12 +1,7 @@ component StatusTabs(urlPrefix string) - .tabs.status-tabs - StatusTab("Watching", "play", urlPrefix + "/watching") - StatusTab("Completed", "check", urlPrefix + "/completed") - StatusTab("Planned", "forward", urlPrefix + "/planned") - StatusTab("On Hold", "pause", urlPrefix + "/hold") - StatusTab("Dropped", "stop", urlPrefix + "/dropped") - -component StatusTab(label string, icon string, url string) - a.tab.status-tab.action(href=url, data-action="diff", data-trigger="click", aria-label=label) - Icon(icon) - span.tab-text= label \ No newline at end of file + .tabs + Tab("Watching", "play", urlPrefix + "/watching") + Tab("Completed", "check", urlPrefix + "/completed") + Tab("Planned", "forward", urlPrefix + "/planned") + Tab("On Hold", "pause", urlPrefix + "/hold") + Tab("Dropped", "stop", urlPrefix + "/dropped") \ No newline at end of file diff --git a/mixins/Tab.pixy b/mixins/Tab.pixy new file mode 100644 index 00000000..c4582768 --- /dev/null +++ b/mixins/Tab.pixy @@ -0,0 +1,4 @@ +component Tab(label string, icon string, url string) + a.tab.action(href=url, data-action="diff", data-trigger="click", aria-label=label) + Icon(icon) + span.tab-text= label \ No newline at end of file diff --git a/pages/admin/admin.go b/pages/admin/admin.go index 94912e64..cf674966 100644 --- a/pages/admin/admin.go +++ b/pages/admin/admin.go @@ -17,7 +17,7 @@ import ( func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) - if user == nil || user.Role != "admin" { + if user == nil || (user.Role != "admin" && user.Role != "editor") { return ctx.Redirect("/") } diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index 77d8b121..bee55b01 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -1,6 +1,14 @@ +component AdminTabs + .tabs + Tab("Server", "server", "/admin") + Tab("AniList", "list", "/admin/anilist") + Tab("WebDev", "html5", "/admin/webdev") + component Admin(user *arn.User, cpuUsage, memUsage, diskUsage float64, platform, family, platformVersion, kernelVersion string) h1.page-title Admin Panel + AdminTabs + .widgets .widget.mountable h3.widget-title Usage diff --git a/pages/editor/editor.go b/pages/admin/anilist.go similarity index 88% rename from pages/editor/editor.go rename to pages/admin/anilist.go index 73de1f37..bcd74838 100644 --- a/pages/editor/editor.go +++ b/pages/admin/anilist.go @@ -1,4 +1,4 @@ -package editor +package admin import ( "net/http" @@ -9,8 +9,8 @@ import ( "github.com/animenotifier/notify.moe/components" ) -// Get ... -func Get(ctx *aero.Context) string { +// AniList ... +func AniList(ctx *aero.Context) string { missing, err := arn.FilterAnime(func(anime *arn.Anime) bool { return anime.GetMapping("anilist/anime") == "" }) diff --git a/pages/editor/editor.pixy b/pages/admin/anilist.pixy similarity index 87% rename from pages/editor/editor.pixy rename to pages/admin/anilist.pixy index f9a1cd52..cbed563c 100644 --- a/pages/editor/editor.pixy +++ b/pages/admin/anilist.pixy @@ -1,5 +1,7 @@ component AniListMissingMapping(missing []*arn.Anime) - h1 Anime without Anilist links + h1.page-title Anime without Anilist links + + AdminTabs table tbody diff --git a/pages/webdev/webdev.go b/pages/admin/webdev.go similarity index 65% rename from pages/webdev/webdev.go rename to pages/admin/webdev.go index e25bcd67..15d7c97c 100644 --- a/pages/webdev/webdev.go +++ b/pages/admin/webdev.go @@ -1,9 +1,9 @@ -package webdev +package admin import "github.com/aerogo/aero" import "github.com/animenotifier/notify.moe/components" -// Get ... -func Get(ctx *aero.Context) string { +// WebDev ... +func WebDev(ctx *aero.Context) string { return ctx.HTML(components.WebDev()) } diff --git a/pages/webdev/webdev.pixy b/pages/admin/webdev.pixy similarity index 98% rename from pages/webdev/webdev.pixy rename to pages/admin/webdev.pixy index 11ae5d3b..ffe3db70 100644 --- a/pages/webdev/webdev.pixy +++ b/pages/admin/webdev.pixy @@ -1,6 +1,8 @@ component WebDev h1.page-title WebDev + AdminTabs + .light-button-group a.light-button(href="https://developers.google.com/speed/pagespeed/insights/?url=https://notify.moe/&tab=desktop", target="_blank", rel="noopener") Icon("external-link") diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 128749b9..0e37f03a 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -13,18 +13,4 @@ transition filter transition-speed ease, opacity transition-speed ease :hover - filter saturate(1.3) - -.status-tabs - // margin-top 2px -// position fixed -// top 4.6rem -// right 1.6rem -// flex-direction column - -// .status-tab -// font-size 0.9rem - -// < 380px -// .profile-watching-list -// justify-content center \ No newline at end of file + filter saturate(1.3) \ No newline at end of file diff --git a/tests.go b/tests.go index d4079096..65e568d8 100644 --- a/tests.go +++ b/tests.go @@ -229,7 +229,7 @@ var routeTests = map[string][]string{ "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, - "/editor": nil, + "/admin/anilist": nil, "/user": nil, "/settings": nil, "/extension/embed": nil, From ba445f6d74db893537690859b18b654fb56264e9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 15:12:17 +0200 Subject: [PATCH 367/527] Improved anilist connect --- patches/import-anilist/import-anilist.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go index 7711fb4b..17335d01 100644 --- a/patches/import-anilist/import-anilist.go +++ b/patches/import-anilist/import-anilist.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/animenotifier/anilist" "github.com/animenotifier/arn" "github.com/fatih/color" @@ -19,12 +17,17 @@ func main() { stream := anilist.StreamAnime() for aniListAnime := range stream { + println(aniListAnime.TitleRomaji) + anime := arn.FindAniListAnime(aniListAnime, allAnime) - if anime == nil { - fmt.Println(anime.ID, aniListAnime.TitleRomaji) + if anime != nil { + color.Green("%s %s", anime.ID, aniListAnime.TitleRomaji) + count++ + } else { + color.Red("Not found") } - - count++ } + + color.Green("%d anime are connected with AniList", count) } From 1806a623bcaf72507e1eb984749ba465bd6ff7f3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 15:33:39 +0200 Subject: [PATCH 368/527] Added Shoboi to admin view --- main.go | 1 + pages/admin/admin.pixy | 1 + pages/admin/shoboi.go | 27 +++++++++++++++++++++++++++ pages/admin/shoboi.pixy | 16 ++++++++++++++++ tests.go | 1 + 5 files changed, 46 insertions(+) create mode 100644 pages/admin/shoboi.go create mode 100644 pages/admin/shoboi.pixy diff --git a/main.go b/main.go index 8a0224aa..cacc69c1 100644 --- a/main.go +++ b/main.go @@ -126,6 +126,7 @@ func configure(app *aero.Application) *aero.Application { // Admin app.Ajax("/admin", admin.Get) app.Ajax("/admin/anilist", admin.AniList) + app.Ajax("/admin/shoboi", admin.Shoboi) app.Ajax("/admin/webdev", admin.WebDev) // Import diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index bee55b01..247c6719 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -1,6 +1,7 @@ component AdminTabs .tabs Tab("Server", "server", "/admin") + Tab("Shoboi", "calendar", "/admin/shoboi") Tab("AniList", "list", "/admin/anilist") Tab("WebDev", "html5", "/admin/webdev") diff --git a/pages/admin/shoboi.go b/pages/admin/shoboi.go new file mode 100644 index 00000000..c4cdbd33 --- /dev/null +++ b/pages/admin/shoboi.go @@ -0,0 +1,27 @@ +package admin + +import ( + "net/http" + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +// Shoboi ... +func Shoboi(ctx *aero.Context) string { + missing, err := arn.FilterAnime(func(anime *arn.Anime) bool { + return anime.GetMapping("shoboi/anime") == "" + }) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Couldn't filter anime", err) + } + + sort.Slice(missing, func(i, j int) bool { + return missing[i].StartDate > missing[j].StartDate + }) + + return ctx.HTML(components.ShoboiMissingMapping(missing)) +} diff --git a/pages/admin/shoboi.pixy b/pages/admin/shoboi.pixy new file mode 100644 index 00000000..5224eae7 --- /dev/null +++ b/pages/admin/shoboi.pixy @@ -0,0 +1,16 @@ +component ShoboiMissingMapping(missing []*arn.Anime) + h1.page-title Anime without Shoboi links + + AdminTabs + + table + tbody + each anime in missing + tr + td + if len(anime.StartDate) >= 4 + span= anime.StartDate[:4] + td + a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical + td + a(href="http://cal.syoboi.jp/find?type=quick&sd=1&kw=" + anime.Title.Japanese, target="_blank", rel="noopener") Search diff --git a/tests.go b/tests.go index 65e568d8..3fe8f199 100644 --- a/tests.go +++ b/tests.go @@ -230,6 +230,7 @@ var routeTests = map[string][]string{ "/new/thread": nil, "/new/soundtrack": nil, "/admin/anilist": nil, + "/admin/shoboi": nil, "/user": nil, "/settings": nil, "/extension/embed": nil, From 52c830ba885bb495d699000909bac84f80f134d9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 16:39:23 +0200 Subject: [PATCH 369/527] Improved sync --- jobs/sync-shoboi/sync-shoboi.go | 46 +++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/jobs/sync-shoboi/sync-shoboi.go b/jobs/sync-shoboi/sync-shoboi.go index e0addec4..00da2251 100644 --- a/jobs/sync-shoboi/sync-shoboi.go +++ b/jobs/sync-shoboi/sync-shoboi.go @@ -11,22 +11,34 @@ import ( func main() { color.Yellow("Syncing Shoboi Anime") - // Get a slice of all anime - allAnime, _ := arn.AllAnime() + // Priority queues + highPriority := []*arn.Anime{} + mediumPriority := []*arn.Anime{} + lowPriority := []*arn.Anime{} - // Iterate over the slice - count := 0 - for _, anime := range allAnime { - if sync(anime) { - count++ + for anime := range arn.MustStreamAnime() { + if anime.GetMapping("shoboi/anime") != "" { + continue } - // Lower the request interval - time.Sleep(2 * time.Second) + switch anime.Status { + case "current": + highPriority = append(highPriority, anime) + case "upcoming": + mediumPriority = append(mediumPriority, anime) + default: + lowPriority = append(lowPriority, anime) + } } - // Log - color.Green("Successfully added Shoboi IDs for %d anime", count) + color.Cyan("High priority queue (%d):", len(highPriority)) + refreshQueue(highPriority) + + color.Cyan("Medium priority queue (%d):", len(mediumPriority)) + refreshQueue(mediumPriority) + + color.Cyan("Low priority queue (%d):", len(lowPriority)) + refreshQueue(lowPriority) // This is a lazy hack: Wait 5 minutes for goroutines to finish their remaining work. time.Sleep(5 * time.Minute) @@ -34,6 +46,18 @@ func main() { color.Green("Finished.") } +func refreshQueue(queue []*arn.Anime) { + count := 0 + + for _, anime := range queue { + if sync(anime) { + count++ + } + } + + color.Green("Added Shoboi IDs for %d anime", count) +} + func sync(anime *arn.Anime) bool { // If we already have the ID, nothing to do here if anime.GetMapping("shoboi/anime") != "" { From 5c9cbf2d2e0277c1295614ca9538bdff79f4790e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 17:29:42 +0200 Subject: [PATCH 370/527] Fixed sync --- jobs/sync-shoboi/sync-shoboi.go | 1 + 1 file changed, 1 insertion(+) diff --git a/jobs/sync-shoboi/sync-shoboi.go b/jobs/sync-shoboi/sync-shoboi.go index 00da2251..4624adda 100644 --- a/jobs/sync-shoboi/sync-shoboi.go +++ b/jobs/sync-shoboi/sync-shoboi.go @@ -51,6 +51,7 @@ func refreshQueue(queue []*arn.Anime) { for _, anime := range queue { if sync(anime) { + arn.PanicOnError(anime.Save()) count++ } } From 034000f1299ccb5a927e860a311253f7924757eb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 18:30:50 +0200 Subject: [PATCH 371/527] Fixed output --- jobs/refresh-episodes/refresh-episodes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index e3be83cd..d30177ea 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -71,7 +71,7 @@ func refresh(anime *arn.Anime) { episodes := anime.Episodes() fmt.Println(faint(episodes)) - fmt.Printf("+%d airing | +%d available (%d total)\n", len(episodes.Items), len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount) + fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes.Items)-episodeCount, len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount) println() } } From 48049f7866ee35adaee53d53373c5697bbd51330 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 18:56:58 +0200 Subject: [PATCH 372/527] Fixed output --- jobs/refresh-episodes/refresh-episodes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index d30177ea..5fcaca75 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -71,7 +71,7 @@ func refresh(anime *arn.Anime) { episodes := anime.Episodes() fmt.Println(faint(episodes)) - fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes.Items)-episodeCount, len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount) + fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount) println() } } From cf91fe37d07b00158e7987ed5e75ab35b6d6616d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 18:59:53 +0200 Subject: [PATCH 373/527] Fixed output --- jobs/refresh-episodes/refresh-episodes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 5fcaca75..d7d4f552 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -71,7 +71,7 @@ func refresh(anime *arn.Anime) { episodes := anime.Episodes() fmt.Println(faint(episodes)) - fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount) + fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount, len(episodes.Items)) println() } } From ad4a43f09dca2f548d53795c70f06c426852516c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 2 Oct 2017 19:17:23 +0200 Subject: [PATCH 374/527] Added airing date to planned list --- pages/animelist/animelist.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 96de01db..0ec5615b 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -59,7 +59,7 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User //- RawIcon("download") td.anime-list-item-airing-date - if item.Status == arn.AnimeListStatusWatching && item.Anime().UpcomingEpisode() != nil + if (item.Status == arn.AnimeListStatusWatching || item.Status == arn.AnimeListStatusPlanned) && item.Anime().UpcomingEpisode() != nil span.utc-airing-date(data-start-date=item.Anime().UpcomingEpisode().Episode.AiringDate.Start, data-end-date=item.Anime().UpcomingEpisode().Episode.AiringDate.End, data-episode-number=item.Anime().UpcomingEpisode().Episode.Number) td.anime-list-item-episodes From 3ae0e51110c3289e56f6ef8054e328da880130bb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 3 Oct 2017 04:24:01 +0200 Subject: [PATCH 375/527] Added tests for higher coverage --- tests.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests.go b/tests.go index 3fe8f199..6cb5ffb9 100644 --- a/tests.go +++ b/tests.go @@ -205,6 +205,19 @@ var routeTests = map[string][]string{ "/images/elements/no-avatar.svg", }, + // Extra tests for higher coverage + "/_/+Akyoto": []string{ + "/_/+Akyoto", + }, + + "/_/search/dragon": []string{ + "/_/search/dragon", + }, + + "/dark-flame-master": []string{ + "/dark-flame-master", + }, + // Disable these tests because they require authorization "/auth/google": nil, "/auth/google/callback": nil, From 8d07099cea51c13a665a96cb4bc3510dd1234332 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 3 Oct 2017 04:59:39 +0200 Subject: [PATCH 376/527] HTML5 valid --- mixins/Sidebar.pixy | 2 +- utils/EmptyImage.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 utils/EmptyImage.go diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index b6b66e8e..390e300c 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -3,7 +3,7 @@ component Sidebar(user *arn.User) if user != nil Avatar(user) else - img.user-image.lazy(data-src="/images/brand/64.png", data-webp="true", alt="Anime Notifier") + img.user-image.lazy(src=utils.EmptyImage(), data-src="/images/brand/64.png", data-webp="true", alt="Anime Notifier") if user != nil SidebarButton("Home", "/animelist/watching", "home") diff --git a/utils/EmptyImage.go b/utils/EmptyImage.go new file mode 100644 index 00000000..f7c3edf7 --- /dev/null +++ b/utils/EmptyImage.go @@ -0,0 +1,6 @@ +package utils + +// EmptyImage returns the smallest possible 1x1 pixel image encoded in Base64. +func EmptyImage() string { + return "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" +} From 52e83a4a37a5c52f8db44a4c79cd90f52301760f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 3 Oct 2017 07:22:20 +0200 Subject: [PATCH 377/527] New job scheduler times --- jobs/jobs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/jobs.go b/jobs/jobs.go index 5e0dca87..416a5aba 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -31,13 +31,13 @@ var jobs = map[string]time.Duration{ "popular-anime": 20 * time.Minute, "avatars": 1 * time.Hour, "test": 1 * time.Hour, - "twist": 1 * time.Hour, + "twist": 2 * time.Hour, "search-index": 2 * time.Hour, - "sync-shoboi": 8 * time.Hour, "refresh-episodes": 10 * time.Hour, "refresh-track-titles": 10 * time.Hour, "refresh-osu": 12 * time.Hour, "sync-anime": 12 * time.Hour, + "sync-shoboi": 24 * time.Hour, } func main() { From b6d6ce92dafc8818fa233bc53306f2236240809a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 3 Oct 2017 08:08:52 +0200 Subject: [PATCH 378/527] Added patch to fix airing dates --- jobs/refresh-episodes/refresh-episodes.go | 1 + patches/fix-airing-dates/fix-airing-dates.go | 44 ++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 patches/fix-airing-dates/fix-airing-dates.go diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index d7d4f552..7f500693 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -24,6 +24,7 @@ func main() { continue } + // The rest gets sorted by airing status switch anime.Status { case "current": highPriority = append(highPriority, anime) diff --git a/patches/fix-airing-dates/fix-airing-dates.go b/patches/fix-airing-dates/fix-airing-dates.go new file mode 100644 index 00000000..9d90af62 --- /dev/null +++ b/patches/fix-airing-dates/fix-airing-dates.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "time" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + now := time.Now() + futureThreshold := 8 * 7 * 24 * time.Hour + + for anime := range arn.MustStreamAnime() { + modified := false + + // Try to find incorrect airing dates + for _, episode := range anime.Episodes().Items { + if episode.AiringDate.Start == "" { + continue + } + + startTime, err := time.Parse(time.RFC3339, episode.AiringDate.Start) + + if err == nil && startTime.Sub(now) < futureThreshold { + continue + } + + // Definitely wrong airing date on this episode + fmt.Printf("%s | %s | Ep %d | %s\n", anime.ID, color.YellowString(anime.Title.Canonical), episode.Number, episode.AiringDate.Start) + + // Delete the wrong airing date + episode.AiringDate.Start = "" + episode.AiringDate.End = "" + + modified = true + } + + if modified == true { + arn.PanicOnError(anime.Episodes().Save()) + } + } +} From aec23d0b6e4a7e092e3ecd7a9521669fd483ceda Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 3 Oct 2017 15:26:29 +0200 Subject: [PATCH 379/527] Started working on the shop --- main.go | 2 ++ mixins/Sidebar.pixy | 7 ++++--- pages/admin/admin.pixy | 2 +- pages/shop/pro-account.md | 9 +++++++++ pages/shop/shop.go | 28 ++++++++++++++++++++++++++++ pages/shop/shop.pixy | 29 +++++++++++++++++++++++++++++ pages/shop/shop.scarlet | 33 +++++++++++++++++++++++++++++++++ 7 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 pages/shop/pro-account.md create mode 100644 pages/shop/shop.go create mode 100644 pages/shop/shop.pixy create mode 100644 pages/shop/shop.scarlet diff --git a/main.go b/main.go index cacc69c1..b0a4555c 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,7 @@ import ( "github.com/animenotifier/notify.moe/pages/profile" "github.com/animenotifier/notify.moe/pages/search" "github.com/animenotifier/notify.moe/pages/settings" + "github.com/animenotifier/notify.moe/pages/shop" "github.com/animenotifier/notify.moe/pages/soundtrack" "github.com/animenotifier/notify.moe/pages/soundtracks" "github.com/animenotifier/notify.moe/pages/statistics" @@ -94,6 +95,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/users/anime/watching", users.AnimeWatching) app.Ajax("/statistics", statistics.Get) app.Ajax("/statistics/anime", statistics.Anime) + app.Ajax("/shop", shop.Get) app.Ajax("/login", login.Get) // User profiles diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 390e300c..a9124078 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -20,12 +20,10 @@ component Sidebar(user *arn.User) //- SidebarButton("Search", "/search", "search") if user != nil + SidebarButton("Shop", "/shop", "shopping-cart") SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") - if user.Role == "admin" || user.Role == "editor" - SidebarButton("Admin", "/admin", "wrench") - .spacer .sidebar-link(aria-label="Search") @@ -33,6 +31,9 @@ component Sidebar(user *arn.User) Icon("search") FuzzySearch + if user != nil && (user.Role == "admin" || user.Role == "editor") + SidebarButton("Admin", "/admin", "wrench") + SidebarButton("Help", "/thread/I3MMiOtzR", "question-circle") if user != nil diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index 247c6719..d7a94611 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -1,9 +1,9 @@ component AdminTabs .tabs Tab("Server", "server", "/admin") + Tab("WebDev", "html5", "/admin/webdev") Tab("Shoboi", "calendar", "/admin/shoboi") Tab("AniList", "list", "/admin/anilist") - Tab("WebDev", "html5", "/admin/webdev") component Admin(user *arn.User, cpuUsage, memUsage, diskUsage float64, platform, family, platformVersion, kernelVersion string) h1.page-title Admin Panel diff --git a/pages/shop/pro-account.md b/pages/shop/pro-account.md new file mode 100644 index 00000000..ba937048 --- /dev/null +++ b/pages/shop/pro-account.md @@ -0,0 +1,9 @@ +PRO account for 1 anime season (3 months). + +Includes: + +* Avatar highlight on the forums +* Customizable cover image for your profile +* Custom title for profile and forums +* Your suggestions will have a high priority +* Access to the VIP channel on Discord \ No newline at end of file diff --git a/pages/shop/shop.go b/pages/shop/shop.go new file mode 100644 index 00000000..a2d6ba3a --- /dev/null +++ b/pages/shop/shop.go @@ -0,0 +1,28 @@ +package shop + +import ( + "io/ioutil" + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +var proAccount = "" + +func init() { + data, _ := ioutil.ReadFile("pages/shop/pro-account.md") + proAccount = string(data) +} + +// Get shop page. +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + return ctx.HTML(components.Shop(user, proAccount)) +} diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy new file mode 100644 index 00000000..bac44191 --- /dev/null +++ b/pages/shop/shop.pixy @@ -0,0 +1,29 @@ +component Shop(user *arn.User, proAccountMarkdown string) + h1.page-title Shop + + .user-balance-bar + Icon("diamond") + span.user-balance 0 + + .tabs + .tab Goods + .tab Top-Up + + .widgets.shop-items + ShopItem("PRO Account", "3 months", "900", "star", proAccountMarkdown) + ShopItem("PRO Account", "6 months", "1700", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "6 months", 1), "1 anime season", "2 anime seasons", 1)) + ShopItem("PRO Account", "1 year", "3000", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "12 months", 1), "1 anime season", "4 anime seasons", 1)) + ShopItem("PRO Account", "2 years", "5900", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "24 months", 1), "1 anime season", "8 anime seasons", 1)) + ShopItem("Anime Support Ticket", "", "100", "ticket", "Support the makers of your favourite anime by using an anime support ticket. Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly to the studios involved in the creation of your favourite anime.") + +component ShopItem(name string, duration string, price string, icon string, description string) + .widget.shop-item + h3.widget-title.shop-item-name + Icon(icon) + span= name + span.shop-item-duration= " " + duration + .shop-item-description!= aero.Markdown(description) + .buttons.shop-buttons + button.shop-button-buy + span.shop-item-price= price + Icon("diamond") \ No newline at end of file diff --git a/pages/shop/shop.scarlet b/pages/shop/shop.scarlet new file mode 100644 index 00000000..9bb1bccf --- /dev/null +++ b/pages/shop/shop.scarlet @@ -0,0 +1,33 @@ +.shop-items + // ... + +.shop-item + // ... + +.shop-item-name + // ... + +.shop-item-price + // ... + +.shop-item-price-currency + margin-left 0.3rem + margin-right 0 + +.shop-item-duration + opacity 0.5 + text-align right + float right + +.shop-buttons + margin-top 1rem + +.shop-button-buy + .icon-diamond + margin-left 0.3rem + margin-right 0 + +.user-balance-bar + text-align center + font-size 2.5rem + margin-bottom 2rem \ No newline at end of file From 16ab79b3a8b338e4e41ada2eedb3f83c92cfcb33 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 4 Oct 2017 08:12:12 +0200 Subject: [PATCH 380/527] Shop improvements --- main.go | 8 ++++- pages/charge/charge.go | 21 ++++++++++++ pages/charge/charge.pixy | 3 ++ pages/inventory/inventory.go | 29 +++++++++++++++++ pages/inventory/inventory.pixy | 7 ++++ pages/inventory/inventory.scarlet | 0 pages/shop/pro-account.md | 9 ------ pages/shop/shop.go | 14 +++----- pages/shop/shop.pixy | 37 ++++++++++------------ pages/shop/shop.scarlet | 7 +--- patches/add-follows/add-follows.go | 13 +++----- patches/add-inventories/add-inventories.go | 36 +++++++++++++++++++++ tests.go | 3 ++ 13 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 pages/charge/charge.go create mode 100644 pages/charge/charge.pixy create mode 100644 pages/inventory/inventory.go create mode 100644 pages/inventory/inventory.pixy create mode 100644 pages/inventory/inventory.scarlet delete mode 100644 pages/shop/pro-account.md create mode 100644 patches/add-inventories/add-inventories.go diff --git a/main.go b/main.go index b0a4555c..4ca3ed9f 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/animenotifier/notify.moe/pages/artworks" "github.com/animenotifier/notify.moe/pages/best" "github.com/animenotifier/notify.moe/pages/character" + "github.com/animenotifier/notify.moe/pages/charge" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/embed" @@ -25,6 +26,7 @@ import ( "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/home" + "github.com/animenotifier/notify.moe/pages/inventory" "github.com/animenotifier/notify.moe/pages/listimport" "github.com/animenotifier/notify.moe/pages/listimport/listimportanilist" "github.com/animenotifier/notify.moe/pages/listimport/listimportkitsu" @@ -95,7 +97,6 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/users/anime/watching", users.AnimeWatching) app.Ajax("/statistics", statistics.Get) app.Ajax("/statistics/anime", statistics.Anime) - app.Ajax("/shop", shop.Get) app.Ajax("/login", login.Get) // User profiles @@ -125,6 +126,11 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) + // Shop + app.Ajax("/shop", shop.Get) + app.Ajax("/inventory", inventory.Get) + app.Ajax("/charge", charge.Get) + // Admin app.Ajax("/admin", admin.Get) app.Ajax("/admin/anilist", admin.AniList) diff --git a/pages/charge/charge.go b/pages/charge/charge.go new file mode 100644 index 00000000..5dc7706b --- /dev/null +++ b/pages/charge/charge.go @@ -0,0 +1,21 @@ +package charge + +import ( + "net/http" + + "github.com/animenotifier/notify.moe/components" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/utils" +) + +// Get charge page. +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + return ctx.HTML(components.Charge()) +} diff --git a/pages/charge/charge.pixy b/pages/charge/charge.pixy new file mode 100644 index 00000000..3245271c --- /dev/null +++ b/pages/charge/charge.pixy @@ -0,0 +1,3 @@ +component Charge + ShopTabs + p Coming soon. \ No newline at end of file diff --git a/pages/inventory/inventory.go b/pages/inventory/inventory.go new file mode 100644 index 00000000..bd91a8ea --- /dev/null +++ b/pages/inventory/inventory.go @@ -0,0 +1,29 @@ +package inventory + +import ( + "net/http" + + "github.com/animenotifier/arn" + + "github.com/animenotifier/notify.moe/components" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/utils" +) + +// Get inventory page. +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + inventory, err := arn.GetInventory(user.ID) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching inventory data", err) + } + + return ctx.HTML(components.Inventory(inventory)) +} diff --git a/pages/inventory/inventory.pixy b/pages/inventory/inventory.pixy new file mode 100644 index 00000000..fc92ddc4 --- /dev/null +++ b/pages/inventory/inventory.pixy @@ -0,0 +1,7 @@ +component Inventory(inventory *arn.Inventory) + ShopTabs + .inventory + each slot in inventory.Slots + .inventory-slot + span= slot.ItemID + p Coming soon. \ No newline at end of file diff --git a/pages/inventory/inventory.scarlet b/pages/inventory/inventory.scarlet new file mode 100644 index 00000000..e69de29b diff --git a/pages/shop/pro-account.md b/pages/shop/pro-account.md deleted file mode 100644 index ba937048..00000000 --- a/pages/shop/pro-account.md +++ /dev/null @@ -1,9 +0,0 @@ -PRO account for 1 anime season (3 months). - -Includes: - -* Avatar highlight on the forums -* Customizable cover image for your profile -* Custom title for profile and forums -* Your suggestions will have a high priority -* Access to the VIP channel on Discord \ No newline at end of file diff --git a/pages/shop/shop.go b/pages/shop/shop.go index a2d6ba3a..1ef04e3d 100644 --- a/pages/shop/shop.go +++ b/pages/shop/shop.go @@ -1,21 +1,15 @@ package shop import ( - "io/ioutil" "net/http" + "github.com/animenotifier/arn" + "github.com/aerogo/aero" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" ) -var proAccount = "" - -func init() { - data, _ := ioutil.ReadFile("pages/shop/pro-account.md") - proAccount = string(data) -} - // Get shop page. func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) @@ -24,5 +18,7 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } - return ctx.HTML(components.Shop(user, proAccount)) + items := arn.AllItems() + + return ctx.HTML(components.Shop(user, items)) } diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy index bac44191..554f75b4 100644 --- a/pages/shop/shop.pixy +++ b/pages/shop/shop.pixy @@ -1,29 +1,26 @@ -component Shop(user *arn.User, proAccountMarkdown string) +component Shop(user *arn.User, items []*arn.Item) h1.page-title Shop - - .user-balance-bar - Icon("diamond") - span.user-balance 0 - .tabs - .tab Goods - .tab Top-Up + ShopTabs .widgets.shop-items - ShopItem("PRO Account", "3 months", "900", "star", proAccountMarkdown) - ShopItem("PRO Account", "6 months", "1700", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "6 months", 1), "1 anime season", "2 anime seasons", 1)) - ShopItem("PRO Account", "1 year", "3000", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "12 months", 1), "1 anime season", "4 anime seasons", 1)) - ShopItem("PRO Account", "2 years", "5900", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "24 months", 1), "1 anime season", "8 anime seasons", 1)) - ShopItem("Anime Support Ticket", "", "100", "ticket", "Support the makers of your favourite anime by using an anime support ticket. Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly to the studios involved in the creation of your favourite anime.") + each item in items + ShopItem(item) -component ShopItem(name string, duration string, price string, icon string, description string) - .widget.shop-item +component ShopTabs + .tabs + Tab("Shop", "shopping-cart", "/shop") + Tab("Inventory", "briefcase", "/inventory") + Tab("0", "diamond", "/charge") + +component ShopItem(item *arn.Item) + .widget.shop-item.mountable h3.widget-title.shop-item-name - Icon(icon) - span= name - span.shop-item-duration= " " + duration - .shop-item-description!= aero.Markdown(description) + Icon(item.Icon) + span= item.Name + //- span.shop-item-duration= " " + duration + .shop-item-description!= aero.Markdown(item.Description) .buttons.shop-buttons button.shop-button-buy - span.shop-item-price= price + span.shop-item-price= item.Price Icon("diamond") \ No newline at end of file diff --git a/pages/shop/shop.scarlet b/pages/shop/shop.scarlet index 9bb1bccf..b2374edc 100644 --- a/pages/shop/shop.scarlet +++ b/pages/shop/shop.scarlet @@ -25,9 +25,4 @@ .shop-button-buy .icon-diamond margin-left 0.3rem - margin-right 0 - -.user-balance-bar - text-align center - font-size 2.5rem - margin-bottom 2rem \ No newline at end of file + margin-right 0 \ No newline at end of file diff --git a/patches/add-follows/add-follows.go b/patches/add-follows/add-follows.go index 5b0923c2..a30c9a40 100644 --- a/patches/add-follows/add-follows.go +++ b/patches/add-follows/add-follows.go @@ -12,18 +12,15 @@ func main() { // Get a stream of all users allUsers, err := arn.StreamUsers() - - if err != nil { - panic(err) - } + arn.PanicOnError(err) // Iterate over the stream for user := range allUsers { - // exists, err := arn.DB.Exists("UserFollows", user.ID) + exists, err := arn.DB.Exists("UserFollows", user.ID) - // if err != nil || exists { - // continue - // } + if err != nil || exists { + continue + } fmt.Println(user.Nick) diff --git a/patches/add-inventories/add-inventories.go b/patches/add-inventories/add-inventories.go new file mode 100644 index 00000000..e1463128 --- /dev/null +++ b/patches/add-inventories/add-inventories.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Adding inventories to users who don't have one") + + // Get a stream of all users + allUsers, err := arn.StreamUsers() + arn.PanicOnError(err) + + // Iterate over the stream + for user := range allUsers { + exists, err := arn.DB.Exists("Inventory", user.ID) + + if err != nil || exists { + continue + } + + fmt.Println(user.Nick) + + inventory := arn.NewInventory(user.ID) + err = arn.DB.Set("Inventory", inventory.UserID, inventory) + + if err != nil { + color.Red(err.Error()) + } + } + + color.Green("Finished.") +} diff --git a/tests.go b/tests.go index 6cb5ffb9..0529e2b5 100644 --- a/tests.go +++ b/tests.go @@ -246,5 +246,8 @@ var routeTests = map[string][]string{ "/admin/shoboi": nil, "/user": nil, "/settings": nil, + "/shop": nil, + "/charge": nil, + "/inventory": nil, "/extension/embed": nil, } From 6c2782f18deffd1e9fef4d732e66982b491b2775 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 4 Oct 2017 13:39:59 +0200 Subject: [PATCH 381/527] Inventory implementation --- pages/inventory/inventory.go | 6 ++ pages/inventory/inventory.pixy | 12 ++- pages/inventory/inventory.scarlet | 35 ++++++++ pages/shop/shop.go | 11 ++- patches/add-shop-items/add-shop-items.go | 104 +++++++++++++++++++++++ scripts/AnimeNotifier.ts | 81 ++++++++++++++---- scripts/Utils.ts | 23 +++++ 7 files changed, 250 insertions(+), 22 deletions(-) create mode 100644 patches/add-shop-items/add-shop-items.go diff --git a/pages/inventory/inventory.go b/pages/inventory/inventory.go index bd91a8ea..473698ed 100644 --- a/pages/inventory/inventory.go +++ b/pages/inventory/inventory.go @@ -25,5 +25,11 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Error fetching inventory data", err) } + // TEST + inventory.AddItem("anime-support-ticket", 35) + inventory.AddItem("pro-account-24", 20) + inventory.AddItem("anime-support-ticket", 15) + inventory.AddItem("pro-account-24", 10) + return ctx.HTML(components.Inventory(inventory)) } diff --git a/pages/inventory/inventory.pixy b/pages/inventory/inventory.pixy index fc92ddc4..11326778 100644 --- a/pages/inventory/inventory.pixy +++ b/pages/inventory/inventory.pixy @@ -1,7 +1,11 @@ component Inventory(inventory *arn.Inventory) ShopTabs .inventory - each slot in inventory.Slots - .inventory-slot - span= slot.ItemID - p Coming soon. \ No newline at end of file + for index, slot := range inventory.Slots + if slot.ItemID == "" + .inventory-slot.mountable(draggable="false", data-index=index) + else + .inventory-slot.mountable(title=slot.Item().Name, draggable="true", data-index=index) + Icon(slot.Item().Icon) + if slot.Quantity > 1 + .inventory-slot-quantity= slot.Quantity \ No newline at end of file diff --git a/pages/inventory/inventory.scarlet b/pages/inventory/inventory.scarlet index e69de29b..b4829920 100644 --- a/pages/inventory/inventory.scarlet +++ b/pages/inventory/inventory.scarlet @@ -0,0 +1,35 @@ +inventory-slot-size = 64px + +.inventory + display grid + grid-gap 0.25rem + grid-template-columns repeat(auto-fit, inventory-slot-size) + grid-auto-rows inventory-slot-size + justify-content center + width 100% + max-width 450px + margin 0 auto + +.inventory-slot + ui-element + position relative + display flex + align-items center + justify-content center + font-size 2rem + + .icon + margin 0 + pointer-events none + +.inventory-slot-quantity + position absolute + bottom 0.25rem + right 0.25rem + font-size 0.8rem + line-height 1em + opacity 0.5 + pointer-events none + +.drag-enter + border-style dashed \ No newline at end of file diff --git a/pages/shop/shop.go b/pages/shop/shop.go index 1ef04e3d..16ec90a1 100644 --- a/pages/shop/shop.go +++ b/pages/shop/shop.go @@ -2,6 +2,7 @@ package shop import ( "net/http" + "sort" "github.com/animenotifier/arn" @@ -18,7 +19,15 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } - items := arn.AllItems() + items, err := arn.AllItems() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching shop item data", err) + } + + sort.Slice(items, func(i, j int) bool { + return items[i].Order < items[j].Order + }) return ctx.HTML(components.Shop(user, items)) } diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go new file mode 100644 index 00000000..57592e09 --- /dev/null +++ b/patches/add-shop-items/add-shop-items.go @@ -0,0 +1,104 @@ +package main + +import "github.com/animenotifier/arn" + +var items = []*arn.Item{ + &arn.Item{ + ID: "pro-account-3", + Name: "PRO Account (1 season)", + Price: 900, + Description: `PRO account for 1 anime season (3 months). + +Includes: + +* Special highlight on the forums +* Customizable cover image for your profile +* Custom title for profile and forums +* Your suggestions will have a high priority +* Access to the VIP channel on Discord`, + Icon: "star", + Rarity: arn.ItemRaritySuperior, + Order: 1, + Consumable: true, + }, + &arn.Item{ + ID: "pro-account-6", + Name: "PRO Account (2 seasons)", + Price: 1600, + Description: `PRO account for 2 anime seasons (6 months). + +Includes: + +* Special highlight on the forums +* Customizable cover image for your profile +* Custom title for profile and forums +* Your suggestions will have a high priority +* Access to the VIP channel on Discord`, + Icon: "star", + Rarity: arn.ItemRarityRare, + Order: 2, + Consumable: true, + }, + &arn.Item{ + ID: "pro-account-12", + Name: "PRO Account (4 seasons)", + Price: 3000, + Description: `PRO account for 4 anime seasons (12 months). + +Includes: + +* Special highlight on the forums +* Customizable cover image for your profile +* Custom title for profile and forums +* Your suggestions will have a high priority +* Access to the VIP channel on Discord`, + Icon: "star", + Rarity: arn.ItemRarityUnique, + Order: 3, + Consumable: true, + }, + &arn.Item{ + ID: "pro-account-24", + Name: "PRO Account (8 seasons)", + Price: 5900, + Description: `PRO account for 8 anime seasons (24 months). + +Includes: + +* Special highlight on the forums +* Customizable cover image for your profile +* Custom title for profile and forums +* Your suggestions will have a high priority +* Access to the VIP channel on Discord`, + Icon: "star", + Rarity: arn.ItemRarityLegendary, + Order: 4, + Consumable: true, + }, + &arn.Item{ + ID: "anime-support-ticket", + Name: "Anime Support Ticket", + Price: 100, + Description: `Support the makers of your favourite anime by using an anime support ticket. +Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly +to the studios involved in the creation of your favourite anime.`, + Icon: "ticket", + Rarity: arn.ItemRarityRare, + Order: 5, + Consumable: false, + }, +} + +func main() { + for _, item := range items { + item.Save() + } +} + +//- ShopItem("PRO Account", "6 months", "1600", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "6 months", 1), "1 anime season", "2 anime seasons", 1)) +//- ShopItem("PRO Account", "1 year", "3000", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "12 months", 1), "1 anime season", "4 anime seasons", 1)) +//- ShopItem("PRO Account", "2 years", "5900", "star", strings.Replace(strings.Replace(proAccountMarkdown, "3 months", "24 months", 1), "1 anime season", "8 anime seasons", 1)) +//- ShopItem("Anime Support Ticket", "", "100", "ticket", "Support the makers of your favourite anime by using an anime support ticket. Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly to the studios involved in the creation of your favourite anime.") +//- ShopItem("Artwork Support Ticket", "", "100", "ticket", "Support the makers of your favourite artwork by using an artwork support ticket. Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly to the creator.") +//- ShopItem("Soundtrack Support Ticket", "", "100", "ticket", "Support the makers of your favourite soundtrack by using a soundtrack support ticket. Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly to the creator.") +//- ShopItem("AMV Support Ticket", "", "100", "ticket", "Support the makers of your favourite AMV by using an AMV support ticket. Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly to the creator.") diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index b9a78533..4a94eb7d 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -1,6 +1,6 @@ import * as actions from "./Actions" import { displayAiringDate, displayDate } from "./DateView" -import { findAll, delay, canUseWebP } from "./Utils" +import { findAll, delay, canUseWebP, swapElements } from "./Utils" import { Application } from "./Application" import { Diff } from "./Diff" import { MutationQueue } from "./MutationQueue" @@ -139,6 +139,7 @@ export class AnimeNotifier { Promise.resolve().then(() => this.setSelectBoxValue()), Promise.resolve().then(() => this.assignActions()), Promise.resolve().then(() => this.updatePushUI()), + Promise.resolve().then(() => this.dragAndDrop()), Promise.resolve().then(() => this.countUp()) ]) @@ -154,22 +155,6 @@ export class AnimeNotifier { } } - async updatePushUI() { - if(!this.pushManager.pushSupported || !this.app.currentPath.includes("/settings")) { - return - } - - let subscription = await this.pushManager.subscription() - - if(subscription) { - this.app.find("enable-notifications").style.display = "none" - this.app.find("disable-notifications").style.display = "flex" - } else { - this.app.find("enable-notifications").style.display = "flex" - this.app.find("disable-notifications").style.display = "none" - } - } - onIdle() { // Service worker this.registerServiceWorker() @@ -261,6 +246,68 @@ export class AnimeNotifier { } } + dragAndDrop() { + for(let element of findAll("inventory-slot")) { + if(element.draggable) { + element.addEventListener("dragstart", e => { + let element = e.target as HTMLElement + e.dataTransfer.setData("text", element.dataset.index) + }, false) + } + + element.addEventListener("dragenter", e => { + let element = e.target as HTMLElement + element.classList.add("drag-enter") + }, false) + + element.addEventListener("dragleave", e => { + let element = e.target as HTMLElement + element.classList.remove("drag-enter") + }, false) + + element.addEventListener("dragover", e => { + e.preventDefault() + }, false) + + element.addEventListener("drop", e => { + let inventory = e.toElement.parentElement + let fromIndex = e.dataTransfer.getData("text") + let fromElement = inventory.childNodes[fromIndex] as HTMLElement + let toElement = e.toElement as HTMLElement + let toIndex = toElement.dataset.index + + e.stopPropagation() + e.preventDefault() + + if(fromElement === toElement || fromIndex === toIndex) { + return + } + + toElement.classList.remove("drag-enter") + swapElements(fromElement, toElement) + + fromElement.dataset.index = toIndex + toElement.dataset.index = fromIndex + }, false) + } + } + + async updatePushUI() { + if(!this.pushManager.pushSupported || !this.app.currentPath.includes("/settings")) { + return + } + + let subscription = await this.pushManager.subscription() + + if(subscription) { + this.app.find("enable-notifications").style.display = "none" + this.app.find("disable-notifications").style.display = "flex" + } else { + this.app.find("enable-notifications").style.display = "flex" + this.app.find("disable-notifications").style.display = "none" + } + } + countUp() { for(let element of findAll("count-up")) { let final = parseInt(element.innerText) diff --git a/scripts/Utils.ts b/scripts/Utils.ts index 17ad8d01..cc164528 100644 --- a/scripts/Utils.ts +++ b/scripts/Utils.ts @@ -24,4 +24,27 @@ export function canUseWebP(): boolean { // In very old browsers (IE 8) canvas is not supported return false } +} + +export function swapElements(a: Node, b: Node) { + let parent = b.parentNode + let bNext = b.nextSibling + + // Special case for when a is the next sibling of b + if(bNext === a) { + // Just put a before b + parent.insertBefore(a, b) + } else { + // Insert b right before a + a.parentNode.insertBefore(b, a) + + // Now insert a where b was + if(bNext) { + // If there was an element after b, then insert a right before that + parent.insertBefore(a, bNext) + } else { + // Otherwise just append it as the last child + parent.appendChild(a) + } + } } \ No newline at end of file From 77ae39d330a6889cc1a1ca3a887dcbfbf1b13f67 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 07:22:14 +0200 Subject: [PATCH 382/527] New API --- main.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/main.go b/main.go index 4ca3ed9f..32b2c2ca 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "github.com/aerogo/aero" - "github.com/aerogo/api" "github.com/aerogo/session-store-aerospike" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/auth" @@ -175,8 +174,7 @@ func configure(app *aero.Application) *aero.Application { app.Use(middleware.UserInfo()) // API - api := api.New("/api/", arn.DB) - api.Install(app) + arn.API.Install(app) // Domain if arn.IsDevelopment() { From 09d28e146c8169f87a43df22638a1fba5ffbbdc6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 09:39:37 +0200 Subject: [PATCH 383/527] Improved inventory --- pages/inventory/inventory.go | 11 ++----- pages/inventory/inventory.pixy | 6 ++-- patches/add-inventories/add-inventories.go | 13 +++++--- scripts/Actions.ts | 19 ++++++++++++ scripts/AnimeNotifier.ts | 35 ++++++++++++++++++++++ 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/pages/inventory/inventory.go b/pages/inventory/inventory.go index 473698ed..c5e11673 100644 --- a/pages/inventory/inventory.go +++ b/pages/inventory/inventory.go @@ -14,22 +14,17 @@ import ( // Get inventory page. func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) + viewUser := user if user == nil { return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } - inventory, err := arn.GetInventory(user.ID) + inventory, err := arn.GetInventory(viewUser.ID) if err != nil { return ctx.Error(http.StatusInternalServerError, "Error fetching inventory data", err) } - // TEST - inventory.AddItem("anime-support-ticket", 35) - inventory.AddItem("pro-account-24", 20) - inventory.AddItem("anime-support-ticket", 15) - inventory.AddItem("pro-account-24", 10) - - return ctx.HTML(components.Inventory(inventory)) + return ctx.HTML(components.Inventory(inventory, viewUser)) } diff --git a/pages/inventory/inventory.pixy b/pages/inventory/inventory.pixy index 11326778..ccdb7a63 100644 --- a/pages/inventory/inventory.pixy +++ b/pages/inventory/inventory.pixy @@ -1,11 +1,11 @@ -component Inventory(inventory *arn.Inventory) +component Inventory(inventory *arn.Inventory, viewUser *arn.User) ShopTabs - .inventory + .inventory(data-api="/api/inventory/" + viewUser.ID) for index, slot := range inventory.Slots if slot.ItemID == "" .inventory-slot.mountable(draggable="false", data-index=index) else - .inventory-slot.mountable(title=slot.Item().Name, draggable="true", data-index=index) + .inventory-slot.mountable(title=slot.Item().Name, draggable="true", data-index=index, data-item-id=slot.ItemID, data-consumable=slot.Item().Consumable) Icon(slot.Item().Icon) if slot.Quantity > 1 .inventory-slot-quantity= slot.Quantity \ No newline at end of file diff --git a/patches/add-inventories/add-inventories.go b/patches/add-inventories/add-inventories.go index e1463128..c9c591a3 100644 --- a/patches/add-inventories/add-inventories.go +++ b/patches/add-inventories/add-inventories.go @@ -16,15 +16,20 @@ func main() { // Iterate over the stream for user := range allUsers { - exists, err := arn.DB.Exists("Inventory", user.ID) + // exists, err := arn.DB.Exists("Inventory", user.ID) - if err != nil || exists { - continue - } + // if err != nil || exists { + // continue + // } fmt.Println(user.Nick) inventory := arn.NewInventory(user.ID) + + // TEST + inventory.AddItem("anime-support-ticket", 50) + inventory.AddItem("pro-account-24", 30) + err = arn.DB.Set("Inventory", inventory.UserID, inventory) if err != nil { diff --git a/scripts/Actions.ts b/scripts/Actions.ts index aa1e925a..e7deb1fc 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -302,6 +302,25 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen .then(() => arn.loading(false)) } +// Use item +// export function useItem(arn: AnimeNotifier, button: HTMLElement) { +// let slotIndex = "" +// let parent = button + +// while(parent = parent.parentElement) { +// if(parent.dataset.index !== undefined) { +// slotIndex = parent.dataset.index +// break +// } +// } + +// let apiEndpoint = arn.findAPIEndpoint(button) + +// arn.post(apiEndpoint + "/use/" + slotIndex, "") +// .then(() => arn.reloadContent()) +// .catch(err => arn.statusMessage.showError(err)) +// } + // Chrome extension installation export function installExtension(arn: AnimeNotifier, button: HTMLElement) { let browser: any = window["chrome"] diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 4a94eb7d..c61b33dc 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -248,11 +248,32 @@ export class AnimeNotifier { dragAndDrop() { for(let element of findAll("inventory-slot")) { + // Skip elements that have their event listeners attached already + if(element["listeners-attached"]) { + continue + } + + // If the slot has an item and is therefore draggable if(element.draggable) { element.addEventListener("dragstart", e => { let element = e.target as HTMLElement e.dataTransfer.setData("text", element.dataset.index) }, false) + + element.addEventListener("dblclick", e => { + let itemName = element.title + + if(element.dataset.consumable !== "true") { + return this.statusMessage.showError(itemName + " is not a consumable item.") + } + + let apiEndpoint = this.findAPIEndpoint(element) + + this.post(apiEndpoint + "/use/" + element.dataset.index, "") + .then(() => this.reloadContent()) + .then(() => this.statusMessage.showInfo(`You used ${itemName}.`)) + .catch(err => this.statusMessage.showError(err)) + }, false) } element.addEventListener("dragenter", e => { @@ -283,12 +304,22 @@ export class AnimeNotifier { return } + // Swap in database + let apiEndpoint = this.findAPIEndpoint(inventory) + + this.post(apiEndpoint + "/swap/" + fromIndex + "/" + toIndex, "") + .catch(err => this.statusMessage.showError(err)) + + // Swap in UI toElement.classList.remove("drag-enter") swapElements(fromElement, toElement) fromElement.dataset.index = toIndex toElement.dataset.index = fromIndex }, false) + + // Prevent re-attaching the same listeners + element["listeners-attached"] = true } } @@ -660,6 +691,10 @@ export class AnimeNotifier { } findAPIEndpoint(element: HTMLElement) { + if(element.dataset.api !== undefined) { + return element.dataset.api + } + let apiObject: HTMLElement let parent = element From 43317fd3da650c4329102aa3ee4416b5703c9755 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 09:54:11 +0200 Subject: [PATCH 384/527] Fixed inventory swap bug --- scripts/AnimeNotifier.ts | 63 +++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index c61b33dc..ed7eb906 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -253,36 +253,38 @@ export class AnimeNotifier { continue } - // If the slot has an item and is therefore draggable - if(element.draggable) { - element.addEventListener("dragstart", e => { - let element = e.target as HTMLElement - e.dataTransfer.setData("text", element.dataset.index) - }, false) + element.addEventListener("dragstart", e => { + if(!element.draggable) { + return + } - element.addEventListener("dblclick", e => { - let itemName = element.title + e.dataTransfer.setData("text", element.dataset.index) + }, false) - if(element.dataset.consumable !== "true") { - return this.statusMessage.showError(itemName + " is not a consumable item.") - } - - let apiEndpoint = this.findAPIEndpoint(element) - - this.post(apiEndpoint + "/use/" + element.dataset.index, "") - .then(() => this.reloadContent()) - .then(() => this.statusMessage.showInfo(`You used ${itemName}.`)) - .catch(err => this.statusMessage.showError(err)) - }, false) - } + element.addEventListener("dblclick", e => { + if(!element.draggable) { + return + } + + let itemName = element.title + + if(element.dataset.consumable !== "true") { + return this.statusMessage.showError(itemName + " is not a consumable item.") + } + + let apiEndpoint = this.findAPIEndpoint(element) + + this.post(apiEndpoint + "/use/" + element.dataset.index, "") + .then(() => this.reloadContent()) + .then(() => this.statusMessage.showInfo(`You used ${itemName}.`)) + .catch(err => this.statusMessage.showError(err)) + }, false) element.addEventListener("dragenter", e => { - let element = e.target as HTMLElement element.classList.add("drag-enter") }, false) element.addEventListener("dragleave", e => { - let element = e.target as HTMLElement element.classList.remove("drag-enter") }, false) @@ -291,15 +293,23 @@ export class AnimeNotifier { }, false) element.addEventListener("drop", e => { - let inventory = e.toElement.parentElement - let fromIndex = e.dataTransfer.getData("text") - let fromElement = inventory.childNodes[fromIndex] as HTMLElement let toElement = e.toElement as HTMLElement - let toIndex = toElement.dataset.index + toElement.classList.remove("drag-enter") e.stopPropagation() e.preventDefault() + let inventory = e.toElement.parentElement + let fromIndex = e.dataTransfer.getData("text") + + if(!fromIndex) { + return + } + + let fromElement = inventory.childNodes[fromIndex] as HTMLElement + + let toIndex = toElement.dataset.index + if(fromElement === toElement || fromIndex === toIndex) { return } @@ -311,7 +321,6 @@ export class AnimeNotifier { .catch(err => this.statusMessage.showError(err)) // Swap in UI - toElement.classList.remove("drag-enter") swapElements(fromElement, toElement) fromElement.dataset.index = toIndex From 41155c8bff4abba6bea47504a511621d29fcb46d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 10:26:07 +0200 Subject: [PATCH 385/527] Improved shop --- pages/charge/charge.go | 2 +- pages/charge/charge.pixy | 4 ++-- pages/inventory/inventory.go | 2 +- pages/inventory/inventory.pixy | 5 +++-- pages/paypal/success.go | 4 ++-- pages/paypal/success.pixy | 12 +++++++++--- pages/paypal/success.scarlet | 1 + pages/shop/shop.pixy | 6 +++--- 8 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pages/charge/charge.go b/pages/charge/charge.go index 5dc7706b..441b4563 100644 --- a/pages/charge/charge.go +++ b/pages/charge/charge.go @@ -17,5 +17,5 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } - return ctx.HTML(components.Charge()) + return ctx.HTML(components.Charge(user)) } diff --git a/pages/charge/charge.pixy b/pages/charge/charge.pixy index 3245271c..f41a0ff1 100644 --- a/pages/charge/charge.pixy +++ b/pages/charge/charge.pixy @@ -1,3 +1,3 @@ -component Charge - ShopTabs +component Charge(user *arn.User) + ShopTabs(user) p Coming soon. \ No newline at end of file diff --git a/pages/inventory/inventory.go b/pages/inventory/inventory.go index c5e11673..e7aa1904 100644 --- a/pages/inventory/inventory.go +++ b/pages/inventory/inventory.go @@ -26,5 +26,5 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Error fetching inventory data", err) } - return ctx.HTML(components.Inventory(inventory, viewUser)) + return ctx.HTML(components.Inventory(inventory, viewUser, user)) } diff --git a/pages/inventory/inventory.pixy b/pages/inventory/inventory.pixy index ccdb7a63..22b9b911 100644 --- a/pages/inventory/inventory.pixy +++ b/pages/inventory/inventory.pixy @@ -1,5 +1,6 @@ -component Inventory(inventory *arn.Inventory, viewUser *arn.User) - ShopTabs +component Inventory(inventory *arn.Inventory, viewUser *arn.User, user *arn.User) + ShopTabs(user) + .inventory(data-api="/api/inventory/" + viewUser.ID) for index, slot := range inventory.Slots if slot.ItemID == "" diff --git a/pages/paypal/success.go b/pages/paypal/success.go index 954531db..52c57b81 100644 --- a/pages/paypal/success.go +++ b/pages/paypal/success.go @@ -73,7 +73,7 @@ func Success(ctx *aero.Context) string { } // Increase user's balance - user.Balance += payment.AnimeDollar() + user.Balance += payment.Gems() // Save in DB err = user.Save() @@ -86,7 +86,7 @@ func Success(ctx *aero.Context) string { go func() { admin, _ := arn.GetUser(adminID) admin.SendNotification(&arn.Notification{ - Title: user.Nick + " bought " + strconv.Itoa(payment.AnimeDollar()) + " AD", + Title: user.Nick + " bought " + strconv.Itoa(payment.Gems()) + " AD", Message: user.Nick + " paid " + payment.Amount + " " + payment.Currency + " making his new balance " + strconv.Itoa(user.Balance), Icon: user.LargeAvatar(), Link: "https://" + ctx.App.Config.Domain + "/api/paypalpayment/" + payment.ID, diff --git a/pages/paypal/success.pixy b/pages/paypal/success.pixy index ee13b1d4..3b20c271 100644 --- a/pages/paypal/success.pixy +++ b/pages/paypal/success.pixy @@ -3,8 +3,14 @@ component PayPalSuccess(payment *arn.PayPalPayment) .new-payment.mountable span + - .new-payment-amount.count-up= payment.AnimeDollar() - span.new-payment-currency AD + .new-payment-amount.count-up= payment.Gems() + span.new-payment-currency + Icon("diamond") p.mountable - img.new-payment-thank-you-image(src="/images/elements/thank-you.jpg", alt="Thank you!") \ No newline at end of file + img.new-payment-thank-you-image(src="/images/elements/thank-you.jpg", alt="Thank you!") + + .buttons + a.button.ajax(href="/shop") + Icon("shopping-cart") + span Return to the shop \ No newline at end of file diff --git a/pages/paypal/success.scarlet b/pages/paypal/success.scarlet index ebb83b08..d528c358 100644 --- a/pages/paypal/success.scarlet +++ b/pages/paypal/success.scarlet @@ -2,6 +2,7 @@ horizontal margin 2rem auto font-size 4rem + line-height 1em color green .new-payment-currency diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy index 554f75b4..e741f368 100644 --- a/pages/shop/shop.pixy +++ b/pages/shop/shop.pixy @@ -1,17 +1,17 @@ component Shop(user *arn.User, items []*arn.Item) h1.page-title Shop - ShopTabs + ShopTabs(user) .widgets.shop-items each item in items ShopItem(item) -component ShopTabs +component ShopTabs(user *arn.User) .tabs Tab("Shop", "shopping-cart", "/shop") Tab("Inventory", "briefcase", "/inventory") - Tab("0", "diamond", "/charge") + Tab(strconv.Itoa(user.Balance), "diamond", "/charge") component ShopItem(item *arn.Item) .widget.shop-item.mountable From 71303ef35117c8cc8708d91b835aa5dae5908b06 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 12:36:26 +0200 Subject: [PATCH 386/527] Implemented charge up --- main.go | 2 +- pages/anime/anime.scarlet | 6 +++++ pages/charge/charge.pixy | 28 ++++++++++++++++++++++- pages/paypal/paypal.go | 17 +++++++++++++- scripts/Actions.ts | 47 ++++++++++++++++++++++++++------------- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/main.go b/main.go index 32b2c2ca..75f6e299 100644 --- a/main.go +++ b/main.go @@ -159,7 +159,7 @@ func configure(app *aero.Application) *aero.Application { // PayPal app.Ajax("/paypal/success", paypal.Success) app.Ajax("/paypal/cancel", paypal.Cancel) - app.Get("/api/paypal/payment/create", paypal.CreatePayment) + app.Post("/api/paypal/payment/create", paypal.CreatePayment) // Assets configureAssets(app) diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index cde6a43c..117b3536 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -101,6 +101,12 @@ opacity 0.7 margin-top 0.5rem + &.mountable + opacity 0 !important + + &.mounted + opacity 0.7 !important + .relations horizontal-wrap diff --git a/pages/charge/charge.pixy b/pages/charge/charge.pixy index f41a0ff1..35111777 100644 --- a/pages/charge/charge.pixy +++ b/pages/charge/charge.pixy @@ -1,3 +1,29 @@ component Charge(user *arn.User) ShopTabs(user) - p Coming soon. \ No newline at end of file + + h1.mountable Charge up + + p.text-center.mountable You can charge up your balance via PayPal. 1 USD equals 100 gems. + + .buttons + button.action.mountable(data-trigger="click", data-action="chargeUp", data-amount=1000) + Icon("diamond") + span 1000 + + button.action.mountable(data-trigger="click", data-action="chargeUp", data-amount=2000) + Icon("diamond") + span 2000 + + button.action.mountable(data-trigger="click", data-action="chargeUp", data-amount=3000) + Icon("diamond") + span 3000 + + button.action.mountable(data-trigger="click", data-action="chargeUp", data-amount=6000) + Icon("diamond") + span 6000 + + button.action.mountable(data-trigger="click", data-action="chargeUp", data-amount=12000) + Icon("diamond") + span 12000 + + .footer.text-center.mountable You need to enable popup windows in your browser. \ No newline at end of file diff --git a/pages/paypal/paypal.go b/pages/paypal/paypal.go index d2a1b7e4..1c363abb 100644 --- a/pages/paypal/paypal.go +++ b/pages/paypal/paypal.go @@ -17,12 +17,24 @@ func CreatePayment(ctx *aero.Context) string { return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } + amount := string(ctx.RequestBody()) + + // Verify amount + switch amount { + case "1000", "2000", "3000", "6000", "12000": + // OK + default: + return ctx.Error(http.StatusBadRequest, "Incorrect amount", nil) + } + + // Initiate PayPal client c, err := arn.PayPal() if err != nil { return ctx.Error(http.StatusInternalServerError, "Could not initiate PayPal client", err) } + // Get access token _, err = c.GetAccessToken() if err != nil { @@ -55,6 +67,9 @@ func CreatePayment(ctx *aero.Context) string { // return ctx.Error(http.StatusInternalServerError, "Could not create PayPal web profile", err) // } + total := amount[:len(amount)-2] + "." + amount[len(amount)-2:] + + // Create payment p := paypalsdk.Payment{ Intent: "sale", Payer: &paypalsdk.Payer{ @@ -63,7 +78,7 @@ func CreatePayment(ctx *aero.Context) string { Transactions: []paypalsdk.Transaction{paypalsdk.Transaction{ Amount: &paypalsdk.Amount{ Currency: "USD", - Total: "10.00", + Total: total, }, Description: "Top Up Balance", }}, diff --git a/scripts/Actions.ts b/scripts/Actions.ts index e7deb1fc..68f70029 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -302,24 +302,39 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElemen .then(() => arn.loading(false)) } -// Use item -// export function useItem(arn: AnimeNotifier, button: HTMLElement) { -// let slotIndex = "" -// let parent = button +// Charge up +export function chargeUp(arn: AnimeNotifier, button: HTMLElement) { + let amount = button.dataset.amount -// while(parent = parent.parentElement) { -// if(parent.dataset.index !== undefined) { -// slotIndex = parent.dataset.index -// break -// } -// } + arn.loading(true) + arn.statusMessage.showInfo("Creating PayPal transaction... This might take a few seconds.") -// let apiEndpoint = arn.findAPIEndpoint(button) - -// arn.post(apiEndpoint + "/use/" + slotIndex, "") -// .then(() => arn.reloadContent()) -// .catch(err => arn.statusMessage.showError(err)) -// } + fetch("/api/paypal/payment/create", { + method: "POST", + body: amount, + credentials: "same-origin" + }) + .then(response => response.json()) + .then(payment => { + if(!payment || !payment.links) { + throw "Error creating PayPal payment" + } + + console.log(payment) + let link = payment.links.find(link => link.rel === "approval_url") + + if(!link) { + throw "Error finding PayPal payment link" + } + + let url = link.href + console.log(url) + + window.open(url, "_blank") + }) + .catch(err => arn.statusMessage.showError(err)) + .then(() => arn.loading(false)) +} // Chrome extension installation export function installExtension(arn: AnimeNotifier, button: HTMLElement) { From a52668ada5656812df744a26cff393ffc451c60a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 13:48:16 +0200 Subject: [PATCH 387/527] Finished shop system --- main.go | 1 + mixins/Sidebar.pixy | 4 +- pages/inventory/inventory.pixy | 2 + pages/shop/shop.go | 56 ++++++++++++++++++++++ pages/shop/shop.pixy | 6 +-- patches/add-balance/add-balance.go | 22 +++++++++ patches/add-inventories/add-inventories.go | 14 +++--- scripts/Actions.ts | 29 +++++++++++ scripts/AnimeNotifier.ts | 4 ++ 9 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 patches/add-balance/add-balance.go diff --git a/main.go b/main.go index 75f6e299..25446f24 100644 --- a/main.go +++ b/main.go @@ -129,6 +129,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/shop", shop.Get) app.Ajax("/inventory", inventory.Get) app.Ajax("/charge", charge.Get) + app.Post("/api/shop/buy/:item/:quantity", shop.BuyItem) // Admin app.Ajax("/admin", admin.Get) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index a9124078..e87709ea 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -20,7 +20,9 @@ component Sidebar(user *arn.User) //- SidebarButton("Search", "/search", "search") if user != nil - SidebarButton("Shop", "/shop", "shopping-cart") + if user.Role == "admin" || user.Role == "editor" + SidebarButton("Shop", "/shop", "shopping-cart") + SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") diff --git a/pages/inventory/inventory.pixy b/pages/inventory/inventory.pixy index 22b9b911..7eada3df 100644 --- a/pages/inventory/inventory.pixy +++ b/pages/inventory/inventory.pixy @@ -1,6 +1,8 @@ component Inventory(inventory *arn.Inventory, viewUser *arn.User, user *arn.User) ShopTabs(user) + h1.page-title Inventory + .inventory(data-api="/api/inventory/" + viewUser.ID) for index, slot := range inventory.Slots if slot.ItemID == "" diff --git a/pages/shop/shop.go b/pages/shop/shop.go index 16ec90a1..928fe12c 100644 --- a/pages/shop/shop.go +++ b/pages/shop/shop.go @@ -3,6 +3,7 @@ package shop import ( "net/http" "sort" + "sync" "github.com/animenotifier/arn" @@ -11,6 +12,8 @@ import ( "github.com/animenotifier/notify.moe/utils" ) +var itemBuyMutex sync.Mutex + // Get shop page. func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) @@ -31,3 +34,56 @@ func Get(ctx *aero.Context) string { return ctx.HTML(components.Shop(user, items)) } + +// BuyItem ... +func BuyItem(ctx *aero.Context) string { + // Lock via mutex to prevent race conditions + itemBuyMutex.Lock() + defer itemBuyMutex.Unlock() + + // Logged in user + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + // Item ID and quantity + itemID := ctx.Get("item") + quantity, err := ctx.GetInt("quantity") + + if err != nil || quantity == 0 { + return ctx.Error(http.StatusBadRequest, "Invalid item quantity", err) + } + + item, err := arn.GetItem(itemID) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching item data", err) + } + + // Calculate total price and subtract balance + totalPrice := int(item.Price) * quantity + + if user.Balance < totalPrice { + return ctx.Error(http.StatusBadRequest, "Not enough gems", nil) + } + + user.Balance -= totalPrice + err = user.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving user data", err) + } + + // Add item to user inventory + inventory := user.Inventory() + inventory.AddItem(itemID, uint(quantity)) + err = inventory.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving inventory", err) + } + + return "ok" +} diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy index e741f368..8556163e 100644 --- a/pages/shop/shop.pixy +++ b/pages/shop/shop.pixy @@ -1,8 +1,8 @@ component Shop(user *arn.User, items []*arn.Item) - h1.page-title Shop - ShopTabs(user) + h1.page-title Shop + .widgets.shop-items each item in items ShopItem(item) @@ -21,6 +21,6 @@ component ShopItem(item *arn.Item) //- span.shop-item-duration= " " + duration .shop-item-description!= aero.Markdown(item.Description) .buttons.shop-buttons - button.shop-button-buy + button.shop-button-buy.action(data-item-id=item.ID, data-item-name=item.Name, data-price=item.Price, data-trigger="click", data-action="buyItem") span.shop-item-price= item.Price Icon("diamond") \ No newline at end of file diff --git a/patches/add-balance/add-balance.go b/patches/add-balance/add-balance.go new file mode 100644 index 00000000..8289aab3 --- /dev/null +++ b/patches/add-balance/add-balance.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Adding balance to all users") + + // Get a stream of all users + allUsers, err := arn.StreamUsers() + arn.PanicOnError(err) + + // Iterate over the stream + for user := range allUsers { + user.Balance += 100000 + arn.PanicOnError(user.Save()) + } + + color.Green("Finished.") +} diff --git a/patches/add-inventories/add-inventories.go b/patches/add-inventories/add-inventories.go index c9c591a3..28966b6d 100644 --- a/patches/add-inventories/add-inventories.go +++ b/patches/add-inventories/add-inventories.go @@ -16,19 +16,19 @@ func main() { // Iterate over the stream for user := range allUsers { - // exists, err := arn.DB.Exists("Inventory", user.ID) + exists, err := arn.DB.Exists("Inventory", user.ID) - // if err != nil || exists { - // continue - // } + if err != nil || exists { + continue + } fmt.Println(user.Nick) inventory := arn.NewInventory(user.ID) - // TEST - inventory.AddItem("anime-support-ticket", 50) - inventory.AddItem("pro-account-24", 30) + // // TEST + // inventory.AddItem("anime-support-ticket", 50) + // inventory.AddItem("pro-account-24", 30) err = arn.DB.Set("Inventory", inventory.UserID, inventory) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 68f70029..24b08867 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -336,6 +336,35 @@ export function chargeUp(arn: AnimeNotifier, button: HTMLElement) { .then(() => arn.loading(false)) } +// Buy item +export function buyItem(arn: AnimeNotifier, button: HTMLElement) { + let itemId = button.dataset.itemId + let itemName = button.dataset.itemName + let price = button.dataset.price + + if(!confirm(`Would you like to buy ${itemName} for ${price} gems?`)) { + return + } + + arn.loading(true) + + fetch(`/api/shop/buy/${itemId}/1`, { + method: "POST", + credentials: "same-origin" + }) + .then(response => response.text()) + .then(body => { + if(body !== "ok") { + throw body + } + + return arn.reloadContent() + }) + .then(() => arn.statusMessage.showInfo(`You bought ${itemName} for ${price} gems. Check out your inventory to confirm the purchase.`, 4000)) + .catch(err => arn.statusMessage.showError(err)) + .then(() => arn.loading(false)) +} + // Chrome extension installation export function installExtension(arn: AnimeNotifier, button: HTMLElement) { let browser: any = window["chrome"] diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index ed7eb906..606e8cb5 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -471,6 +471,10 @@ export class AnimeNotifier { element.removeEventListener(oldAction.trigger, oldAction.handler) } + if(!(actionName in actions)) { + this.statusMessage.showError(`Action '${actionName}' has not been defined`) + } + let actionHandler = e => { actions[actionName](this, element, e) From ce9a2538d6eabc3164007a00ddd9d5ced459ead9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 13:55:46 +0200 Subject: [PATCH 388/527] Added balance deletion --- patches/delete-balance/delete-balance.go | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 patches/delete-balance/delete-balance.go diff --git a/patches/delete-balance/delete-balance.go b/patches/delete-balance/delete-balance.go new file mode 100644 index 00000000..bc329500 --- /dev/null +++ b/patches/delete-balance/delete-balance.go @@ -0,0 +1,38 @@ +package main + +import ( + "flag" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +// Shell parameters +var confirmed bool + +// Shell flags +func init() { + flag.BoolVar(&confirmed, "confirm", false, "Confirm that you really want to execute this.") + flag.Parse() +} + +func main() { + if !confirmed { + color.Green("Please run this command with -confirm option if you really want to reset the balance of all users.") + return + } + + color.Yellow("Resetting balance of all users to 0") + + // Get a stream of all users + allUsers, err := arn.StreamUsers() + arn.PanicOnError(err) + + // Iterate over the stream + for user := range allUsers { + user.Balance = 0 + arn.PanicOnError(user.Save()) + } + + color.Green("Finished.") +} From 03c79e29326051a7848812d16a27895bc07f9710 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 5 Oct 2017 21:27:49 +0200 Subject: [PATCH 389/527] Started working on pro features --- mixins/Postable.pixy | 2 +- pages/admin/webdev.pixy | 12 ++++++------ pages/threads/threads.scarlet | 4 ++++ scripts/AnimeNotifier.ts | 6 ++++++ styles/include/config.scarlet | 2 +- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/mixins/Postable.pixy b/mixins/Postable.pixy index c0c8e366..9fd02261 100644 --- a/mixins/Postable.pixy +++ b/mixins/Postable.pixy @@ -1,5 +1,5 @@ component Postable(post arn.Postable, user *arn.User, highlightAuthorID string) - .post.mountable(id=strings.ToLower(post.Type()) + "-" + toString(post.ID()), data-highlight=post.Author().ID == highlightAuthorID, data-api="/api/" + strings.ToLower(post.Type()) + "/" + post.ID()) + .post.mountable(id=strings.ToLower(post.Type()) + "-" + toString(post.ID()), data-highlight=post.Author().ID == highlightAuthorID, data-pro=post.Author().IsPro(), data-api="/api/" + strings.ToLower(post.Type()) + "/" + post.ID()) .post-author Avatar(post.Author()) diff --git a/pages/admin/webdev.pixy b/pages/admin/webdev.pixy index ffe3db70..0284a844 100644 --- a/pages/admin/webdev.pixy +++ b/pages/admin/webdev.pixy @@ -1,15 +1,15 @@ component WebDev - h1.page-title WebDev - AdminTabs - .light-button-group - a.light-button(href="https://developers.google.com/speed/pagespeed/insights/?url=https://notify.moe/&tab=desktop", target="_blank", rel="noopener") + h1.page-title WebDev + + .buttons + a.button.mountable(href="https://developers.google.com/speed/pagespeed/insights/?url=https://notify.moe/&tab=desktop", target="_blank", rel="noopener") Icon("external-link") span Google PageSpeed - a.light-button(href="https://observatory.mozilla.org/analyze.html?host=notify.moe", target="_blank", rel="noopener") + a.button.mountable(href="https://observatory.mozilla.org/analyze.html?host=notify.moe", target="_blank", rel="noopener") Icon("external-link") span Mozilla Observatory - a.light-button(href="https://html5.validator.nu/?doc=https://notify.moe", target="_blank", rel="noopener") + a.button.mountable(href="https://html5.validator.nu/?doc=https://notify.moe", target="_blank", rel="noopener") Icon("external-link") span HTML5 Validator \ No newline at end of file diff --git a/pages/threads/threads.scarlet b/pages/threads/threads.scarlet index da44d2d9..14c349d2 100644 --- a/pages/threads/threads.scarlet +++ b/pages/threads/threads.scarlet @@ -17,6 +17,10 @@ [data-highlight="true"] .post-content border 2px solid post-highlight-color + + [data-pro="true"] + .post-content + border 2px solid pro-color > 600px .post diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 606e8cb5..17b8a6c8 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -461,6 +461,10 @@ export class AnimeNotifier { let actionTrigger = element.dataset.trigger let actionName = element.dataset.action + if(!actionTrigger || !actionName) { + continue + } + let oldAction = element["action assigned"] if(oldAction) { @@ -473,6 +477,8 @@ export class AnimeNotifier { if(!(actionName in actions)) { this.statusMessage.showError(`Action '${actionName}' has not been defined`) + console.error(element) + continue } let actionHandler = e => { diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index a7c21a28..9e87a449 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -4,8 +4,8 @@ main-color = rgb(248, 165, 130) link-color = rgb(215, 38, 15) link-hover-color = rgb(242, 60, 30) link-active-color = link-hover-color - bg-color = rgb(246, 246, 246) +pro-color = hsla(0, 100%, 77%, 0.87) // UI ui-border-color = rgba(0, 0, 0, 0.1) From c0d57e51c951dfe222d6c3e2ec2ba929c87b7fbc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 05:53:49 +0200 Subject: [PATCH 390/527] Changed main currency to JPY --- auth/auth.go | 2 + auth/facebook.go | 4 +- auth/google.go | 2 +- mixins/FuzzySearch.pixy | 2 + mixins/Navigation.pixy | 121 +++++++++++++++++---------------------- mixins/Sidebar.pixy | 14 ++++- pages/charge/charge.pixy | 4 +- pages/paypal/paypal.go | 6 +- scripts/Actions.ts | 6 +- scripts/AnimeNotifier.ts | 1 - 10 files changed, 81 insertions(+), 81 deletions(-) create mode 100644 mixins/FuzzySearch.pixy diff --git a/auth/auth.go b/auth/auth.go index 9cad95f6..aed84e2b 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -3,6 +3,8 @@ package auth import "github.com/aerogo/aero" import "github.com/animenotifier/notify.moe/utils" +const newUserStartRoute = "/settings" + // Install ... func Install(app *aero.Application) { // Google diff --git a/auth/facebook.go b/auth/facebook.go index 4bb761cb..fe80d2f9 100644 --- a/auth/facebook.go +++ b/auth/facebook.go @@ -170,7 +170,7 @@ func InstallFacebookAuth(app *aero.Application) { // Log authLog.Info("Registered new user via Facebook", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) - // Redirect to frontpage - return ctx.Redirect("/") + // Redirect to settings + return ctx.Redirect(newUserStartRoute) }) } diff --git a/auth/google.go b/auth/google.go index 8355305a..ff49d05e 100644 --- a/auth/google.go +++ b/auth/google.go @@ -181,6 +181,6 @@ func InstallGoogleAuth(app *aero.Application) { authLog.Info("Registered new user via Google", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) // Redirect to frontpage - return ctx.Redirect("/") + return ctx.Redirect(newUserStartRoute) }) } diff --git a/mixins/FuzzySearch.pixy b/mixins/FuzzySearch.pixy new file mode 100644 index 00000000..cbf95fba --- /dev/null +++ b/mixins/FuzzySearch.pixy @@ -0,0 +1,2 @@ +component FuzzySearch + input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: F") \ No newline at end of file diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index 9140747d..88b95beb 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -1,87 +1,72 @@ -component Navigation(user *arn.User) - if user == nil - LoggedOutMenu - else - LoggedInMenu(user) +//- component Navigation(user *arn.User) +//- if user == nil +//- LoggedOutMenu +//- else +//- LoggedInMenu(user) -component LoggedOutMenu - nav#navigation.logged-out - #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") - .navigation-button - Icon("bars") - span.navigation-text Menu +//- component LoggedOutMenu +//- nav#navigation.logged-out +//- #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") +//- .navigation-button +//- Icon("bars") +//- span.navigation-text Menu - //- NavigationButton("Explore", "/explore", "th") - //- NavigationButton("Forum", "/forum", "comment") +//- //- NavigationButton("Explore", "/explore", "th") +//- //- NavigationButton("Forum", "/forum", "comment") - FuzzySearch +//- FuzzySearch - //- .extra-navigation - //- NavigationButton("Users", "/users", "globe") +//- //- .extra-navigation +//- //- NavigationButton("Users", "/users", "globe") - //- NavigationButton("Soundtracks", "/soundtracks", "headphones") +//- //- NavigationButton("Soundtracks", "/soundtracks", "headphones") - NavigationButton("Login", "/login", "sign-in") +//- NavigationButton("Login", "/login", "sign-in") -component LoggedInMenu(user *arn.User) - nav#navigation.logged-in - .extension-navigation - NavigationButton("Watching list", "/extension/embed", "home") +//- component LoggedInMenu(user *arn.User) +//- nav#navigation.logged-in +//- .extension-navigation +//- NavigationButton("Watching list", "/extension/embed", "home") - #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") - .navigation-button - Icon("bars") - span.navigation-text Menu +//- #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") +//- .navigation-button +//- Icon("bars") +//- span.navigation-text Menu - //- .extra-navigation - //- NavigationButton("Profile", "/+", "user") +//- //- .extra-navigation +//- //- NavigationButton("Profile", "/+", "user") - //- .extra-navigation - //- NavigationButton("Forum", "/forum", "comment") +//- //- .extra-navigation +//- //- NavigationButton("Forum", "/forum", "comment") - //- .extra-navigation - //- NavigationButton("Soundtracks", "/soundtracks", "headphones") +//- //- .extra-navigation +//- //- NavigationButton("Soundtracks", "/soundtracks", "headphones") - FuzzySearch +//- FuzzySearch - //- .extra-navigation - //- NavigationButton("Users", "/users", "globe") +//- //- .extra-navigation +//- //- NavigationButton("Users", "/users", "globe") - //- .extra-navigation - //- NavigationButton("Explore", "/explore", "th") +//- //- .extra-navigation +//- //- NavigationButton("Explore", "/explore", "th") - //- .extra-navigation - //- NavigationButton("Statistics", "/statistics", "pie-chart") +//- //- .extra-navigation +//- //- NavigationButton("Statistics", "/statistics", "pie-chart") - //- .hide-landscape - //- NavigationButton("Settings", "/settings", "cog") +//- //- .hide-landscape +//- //- NavigationButton("Settings", "/settings", "cog") - //- .extra-navigation.hide-landscape - //- NavigationButtonNoAJAX("Logout", "/logout", "sign-out") +//- //- .extra-navigation.hide-landscape +//- //- NavigationButtonNoAJAX("Logout", "/logout", "sign-out") -component FuzzySearch - input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: F") +//- component NavigationButton(name string, target string, icon string) +//- a.navigation-link.ajax(href=target, aria-label=name, title=name) +//- .navigation-button +//- Icon(icon) +//- span.navigation-text= name -component NavigationButton(name string, target string, icon string) - a.navigation-link.ajax(href=target, aria-label=name, title=name) - .navigation-button - Icon(icon) - span.navigation-text= name - -component SidebarButton(name string, target string, icon string) - a.sidebar-link.ajax(href=target, aria-label=name, data-bubble="true") - .sidebar-button - Icon(icon) - span.sidebar-text= name - -component NavigationButtonNoAJAX(name string, target string, icon string) - a.navigation-link(href=target, aria-label=name) - .navigation-button - Icon(icon) - span.navigation-text= name - -component SidebarButtonNoAJAX(name string, target string, icon string) - a.sidebar-link(href=target, aria-label=name, data-bubble="true") - .sidebar-button - Icon(icon) - span.sidebar-text= name \ No newline at end of file +//- component NavigationButtonNoAJAX(name string, target string, icon string) +//- a.navigation-link(href=target, aria-label=name) +//- .navigation-button +//- Icon(icon) +//- span.navigation-text= name \ No newline at end of file diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index e87709ea..8a6c2e31 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -41,4 +41,16 @@ component Sidebar(user *arn.User) if user != nil SidebarButtonNoAJAX("Logout", "/logout", "sign-out") else - SidebarButton("Login", "/login", "sign-in") \ No newline at end of file + SidebarButton("Login", "/login", "sign-in") + +component SidebarButton(name string, target string, icon string) + a.sidebar-link.ajax(href=target, aria-label=name, data-bubble="true") + .sidebar-button + Icon(icon) + span.sidebar-text= name + +component SidebarButtonNoAJAX(name string, target string, icon string) + a.sidebar-link(href=target, aria-label=name, data-bubble="true") + .sidebar-button + Icon(icon) + span.sidebar-text= name \ No newline at end of file diff --git a/pages/charge/charge.pixy b/pages/charge/charge.pixy index 35111777..239e84a5 100644 --- a/pages/charge/charge.pixy +++ b/pages/charge/charge.pixy @@ -3,7 +3,7 @@ component Charge(user *arn.User) h1.mountable Charge up - p.text-center.mountable You can charge up your balance via PayPal. 1 USD equals 100 gems. + p.text-center.mountable You can add balance via PayPal. 1 Japanese Yen equals 1 Gem. .buttons button.action.mountable(data-trigger="click", data-action="chargeUp", data-amount=1000) @@ -26,4 +26,4 @@ component Charge(user *arn.User) Icon("diamond") span 12000 - .footer.text-center.mountable You need to enable popup windows in your browser. \ No newline at end of file + .footer.text-center.mountable Different currencies will automatically be converted. \ No newline at end of file diff --git a/pages/paypal/paypal.go b/pages/paypal/paypal.go index 1c363abb..e3afe009 100644 --- a/pages/paypal/paypal.go +++ b/pages/paypal/paypal.go @@ -67,7 +67,7 @@ func CreatePayment(ctx *aero.Context) string { // return ctx.Error(http.StatusInternalServerError, "Could not create PayPal web profile", err) // } - total := amount[:len(amount)-2] + "." + amount[len(amount)-2:] + // total := amount[:len(amount)-2] + "." + amount[len(amount)-2:] // Create payment p := paypalsdk.Payment{ @@ -77,8 +77,8 @@ func CreatePayment(ctx *aero.Context) string { }, Transactions: []paypalsdk.Transaction{paypalsdk.Transaction{ Amount: &paypalsdk.Amount{ - Currency: "USD", - Total: total, + Currency: "JPY", + Total: amount, }, Description: "Top Up Balance", }}, diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 24b08867..bcba55c4 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -327,10 +327,10 @@ export function chargeUp(arn: AnimeNotifier, button: HTMLElement) { throw "Error finding PayPal payment link" } - let url = link.href - console.log(url) + arn.statusMessage.showInfo("Redirecting to PayPal...", 5000) - window.open(url, "_blank") + let url = link.href + window.location.href = url }) .catch(err => arn.statusMessage.showError(err)) .then(() => arn.loading(false)) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 17b8a6c8..e8641956 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -477,7 +477,6 @@ export class AnimeNotifier { if(!(actionName in actions)) { this.statusMessage.showError(`Action '${actionName}' has not been defined`) - console.error(element) continue } From e27e5a9256d5a6f626e8910e8b7552c03d99cb03 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 06:49:11 +0200 Subject: [PATCH 391/527] Minor improvements --- mixins/Sidebar.pixy | 4 ++-- pages/admin/webdev.pixy | 23 +++++++++++++++++- patches/add-shop-items/add-shop-items.go | 30 ++++++++++-------------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 8a6c2e31..3d5132e2 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -12,9 +12,9 @@ component Sidebar(user *arn.User) SidebarButton("Forum", "/forum", "comment") SidebarButton("Explore", "/explore", "th") - SidebarButton("Artworks", "/artworks", "paint-brush") + //- SidebarButton("Artworks", "/artworks", "paint-brush") SidebarButton("Soundtracks", "/soundtracks", "headphones") - SidebarButton("AMVs", "/amvs", "video-camera") + //- SidebarButton("AMVs", "/amvs", "video-camera") //- SidebarButton("Games", "/games", "gamepad") SidebarButton("Users", "/users", "globe") //- SidebarButton("Search", "/search", "search") diff --git a/pages/admin/webdev.pixy b/pages/admin/webdev.pixy index 0284a844..f053c85e 100644 --- a/pages/admin/webdev.pixy +++ b/pages/admin/webdev.pixy @@ -3,6 +3,8 @@ component WebDev h1.page-title WebDev + h2 Tests + .buttons a.button.mountable(href="https://developers.google.com/speed/pagespeed/insights/?url=https://notify.moe/&tab=desktop", target="_blank", rel="noopener") Icon("external-link") @@ -12,4 +14,23 @@ component WebDev span Mozilla Observatory a.button.mountable(href="https://html5.validator.nu/?doc=https://notify.moe", target="_blank", rel="noopener") Icon("external-link") - span HTML5 Validator \ No newline at end of file + span HTML5 Validator + + h2 Browser Support + + .buttons + a.button.mountable(href="http://caniuse.com/#feat=webp", target="_blank", rel="noopener") + Icon("external-link") + span WebP + a.button.mountable(href="http://caniuse.com/#feat=push-api", target="_blank", rel="noopener") + Icon("external-link") + span Push API + a.button.mountable(href="http://caniuse.com/#feat=serviceworkers", target="_blank", rel="noopener") + Icon("external-link") + span Service Worker + a.button.mountable(href="http://caniuse.com/#feat=intersectionobserver", target="_blank", rel="noopener") + Icon("external-link") + span Intersection Observer + a.button.mountable(href="http://caniuse.com/#feat=requestidlecallback", target="_blank", rel="noopener") + Icon("external-link") + span Request Idle Callback \ No newline at end of file diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index 57592e09..c1e62df5 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -12,10 +12,9 @@ var items = []*arn.Item{ Includes: * Special highlight on the forums -* Customizable cover image for your profile -* Custom title for profile and forums -* Your suggestions will have a high priority -* Access to the VIP channel on Discord`, +* High priority for your personal suggestions +* Access to the VIP channel on Discord +* Early access to new features`, Icon: "star", Rarity: arn.ItemRaritySuperior, Order: 1, @@ -30,10 +29,9 @@ Includes: Includes: * Special highlight on the forums -* Customizable cover image for your profile -* Custom title for profile and forums -* Your suggestions will have a high priority -* Access to the VIP channel on Discord`, +* High priority for your personal suggestions +* Access to the VIP channel on Discord +* Early access to new features`, Icon: "star", Rarity: arn.ItemRarityRare, Order: 2, @@ -48,10 +46,9 @@ Includes: Includes: * Special highlight on the forums -* Customizable cover image for your profile -* Custom title for profile and forums -* Your suggestions will have a high priority -* Access to the VIP channel on Discord`, +* High priority for your personal suggestions +* Access to the VIP channel on Discord +* Early access to new features`, Icon: "star", Rarity: arn.ItemRarityUnique, Order: 3, @@ -66,10 +63,9 @@ Includes: Includes: * Special highlight on the forums -* Customizable cover image for your profile -* Custom title for profile and forums -* Your suggestions will have a high priority -* Access to the VIP channel on Discord`, +* High priority for your personal suggestions +* Access to the VIP channel on Discord +* Early access to new features`, Icon: "star", Rarity: arn.ItemRarityLegendary, Order: 4, @@ -80,7 +76,7 @@ Includes: Name: "Anime Support Ticket", Price: 100, Description: `Support the makers of your favourite anime by using an anime support ticket. -Anime Notifier uses 8% of the money to handle the transaction fees while the remaining 92% go directly +Anime Notifier uses 10% of the money to handle the transaction fees while the remaining 90% go directly to the studios involved in the creation of your favourite anime.`, Icon: "ticket", Rarity: arn.ItemRarityRare, From 52927cd9e0d59f920af9cc1d246264e27b068e10 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 07:28:21 +0200 Subject: [PATCH 392/527] Minor improvements --- pages/admin/webdev.pixy | 11 ++++++++++- pages/frontpage/frontpage.pixy | 24 +++++++++++++++++++++--- pages/frontpage/frontpage.scarlet | 18 +++++++++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/pages/admin/webdev.pixy b/pages/admin/webdev.pixy index f053c85e..a9c31884 100644 --- a/pages/admin/webdev.pixy +++ b/pages/admin/webdev.pixy @@ -15,6 +15,12 @@ component WebDev a.button.mountable(href="https://html5.validator.nu/?doc=https://notify.moe", target="_blank", rel="noopener") Icon("external-link") span HTML5 Validator + a.button.mountable(href="https://testmysite.withgoogle.com/", target="_blank", rel="noopener") + Icon("external-link") + span Mobile Speed + a.button.mountable(href="https://www.webpagetest.org/", target="_blank", rel="noopener") + Icon("external-link") + span Web Page Test h2 Browser Support @@ -33,4 +39,7 @@ component WebDev span Intersection Observer a.button.mountable(href="http://caniuse.com/#feat=requestidlecallback", target="_blank", rel="noopener") Icon("external-link") - span Request Idle Callback \ No newline at end of file + span Request Idle Callback + a.button.mountable(href="http://caniuse.com/#feat=css-variables", target="_blank", rel="noopener") + Icon("external-link") + span CSS Variables \ No newline at end of file diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index abe2a1e9..2c54cce7 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -1,4 +1,6 @@ component FrontPage + .frontpage-background + .frontpage.mountable h1 notify.moe @@ -9,9 +11,25 @@ component FrontPage 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.footer-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") + Icon("microphone") + span Discord + + a.footer-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") + Icon("facebook") + span Facebook + + a.footer-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") + Icon("twitter") + span Twitter + + a.footer-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") + Icon("google-plus") + span Google+ + + a.footer-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") + Icon("github") + span GitHub video.bg-video(autoplay="autoplay", loop="loop") source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") diff --git a/pages/frontpage/frontpage.scarlet b/pages/frontpage/frontpage.scarlet index 1d8a3285..c0521f83 100644 --- a/pages/frontpage/frontpage.scarlet +++ b/pages/frontpage/frontpage.scarlet @@ -1,3 +1,14 @@ +frontpage-bg-color = rgb(32, 32, 32) + +.frontpage-background + position absolute + top 0 + left 0 + width 100% + height 100% + z-index -200 + background frontpage-bg-color + .frontpage vertical align-items center @@ -30,6 +41,7 @@ margin-top content-padding .bg-video + display none position absolute top 50% left 50% @@ -39,7 +51,11 @@ width auto height auto z-index -100 - background rgb(32, 32, 32) + background frontpage-bg-color + +> 1100px + .bg-video + display block .login-button horizontal From 819eb00ab714951506b9b6a5fb7f4f9899d2d37b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 07:34:13 +0200 Subject: [PATCH 393/527] Removed old template --- mixins/Navigation.pixy | 72 ------------------------------------------ 1 file changed, 72 deletions(-) delete mode 100644 mixins/Navigation.pixy diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy deleted file mode 100644 index 88b95beb..00000000 --- a/mixins/Navigation.pixy +++ /dev/null @@ -1,72 +0,0 @@ -//- component Navigation(user *arn.User) -//- if user == nil -//- LoggedOutMenu -//- else -//- LoggedInMenu(user) - -//- component LoggedOutMenu -//- nav#navigation.logged-out -//- #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") -//- .navigation-button -//- Icon("bars") -//- span.navigation-text Menu - -//- //- NavigationButton("Explore", "/explore", "th") -//- //- NavigationButton("Forum", "/forum", "comment") - -//- FuzzySearch - -//- //- .extra-navigation -//- //- NavigationButton("Users", "/users", "globe") - -//- //- NavigationButton("Soundtracks", "/soundtracks", "headphones") - -//- NavigationButton("Login", "/login", "sign-in") - -//- component LoggedInMenu(user *arn.User) -//- nav#navigation.logged-in -//- .extension-navigation -//- NavigationButton("Watching list", "/extension/embed", "home") - -//- #sidebar-toggle.navigation-link.action(data-action="toggleSidebar", data-trigger="click", aria-label="Menu", title="Menu") -//- .navigation-button -//- Icon("bars") -//- span.navigation-text Menu - -//- //- .extra-navigation -//- //- NavigationButton("Profile", "/+", "user") - -//- //- .extra-navigation -//- //- NavigationButton("Forum", "/forum", "comment") - -//- //- .extra-navigation -//- //- NavigationButton("Soundtracks", "/soundtracks", "headphones") - -//- FuzzySearch - -//- //- .extra-navigation -//- //- NavigationButton("Users", "/users", "globe") - -//- //- .extra-navigation -//- //- NavigationButton("Explore", "/explore", "th") - -//- //- .extra-navigation -//- //- NavigationButton("Statistics", "/statistics", "pie-chart") - -//- //- .hide-landscape -//- //- NavigationButton("Settings", "/settings", "cog") - -//- //- .extra-navigation.hide-landscape -//- //- NavigationButtonNoAJAX("Logout", "/logout", "sign-out") - -//- component NavigationButton(name string, target string, icon string) -//- a.navigation-link.ajax(href=target, aria-label=name, title=name) -//- .navigation-button -//- Icon(icon) -//- span.navigation-text= name - -//- component NavigationButtonNoAJAX(name string, target string, icon string) -//- a.navigation-link(href=target, aria-label=name) -//- .navigation-button -//- Icon(icon) -//- span.navigation-text= name \ No newline at end of file From fe83952c351b4f84f8d2d185410c2a1ab0d6c81b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 08:44:29 +0200 Subject: [PATCH 394/527] Added PRO account information --- pages/inventory/inventory.pixy | 5 ++++- pages/settings/settings.pixy | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pages/inventory/inventory.pixy b/pages/inventory/inventory.pixy index 7eada3df..24dc3406 100644 --- a/pages/inventory/inventory.pixy +++ b/pages/inventory/inventory.pixy @@ -11,4 +11,7 @@ component Inventory(inventory *arn.Inventory, viewUser *arn.User, user *arn.User .inventory-slot.mountable(title=slot.Item().Name, draggable="true", data-index=index, data-item-id=slot.ItemID, data-consumable=slot.Item().Consumable) Icon(slot.Item().Icon) if slot.Quantity > 1 - .inventory-slot-quantity= slot.Quantity \ No newline at end of file + .inventory-slot-quantity= slot.Quantity + + .footer.text-center.mountable + p You can consume items by double-clicking them. \ No newline at end of file diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index e3c78740..75bd1a37 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -137,6 +137,30 @@ component Settings(user *arn.User) .profile-image-container.avatar-preview img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") + .widget.mountable(data-api="/api/settings/" + user.ID) + h3.widget-title + Icon("star") + span PRO + + + if user.IsPro() + p Thank you for your support! + + .widget-input + label + span Your PRO account expires in + span.utc-date(data-date=user.ProExpires) + span . + a.button.ajax(href="/shop") + Icon("star") + span Extend PRO account duration + else + .widget-input + label Would you like to support the site development? + a.button.ajax(href="/shop") + Icon("star") + span Go PRO + //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title //- Icon("cogs") From 7e5acf5a9785ca4e2eec879cdde07c69650f430d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 09:35:59 +0200 Subject: [PATCH 395/527] Minor updates --- pages/settings/settings.pixy | 3 --- patches/add-shop-items/add-shop-items.go | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 75bd1a37..32bb50f4 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -141,11 +141,8 @@ component Settings(user *arn.User) h3.widget-title Icon("star") span PRO - if user.IsPro() - p Thank you for your support! - .widget-input label span Your PRO account expires in diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index c1e62df5..1e08a94e 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -9,6 +9,8 @@ var items = []*arn.Item{ Price: 900, Description: `PRO account for 1 anime season (3 months). +1 month equals 300 gems. + Includes: * Special highlight on the forums @@ -26,6 +28,8 @@ Includes: Price: 1600, Description: `PRO account for 2 anime seasons (6 months). +11% less monthly costs compared to standard. + Includes: * Special highlight on the forums @@ -43,6 +47,8 @@ Includes: Price: 3000, Description: `PRO account for 4 anime seasons (12 months). +16% less monthly costs compared to standard. + Includes: * Special highlight on the forums @@ -60,6 +66,8 @@ Includes: Price: 5900, Description: `PRO account for 8 anime seasons (24 months). +18% less monthly costs compared to standard. + Includes: * Special highlight on the forums From bb39234f2dd88c2a48990b56b1d80db391688d3c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 12:44:55 +0200 Subject: [PATCH 396/527] Improved shop --- main.go | 1 + pages/inventory/inventory.pixy | 3 +- pages/inventory/inventory.scarlet | 35 ++++++++- pages/shop/buyitem.go | 72 +++++++++++++++++++ pages/shop/history.go | 32 +++++++++ pages/shop/history.pixy | 22 ++++++ pages/shop/history.scarlet | 2 + pages/shop/shop.go | 56 --------------- pages/shop/shop.pixy | 6 +- pages/shop/shop.scarlet | 30 ++++++-- patches/add-shop-items/add-shop-items.go | 4 +- patches/delete-pro/delete-pro.go | 38 ++++++++++ .../reset-inventories.go} | 28 ++++---- tests.go | 1 + 14 files changed, 253 insertions(+), 77 deletions(-) create mode 100644 pages/shop/buyitem.go create mode 100644 pages/shop/history.go create mode 100644 pages/shop/history.pixy create mode 100644 pages/shop/history.scarlet create mode 100644 patches/delete-pro/delete-pro.go rename patches/{add-inventories/add-inventories.go => reset-inventories/reset-inventories.go} (59%) diff --git a/main.go b/main.go index 25446f24..901769b0 100644 --- a/main.go +++ b/main.go @@ -129,6 +129,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/shop", shop.Get) app.Ajax("/inventory", inventory.Get) app.Ajax("/charge", charge.Get) + app.Ajax("/shop/history", shop.PurchaseHistory) app.Post("/api/shop/buy/:item/:quantity", shop.BuyItem) // Admin diff --git a/pages/inventory/inventory.pixy b/pages/inventory/inventory.pixy index 24dc3406..d50cec70 100644 --- a/pages/inventory/inventory.pixy +++ b/pages/inventory/inventory.pixy @@ -9,7 +9,8 @@ component Inventory(inventory *arn.Inventory, viewUser *arn.User, user *arn.User .inventory-slot.mountable(draggable="false", data-index=index) else .inventory-slot.mountable(title=slot.Item().Name, draggable="true", data-index=index, data-item-id=slot.ItemID, data-consumable=slot.Item().Consumable) - Icon(slot.Item().Icon) + .item-icon + Icon(slot.Item().Icon) if slot.Quantity > 1 .inventory-slot-quantity= slot.Quantity diff --git a/pages/inventory/inventory.scarlet b/pages/inventory/inventory.scarlet index b4829920..fa8e858a 100644 --- a/pages/inventory/inventory.scarlet +++ b/pages/inventory/inventory.scarlet @@ -16,12 +16,45 @@ inventory-slot-size = 64px display flex align-items center justify-content center - font-size 2rem + font-size 2.5rem + + [draggable="true"] + :hover + cursor pointer + + .item-icon + animation hover-item 1s infinite ease-in-out .icon margin 0 pointer-events none + // [data-item-id="pro-account-3"] + // .item-icon + // opacity 0.7 + + // [data-item-id="pro-account-6"] + // .item-icon + // opacity 0.8 + + // [data-item-id="pro-account-12"] + // .item-icon + // opacity 0.9 + + // [data-item-id="pro-account-24"] + // .item-icon + // opacity 1.0 + +animation hover-item + 0% + transform rotateZ(0) + 20% + transform rotateZ(5deg) + 80% + transform rotateZ(-5deg) + 100% + transform rotateZ(0) + .inventory-slot-quantity position absolute bottom 0.25rem diff --git a/pages/shop/buyitem.go b/pages/shop/buyitem.go new file mode 100644 index 00000000..669c4ebf --- /dev/null +++ b/pages/shop/buyitem.go @@ -0,0 +1,72 @@ +package shop + +import ( + "net/http" + "sync" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/utils" +) + +var itemBuyMutex sync.Mutex + +// BuyItem ... +func BuyItem(ctx *aero.Context) string { + // Lock via mutex to prevent race conditions + itemBuyMutex.Lock() + defer itemBuyMutex.Unlock() + + // Logged in user + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + // Item ID and quantity + itemID := ctx.Get("item") + quantity, err := ctx.GetInt("quantity") + + if err != nil || quantity == 0 { + return ctx.Error(http.StatusBadRequest, "Invalid item quantity", err) + } + + item, err := arn.GetItem(itemID) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching item data", err) + } + + // Calculate total price and subtract balance + totalPrice := int(item.Price) * quantity + + if user.Balance < totalPrice { + return ctx.Error(http.StatusBadRequest, "Not enough gems", nil) + } + + user.Balance -= totalPrice + err = user.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving user data", err) + } + + // Add item to user inventory + inventory := user.Inventory() + inventory.AddItem(itemID, uint(quantity)) + err = inventory.Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving inventory", err) + } + + // Save purchase + err = arn.NewPurchase(user.ID, itemID, quantity, int(item.Price), "gem").Save() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error saving purchase", err) + } + + return "ok" +} diff --git a/pages/shop/history.go b/pages/shop/history.go new file mode 100644 index 00000000..d390bb69 --- /dev/null +++ b/pages/shop/history.go @@ -0,0 +1,32 @@ +package shop + +import ( + "net/http" + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// PurchaseHistory ... +func PurchaseHistory(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + purchases, err := arn.AllPurchases() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching shop item data", err) + } + + sort.Slice(purchases, func(i, j int) bool { + return purchases[i].Date > purchases[j].Date + }) + + return ctx.HTML(components.PurchaseHistory(purchases, user)) +} diff --git a/pages/shop/history.pixy b/pages/shop/history.pixy new file mode 100644 index 00000000..9bd2b2f3 --- /dev/null +++ b/pages/shop/history.pixy @@ -0,0 +1,22 @@ +component PurchaseHistory(purchases []*arn.Purchase, user *arn.User) + ShopTabs(user) + + h1.page-title Purchase History + + table + thead + tr.mountable + th Icon + th Item + th.history-quantity Quantity + th.history-price Price + th.history-date Date + tbody + each purchase in purchases + tr.shop-item.mountable(data-item-id=purchase.ItemID) + td.item-icon + Icon(purchase.Item().Icon) + td= purchase.Item().Name + td.history-quantity= purchase.Quantity + td.history-price= purchase.Price + td.history-date.utc-date(data-date=purchase.Date) \ No newline at end of file diff --git a/pages/shop/history.scarlet b/pages/shop/history.scarlet new file mode 100644 index 00000000..da4725b5 --- /dev/null +++ b/pages/shop/history.scarlet @@ -0,0 +1,2 @@ +.history-price, .history-date, .history-quantity + text-align right \ No newline at end of file diff --git a/pages/shop/shop.go b/pages/shop/shop.go index 928fe12c..16ec90a1 100644 --- a/pages/shop/shop.go +++ b/pages/shop/shop.go @@ -3,7 +3,6 @@ package shop import ( "net/http" "sort" - "sync" "github.com/animenotifier/arn" @@ -12,8 +11,6 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -var itemBuyMutex sync.Mutex - // Get shop page. func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) @@ -34,56 +31,3 @@ func Get(ctx *aero.Context) string { return ctx.HTML(components.Shop(user, items)) } - -// BuyItem ... -func BuyItem(ctx *aero.Context) string { - // Lock via mutex to prevent race conditions - itemBuyMutex.Lock() - defer itemBuyMutex.Unlock() - - // Logged in user - user := utils.GetUser(ctx) - - if user == nil { - return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) - } - - // Item ID and quantity - itemID := ctx.Get("item") - quantity, err := ctx.GetInt("quantity") - - if err != nil || quantity == 0 { - return ctx.Error(http.StatusBadRequest, "Invalid item quantity", err) - } - - item, err := arn.GetItem(itemID) - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error fetching item data", err) - } - - // Calculate total price and subtract balance - totalPrice := int(item.Price) * quantity - - if user.Balance < totalPrice { - return ctx.Error(http.StatusBadRequest, "Not enough gems", nil) - } - - user.Balance -= totalPrice - err = user.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving user data", err) - } - - // Add item to user inventory - inventory := user.Inventory() - inventory.AddItem(itemID, uint(quantity)) - err = inventory.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving inventory", err) - } - - return "ok" -} diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy index 8556163e..81239903 100644 --- a/pages/shop/shop.pixy +++ b/pages/shop/shop.pixy @@ -11,12 +11,14 @@ component ShopTabs(user *arn.User) .tabs Tab("Shop", "shopping-cart", "/shop") Tab("Inventory", "briefcase", "/inventory") + Tab("History", "history", "/shop/history") Tab(strconv.Itoa(user.Balance), "diamond", "/charge") component ShopItem(item *arn.Item) - .widget.shop-item.mountable + .widget.shop-item.mountable(data-item-id=item.ID) h3.widget-title.shop-item-name - Icon(item.Icon) + .item-icon + Icon(item.Icon) span= item.Name //- span.shop-item-duration= " " + duration .shop-item-description!= aero.Markdown(item.Description) diff --git a/pages/shop/shop.scarlet b/pages/shop/shop.scarlet index b2374edc..a99e56e2 100644 --- a/pages/shop/shop.scarlet +++ b/pages/shop/shop.scarlet @@ -1,11 +1,33 @@ +item-color-pro-account = hsl(0, 100%, 71%) +item-color-anime-support-ticket = hsl(217, 64%, 50%) + .shop-items // ... -.shop-item - // ... +.item-icon + display inline-block -.shop-item-name - // ... +// Colors +.shop-item, .inventory-slot + [data-item-id="pro-account-3"] + .item-icon + color item-color-pro-account + + [data-item-id="pro-account-6"] + .item-icon + color item-color-pro-account + + [data-item-id="pro-account-12"] + .item-icon + color item-color-pro-account + + [data-item-id="pro-account-24"] + .item-icon + color item-color-pro-account + + [data-item-id="anime-support-ticket"] + .item-icon + color item-color-anime-support-ticket .shop-item-price // ... diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index 1e08a94e..ba9f4784 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -85,7 +85,9 @@ Includes: Price: 100, Description: `Support the makers of your favourite anime by using an anime support ticket. Anime Notifier uses 10% of the money to handle the transaction fees while the remaining 90% go directly -to the studios involved in the creation of your favourite anime.`, +to the studios involved in the creation of your favourite anime. + +*This feature is work in progress.*`, Icon: "ticket", Rarity: arn.ItemRarityRare, Order: 5, diff --git a/patches/delete-pro/delete-pro.go b/patches/delete-pro/delete-pro.go new file mode 100644 index 00000000..05b7b975 --- /dev/null +++ b/patches/delete-pro/delete-pro.go @@ -0,0 +1,38 @@ +package main + +import ( + "flag" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +// Shell parameters +var confirmed bool + +// Shell flags +func init() { + flag.BoolVar(&confirmed, "confirm", false, "Confirm that you really want to execute this.") + flag.Parse() +} + +func main() { + if !confirmed { + color.Green("Please run this command with -confirm option if you really want to delete all pro subscriptions.") + return + } + + color.Yellow("Deleting all pro subscriptions") + + // Get a stream of all users + allUsers, err := arn.StreamUsers() + arn.PanicOnError(err) + + // Iterate over the stream + for user := range allUsers { + user.Balance = 0 + arn.PanicOnError(user.Save()) + } + + color.Green("Finished.") +} diff --git a/patches/add-inventories/add-inventories.go b/patches/reset-inventories/reset-inventories.go similarity index 59% rename from patches/add-inventories/add-inventories.go rename to patches/reset-inventories/reset-inventories.go index 28966b6d..6ab25c83 100644 --- a/patches/add-inventories/add-inventories.go +++ b/patches/reset-inventories/reset-inventories.go @@ -1,14 +1,29 @@ package main import ( + "flag" "fmt" "github.com/animenotifier/arn" "github.com/fatih/color" ) +// Shell parameters +var confirmed bool + +// Shell flags +func init() { + flag.BoolVar(&confirmed, "confirm", false, "Confirm that you really want to execute this.") + flag.Parse() +} + func main() { - color.Yellow("Adding inventories to users who don't have one") + if !confirmed { + color.Green("Please run this command with -confirm option.") + return + } + + color.Yellow("Resetting all inventories") // Get a stream of all users allUsers, err := arn.StreamUsers() @@ -16,20 +31,9 @@ func main() { // Iterate over the stream for user := range allUsers { - exists, err := arn.DB.Exists("Inventory", user.ID) - - if err != nil || exists { - continue - } - fmt.Println(user.Nick) inventory := arn.NewInventory(user.ID) - - // // TEST - // inventory.AddItem("anime-support-ticket", 50) - // inventory.AddItem("pro-account-24", 30) - err = arn.DB.Set("Inventory", inventory.UserID, inventory) if err != nil { diff --git a/tests.go b/tests.go index 0529e2b5..75b135a6 100644 --- a/tests.go +++ b/tests.go @@ -247,6 +247,7 @@ var routeTests = map[string][]string{ "/user": nil, "/settings": nil, "/shop": nil, + "/shop/history": nil, "/charge": nil, "/inventory": nil, "/extension/embed": nil, From 6c641c3632a4bbf3db5fd82271c84c280711efc6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 13:03:36 +0200 Subject: [PATCH 397/527] General improvements --- pages/dashboard/dashboard.pixy | 24 -------------- pages/frontpage/frontpage.pixy | 57 +++++++++++++++++--------------- pages/login/login.pixy | 2 +- patches/add-item/add-item.go | 43 ++++++++++++++++++++++++ patches/delete-pro/delete-pro.go | 9 ++--- 5 files changed, 76 insertions(+), 59 deletions(-) create mode 100644 patches/add-item/add-item.go diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 8a0f3230..06692c32 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -122,27 +122,3 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound //- .widget-element-text //- Icon("github") //- span GitHub - -component Footer - .footer.text-center - span.footer-element Anime Notifier - - a.footer-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") - Icon("microphone") - span Discord - - a.footer-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") - Icon("facebook") - span Facebook - - a.footer-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") - Icon("twitter") - span Twitter - - a.footer-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") - Icon("google-plus") - span Google+ - - a.footer-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") - Icon("github") - span GitHub diff --git a/pages/frontpage/frontpage.pixy b/pages/frontpage/frontpage.pixy index 2c54cce7..46fc1632 100644 --- a/pages/frontpage/frontpage.pixy +++ b/pages/frontpage/frontpage.pixy @@ -1,36 +1,39 @@ component FrontPage .frontpage-background - .frontpage.mountable - h1 notify.moe + .frontpage + h1.mountable notify.moe - h2 Your home for everything about anime. + h2.mountable Your home for everything about anime. - //- 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.footer-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") - Icon("microphone") - span Discord - - a.footer-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") - Icon("facebook") - span Facebook - - a.footer-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") - Icon("twitter") - span Twitter - - a.footer-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") - Icon("google-plus") - span Google+ - - a.footer-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") - Icon("github") - span GitHub + Footer video.bg-video(autoplay="autoplay", loop="loop") source(src="//s1.webmshare.com/nZVby.webm", type="video/webm") - source(src="//cdn-e2.streamable.com/video/mp4/e5mx7.mp4?token=1500414089_8b2b3b0665984dcf4dc8d33e534bc1c8881b2da1", type="video/mp4") \ No newline at end of file + source(src="//cdn-e2.streamable.com/video/mp4/e5mx7.mp4?token=1500414089_8b2b3b0665984dcf4dc8d33e534bc1c8881b2da1", type="video/mp4") + +component Footer + .footer.text-center.mountable + SocialMediaLinks + +component SocialMediaLinks + a.footer-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") + Icon("microphone") + span Discord + + a.footer-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") + Icon("facebook") + span Facebook + + a.footer-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") + Icon("twitter") + span Twitter + + a.footer-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") + Icon("google-plus") + span Google+ + + a.footer-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") + Icon("github") + span GitHub \ No newline at end of file diff --git a/pages/login/login.pixy b/pages/login/login.pixy index 23396163..34d4b59f 100644 --- a/pages/login/login.pixy +++ b/pages/login/login.pixy @@ -1,5 +1,5 @@ component Login - .login-buttons + .login-buttons.mountable a.login-button.login-button-google(href="/auth/google") Icon("google") span Sign in via Google diff --git a/patches/add-item/add-item.go b/patches/add-item/add-item.go new file mode 100644 index 00000000..d624745b --- /dev/null +++ b/patches/add-item/add-item.go @@ -0,0 +1,43 @@ +package main + +import ( + "flag" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +var nick string +var itemID string +var quantity int + +func init() { + flag.StringVar(&nick, "nick", "", "Name of the user.") + flag.StringVar(&itemID, "item", "", "ID of the item.") + flag.IntVar(&quantity, "q", 1, "Item quantity.") + flag.Parse() +} + +func main() { + if nick == "" || itemID == "" { + color.Red("Missing parameters") + return + } + + user, err := arn.GetUserByNick(nick) + arn.PanicOnError(err) + + item, err := arn.GetItem(itemID) + arn.PanicOnError(err) + + if item == nil { + color.Red("Unknown item") + return + } + + // Add to user inventory + inventory := user.Inventory() + inventory.AddItem(itemID, uint(quantity)) + err = inventory.Save() + arn.PanicOnError(err) +} diff --git a/patches/delete-pro/delete-pro.go b/patches/delete-pro/delete-pro.go index 05b7b975..148c7421 100644 --- a/patches/delete-pro/delete-pro.go +++ b/patches/delete-pro/delete-pro.go @@ -24,13 +24,8 @@ func main() { color.Yellow("Deleting all pro subscriptions") - // Get a stream of all users - allUsers, err := arn.StreamUsers() - arn.PanicOnError(err) - - // Iterate over the stream - for user := range allUsers { - user.Balance = 0 + for user := range arn.MustStreamUsers() { + user.ProExpires = "" arn.PanicOnError(user.Save()) } From 3d579da338c5ca501d2520400da57346cc42ea56 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 13:27:07 +0200 Subject: [PATCH 398/527] Dashboard is back --- mixins/Sidebar.pixy | 5 ++--- pages/dashboard/dashboard.pixy | 28 +--------------------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 3d5132e2..bd54c66e 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -7,6 +7,7 @@ component Sidebar(user *arn.User) if user != nil SidebarButton("Home", "/animelist/watching", "home") + SidebarButton("Dash", "/dashboard", "tachometer") else SidebarButton("Home", "/", "home") @@ -20,9 +21,7 @@ component Sidebar(user *arn.User) //- SidebarButton("Search", "/search", "search") if user != nil - if user.Role == "admin" || user.Role == "editor" - SidebarButton("Shop", "/shop", "shopping-cart") - + SidebarButton("Shop", "/shop", "shopping-cart") SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 06692c32..37753eb9 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -95,30 +95,4 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound Icon("address-card") span ... - //- .widget.mountable - //- h3.widget-title Follow - - //- a.widget-element(href="https://discord.gg/0kimAmMCeXGXuzNF", target="_blank", rel="noopener") - //- .widget-element-text - //- Icon("microphone") - //- span Discord - - //- a.widget-element(href="https://www.facebook.com/animenotifier", target="_blank", rel="noopener") - //- .widget-element-text - //- Icon("facebook") - //- span Facebook - - //- a.widget-element(href="https://twitter.com/animenotifier", target="_blank", rel="noopener") - //- .widget-element-text - //- Icon("twitter") - //- span Twitter - - //- a.widget-element(href="https://plus.google.com/+AnimeReleaseNotifierOfficial", target="_blank", rel="noopener") - //- .widget-element-text - //- Icon("google-plus") - //- span Google+ - - //- a.widget-element(href="https://github.com/animenotifier/notify.moe", target="_blank", rel="noopener") - //- .widget-element-text - //- Icon("github") - //- span GitHub + Footer \ No newline at end of file From 42320917f852d70bb354c722630c0a04abbee248 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 13:58:08 +0200 Subject: [PATCH 399/527] Minor changes --- pages/paypal/success.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/paypal/success.go b/pages/paypal/success.go index 52c57b81..ee7f609f 100644 --- a/pages/paypal/success.go +++ b/pages/paypal/success.go @@ -86,7 +86,7 @@ func Success(ctx *aero.Context) string { go func() { admin, _ := arn.GetUser(adminID) admin.SendNotification(&arn.Notification{ - Title: user.Nick + " bought " + strconv.Itoa(payment.Gems()) + " AD", + Title: user.Nick + " bought " + strconv.Itoa(payment.Gems()) + " gems", Message: user.Nick + " paid " + payment.Amount + " " + payment.Currency + " making his new balance " + strconv.Itoa(user.Balance), Icon: user.LargeAvatar(), Link: "https://" + ctx.App.Config.Domain + "/api/paypalpayment/" + payment.ID, From b250f2ea7ee8d3da6f91276726b52ae327edd021 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 20:03:24 +0200 Subject: [PATCH 400/527] Fixed purchase history --- pages/shop/history.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pages/shop/history.go b/pages/shop/history.go index d390bb69..87b77d80 100644 --- a/pages/shop/history.go +++ b/pages/shop/history.go @@ -18,7 +18,9 @@ func PurchaseHistory(ctx *aero.Context) string { return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } - purchases, err := arn.AllPurchases() + purchases, err := arn.FilterPurchases(func(purchase *arn.Purchase) bool { + return purchase.UserID == user.ID + }) if err != nil { return ctx.Error(http.StatusInternalServerError, "Error fetching shop item data", err) From c9f51037dc3999c149cd77d2fd58f63b1eddcd92 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 6 Oct 2017 22:07:12 +0200 Subject: [PATCH 401/527] Improved editor tools --- main.go | 8 ++- pages/admin/admin.pixy | 6 ++- pages/admin/webdev.pixy | 81 ++++++++++++++-------------- pages/{admin => editor}/anilist.go | 20 ++++++- pages/{admin => editor}/anilist.pixy | 15 ++++-- pages/editor/editor.go | 18 +++++++ pages/editor/editor.pixy | 16 ++++++ pages/{admin => editor}/shoboi.go | 20 ++++++- pages/{admin => editor}/shoboi.pixy | 15 ++++-- sw/service-worker.ts | 1 - tests.go | 4 +- 11 files changed, 148 insertions(+), 56 deletions(-) rename pages/{admin => editor}/anilist.go (61%) rename pages/{admin => editor}/anilist.pixy (73%) create mode 100644 pages/editor/editor.go create mode 100644 pages/editor/editor.pixy rename pages/{admin => editor}/shoboi.go (61%) rename pages/{admin => editor}/shoboi.pixy (73%) diff --git a/main.go b/main.go index 901769b0..e7ae17b5 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "github.com/animenotifier/notify.moe/pages/charge" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/editanime" + "github.com/animenotifier/notify.moe/pages/editor" "github.com/animenotifier/notify.moe/pages/embed" "github.com/animenotifier/notify.moe/pages/explore" "github.com/animenotifier/notify.moe/pages/forum" @@ -134,10 +135,13 @@ func configure(app *aero.Application) *aero.Application { // Admin app.Ajax("/admin", admin.Get) - app.Ajax("/admin/anilist", admin.AniList) - app.Ajax("/admin/shoboi", admin.Shoboi) app.Ajax("/admin/webdev", admin.WebDev) + // Editor + app.Ajax("/editor", editor.Get) + app.Ajax("/editor/anilist", editor.AniList) + app.Ajax("/editor/shoboi", editor.Shoboi) + // Import app.Ajax("/import", listimport.Get) app.Ajax("/import/anilist/animelist", listimportanilist.Preview) diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index d7a94611..1bb6596e 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -2,8 +2,10 @@ component AdminTabs .tabs Tab("Server", "server", "/admin") Tab("WebDev", "html5", "/admin/webdev") - Tab("Shoboi", "calendar", "/admin/shoboi") - Tab("AniList", "list", "/admin/anilist") + + a.tab.ajax(href="/editor", aria-label="Editor") + Icon("pencil") + span.tab-text Editor component Admin(user *arn.User, cpuUsage, memUsage, diskUsage float64, platform, family, platformVersion, kernelVersion string) h1.page-title Admin Panel diff --git a/pages/admin/webdev.pixy b/pages/admin/webdev.pixy index a9c31884..0be46d3a 100644 --- a/pages/admin/webdev.pixy +++ b/pages/admin/webdev.pixy @@ -2,44 +2,47 @@ component WebDev AdminTabs h1.page-title WebDev - - h2 Tests - - .buttons - a.button.mountable(href="https://developers.google.com/speed/pagespeed/insights/?url=https://notify.moe/&tab=desktop", target="_blank", rel="noopener") - Icon("external-link") - span Google PageSpeed - a.button.mountable(href="https://observatory.mozilla.org/analyze.html?host=notify.moe", target="_blank", rel="noopener") - Icon("external-link") - span Mozilla Observatory - a.button.mountable(href="https://html5.validator.nu/?doc=https://notify.moe", target="_blank", rel="noopener") - Icon("external-link") - span HTML5 Validator - a.button.mountable(href="https://testmysite.withgoogle.com/", target="_blank", rel="noopener") - Icon("external-link") - span Mobile Speed - a.button.mountable(href="https://www.webpagetest.org/", target="_blank", rel="noopener") - Icon("external-link") - span Web Page Test - h2 Browser Support + .widgets + .widget.mountable + h3.widget-title Tests - .buttons - a.button.mountable(href="http://caniuse.com/#feat=webp", target="_blank", rel="noopener") - Icon("external-link") - span WebP - a.button.mountable(href="http://caniuse.com/#feat=push-api", target="_blank", rel="noopener") - Icon("external-link") - span Push API - a.button.mountable(href="http://caniuse.com/#feat=serviceworkers", target="_blank", rel="noopener") - Icon("external-link") - span Service Worker - a.button.mountable(href="http://caniuse.com/#feat=intersectionobserver", target="_blank", rel="noopener") - Icon("external-link") - span Intersection Observer - a.button.mountable(href="http://caniuse.com/#feat=requestidlecallback", target="_blank", rel="noopener") - Icon("external-link") - span Request Idle Callback - a.button.mountable(href="http://caniuse.com/#feat=css-variables", target="_blank", rel="noopener") - Icon("external-link") - span CSS Variables \ No newline at end of file + .buttons + a.button.mountable(href="https://developers.google.com/speed/pagespeed/insights/?url=https://notify.moe/&tab=desktop", target="_blank", rel="noopener") + Icon("external-link") + span Google PageSpeed + a.button.mountable(href="https://observatory.mozilla.org/analyze.html?host=notify.moe", target="_blank", rel="noopener") + Icon("external-link") + span Mozilla Observatory + a.button.mountable(href="https://html5.validator.nu/?doc=https://notify.moe", target="_blank", rel="noopener") + Icon("external-link") + span HTML5 Validator + a.button.mountable(href="https://testmysite.withgoogle.com/", target="_blank", rel="noopener") + Icon("external-link") + span Mobile Speed + a.button.mountable(href="https://www.webpagetest.org/", target="_blank", rel="noopener") + Icon("external-link") + span Web Page Test + + .widget.mountable + h3.widget-title Browser Support + + .buttons + a.button.mountable(href="http://caniuse.com/#feat=webp", target="_blank", rel="noopener") + Icon("external-link") + span WebP + a.button.mountable(href="http://caniuse.com/#feat=push-api", target="_blank", rel="noopener") + Icon("external-link") + span Push API + a.button.mountable(href="http://caniuse.com/#feat=serviceworkers", target="_blank", rel="noopener") + Icon("external-link") + span Service Worker + a.button.mountable(href="http://caniuse.com/#feat=intersectionobserver", target="_blank", rel="noopener") + Icon("external-link") + span Intersection Observer + a.button.mountable(href="http://caniuse.com/#feat=requestidlecallback", target="_blank", rel="noopener") + Icon("external-link") + span Request Idle Callback + a.button.mountable(href="http://caniuse.com/#feat=css-variables", target="_blank", rel="noopener") + Icon("external-link") + span CSS Variables \ No newline at end of file diff --git a/pages/admin/anilist.go b/pages/editor/anilist.go similarity index 61% rename from pages/admin/anilist.go rename to pages/editor/anilist.go index bcd74838..ffb8678b 100644 --- a/pages/admin/anilist.go +++ b/pages/editor/anilist.go @@ -1,4 +1,4 @@ -package admin +package editor import ( "net/http" @@ -9,6 +9,8 @@ import ( "github.com/animenotifier/notify.moe/components" ) +const maxAniListEntries = 70 + // AniList ... func AniList(ctx *aero.Context) string { missing, err := arn.FilterAnime(func(anime *arn.Anime) bool { @@ -20,8 +22,22 @@ func AniList(ctx *aero.Context) string { } sort.Slice(missing, func(i, j int) bool { - return missing[i].StartDate > missing[j].StartDate + a := missing[i] + b := missing[j] + + aPop := a.Popularity.Total() + bPop := b.Popularity.Total() + + if aPop == bPop { + return a.Title.Canonical < b.Title.Canonical + } + + return aPop > bPop }) + if len(missing) > maxAniListEntries { + missing = missing[:maxAniListEntries] + } + return ctx.HTML(components.AniListMissingMapping(missing)) } diff --git a/pages/admin/anilist.pixy b/pages/editor/anilist.pixy similarity index 73% rename from pages/admin/anilist.pixy rename to pages/editor/anilist.pixy index cbed563c..d27b6086 100644 --- a/pages/admin/anilist.pixy +++ b/pages/editor/anilist.pixy @@ -1,16 +1,25 @@ component AniListMissingMapping(missing []*arn.Anime) h1.page-title Anime without Anilist links - AdminTabs + EditorTabs table + thead + tr + th(title="Popularity") Pop. + th Title + th Type + th Year + th Tools tbody each anime in missing tr + td= anime.Popularity.Total() + td + a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical + td= anime.Type td if len(anime.StartDate) >= 4 span= anime.StartDate[:4] - td - a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical td a(href="https://anilist.co/search?type=anime&q=" + anime.Title.Canonical, target="_blank", rel="noopener") Search diff --git a/pages/editor/editor.go b/pages/editor/editor.go new file mode 100644 index 00000000..3abccc97 --- /dev/null +++ b/pages/editor/editor.go @@ -0,0 +1,18 @@ +package editor + +import ( + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get ... +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil || (user.Role != "admin" && user.Role != "editor") { + return ctx.Redirect("/") + } + + return ctx.HTML(components.Editor()) +} diff --git a/pages/editor/editor.pixy b/pages/editor/editor.pixy new file mode 100644 index 00000000..c4af0c40 --- /dev/null +++ b/pages/editor/editor.pixy @@ -0,0 +1,16 @@ +component Editor + h1.page-title Editor Panel + + EditorTabs + + p Welcome to the Editor Panel! + +component EditorTabs + .tabs + Tab("Editor", "pencil", "/editor") + Tab("Shoboi", "calendar", "/editor/shoboi") + Tab("AniList", "list", "/editor/anilist") + + a.tab.ajax(href="/admin", aria-label="Admin") + Icon("wrench") + span.tab-text Admin \ No newline at end of file diff --git a/pages/admin/shoboi.go b/pages/editor/shoboi.go similarity index 61% rename from pages/admin/shoboi.go rename to pages/editor/shoboi.go index c4cdbd33..dd33e329 100644 --- a/pages/admin/shoboi.go +++ b/pages/editor/shoboi.go @@ -1,4 +1,4 @@ -package admin +package editor import ( "net/http" @@ -9,6 +9,8 @@ import ( "github.com/animenotifier/notify.moe/components" ) +const maxShoboiEntries = 70 + // Shoboi ... func Shoboi(ctx *aero.Context) string { missing, err := arn.FilterAnime(func(anime *arn.Anime) bool { @@ -20,8 +22,22 @@ func Shoboi(ctx *aero.Context) string { } sort.Slice(missing, func(i, j int) bool { - return missing[i].StartDate > missing[j].StartDate + a := missing[i] + b := missing[j] + + aPop := a.Popularity.Total() + bPop := b.Popularity.Total() + + if aPop == bPop { + return a.Title.Canonical < b.Title.Canonical + } + + return aPop > bPop }) + if len(missing) > maxShoboiEntries { + missing = missing[:maxShoboiEntries] + } + return ctx.HTML(components.ShoboiMissingMapping(missing)) } diff --git a/pages/admin/shoboi.pixy b/pages/editor/shoboi.pixy similarity index 73% rename from pages/admin/shoboi.pixy rename to pages/editor/shoboi.pixy index 5224eae7..8dd2aca0 100644 --- a/pages/admin/shoboi.pixy +++ b/pages/editor/shoboi.pixy @@ -1,16 +1,25 @@ component ShoboiMissingMapping(missing []*arn.Anime) h1.page-title Anime without Shoboi links - AdminTabs + EditorTabs table + thead + tr + th(title="Popularity") Pop. + th Title + th Type + th Year + th Tools tbody each anime in missing tr + td= anime.Popularity.Total() + td + a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical + td= anime.Type td if len(anime.StartDate) >= 4 span= anime.StartDate[:4] - td - a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical td a(href="http://cal.syoboi.jp/find?type=quick&sd=1&kw=" + anime.Title.Japanese, target="_blank", rel="noopener") Search diff --git a/sw/service-worker.ts b/sw/service-worker.ts index da79784a..6f800784 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -6,7 +6,6 @@ const ETAGS = new Map() const CACHEREFRESH = new Map>() const EXCLUDECACHE = new Set([ "/api/", - "/admin/", "/paypal/", "/import/", "chrome-extension" diff --git a/tests.go b/tests.go index 75b135a6..07253ae9 100644 --- a/tests.go +++ b/tests.go @@ -242,8 +242,8 @@ var routeTests = map[string][]string{ "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, - "/admin/anilist": nil, - "/admin/shoboi": nil, + "/editor/anilist": nil, + "/editor/shoboi": nil, "/user": nil, "/settings": nil, "/shop": nil, From 13c45d3d2b0623419b0c43a22888cbca58fe5f8b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 7 Oct 2017 10:01:55 +0200 Subject: [PATCH 402/527] Added PRO star to profiles --- pages/profile/profile.pixy | 6 ++++++ pages/profile/profile.scarlet | 25 ++++++++++++++++++------- styles/include/config.scarlet | 2 +- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 2ccb0985..135ce40e 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -44,6 +44,12 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) Icon("rocket") span= arn.Capitalize(viewUser.Role) + if viewUser.IsPro() + p.profile-field.profile-pro-status + a.ajax(href="/shop", title="PRO user") + Icon("star") + span.profile-pro-status-text PRO + if user != nil .profile-actions if user.ID != viewUser.ID diff --git a/pages/profile/profile.scarlet b/pages/profile/profile.scarlet index 1d377010..6b1c9147 100644 --- a/pages/profile/profile.scarlet +++ b/pages/profile/profile.scarlet @@ -20,7 +20,6 @@ profile-boot-duration = 2s .profile-field text-align center - a color white @@ -41,6 +40,13 @@ profile-boot-duration = 2s margin-bottom 0.5rem text-shadow none !important +.profile-pro-status + margin-top calc(typography-margin * 2) + + .icon + color pro-color + animation sk-pulse 1.5s infinite linear + > 740px .profile horizontal @@ -63,6 +69,16 @@ profile-boot-duration = 2s right 0 padding content-padding margin-top 0 + + .profile-pro-status + position absolute + right 0 + bottom 0 + padding content-padding + margin-top 0 + + // .profile-pro-status-text + // display none // animation appear // 0% @@ -111,9 +127,4 @@ profile-boot-duration = 2s .no-data width 100% - text-align center - -// Categories - -// .profile-category -// margin-bottom content-padding \ No newline at end of file + text-align center \ No newline at end of file diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 9e87a449..7636e3bb 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -5,7 +5,7 @@ link-color = rgb(215, 38, 15) link-hover-color = rgb(242, 60, 30) link-active-color = link-hover-color bg-color = rgb(246, 246, 246) -pro-color = hsla(0, 100%, 77%, 0.87) +pro-color = hsla(0, 100%, 73%, 0.87) // UI ui-border-color = rgba(0, 0, 0, 0.1) From d5119d8e2a5b755450232293cec764e4a81fe719 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 7 Oct 2017 10:18:50 +0200 Subject: [PATCH 403/527] Updated shop items --- patches/add-shop-items/add-shop-items.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index ba9f4784..1efd0547 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -14,8 +14,9 @@ var items = []*arn.Item{ Includes: * Special highlight on the forums -* High priority for your personal suggestions * Access to the VIP channel on Discord +* PRO star on your profile +* High priority for your personal suggestions * Early access to new features`, Icon: "star", Rarity: arn.ItemRaritySuperior, @@ -33,8 +34,9 @@ Includes: Includes: * Special highlight on the forums -* High priority for your personal suggestions * Access to the VIP channel on Discord +* PRO star on your profile +* High priority for your personal suggestions * Early access to new features`, Icon: "star", Rarity: arn.ItemRarityRare, @@ -52,8 +54,9 @@ Includes: Includes: * Special highlight on the forums -* High priority for your personal suggestions * Access to the VIP channel on Discord +* PRO star on your profile +* High priority for your personal suggestions * Early access to new features`, Icon: "star", Rarity: arn.ItemRarityUnique, @@ -71,8 +74,9 @@ Includes: Includes: * Special highlight on the forums -* High priority for your personal suggestions * Access to the VIP channel on Discord +* PRO star on your profile +* High priority for your personal suggestions * Early access to new features`, Icon: "star", Rarity: arn.ItemRarityLegendary, From 2d71b2408edd4a795e451052f1dfa63958156332 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 7 Oct 2017 10:20:26 +0200 Subject: [PATCH 404/527] Updated shop items --- patches/add-shop-items/add-shop-items.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index 1efd0547..af303e4b 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -88,7 +88,7 @@ Includes: Name: "Anime Support Ticket", Price: 100, Description: `Support the makers of your favourite anime by using an anime support ticket. -Anime Notifier uses 10% of the money to handle the transaction fees while the remaining 90% go directly +Anime Notifier uses 15% of the money to handle the transaction fees while the remaining 85% go directly to the studios involved in the creation of your favourite anime. *This feature is work in progress.*`, From 5becba19de391afafb9ab996af11efafab03b548 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 7 Oct 2017 10:52:59 +0200 Subject: [PATCH 405/527] Improved osu ranking list --- pages/users/osu.pixy | 21 +++++++++++++++++++++ pages/users/osu.scarlet | 12 ++++++++++++ pages/users/users.go | 6 +++++- pages/users/users.pixy | 26 +++++++++----------------- 4 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 pages/users/osu.pixy create mode 100644 pages/users/osu.scarlet diff --git a/pages/users/osu.pixy b/pages/users/osu.pixy new file mode 100644 index 00000000..653e72ce --- /dev/null +++ b/pages/users/osu.pixy @@ -0,0 +1,21 @@ +component OsuRankingList(users []*arn.User) + h1.page-title osu! ranking list + + UsersTabs + + table.osu-ranking-list + thead + tr + th # + th Player + th.osu-ranking-pp Performance + th.osu-ranking-accuracy Accuracy + tbody + for index, user := range users + tr.osu-ranking.mountable + td= toString(index + 1) + "." + td + Avatar(user) + td.osu-ranking-pp= toString(int(user.Accounts.Osu.PP + 0.5)) + " pp" + td.osu-ranking-accuracy= fmt.Sprintf("%.1f", user.Accounts.Osu.Accuracy) + "%" + \ No newline at end of file diff --git a/pages/users/osu.scarlet b/pages/users/osu.scarlet new file mode 100644 index 00000000..fbb47b8c --- /dev/null +++ b/pages/users/osu.scarlet @@ -0,0 +1,12 @@ +.osu-ranking-list + max-width 400px + +.osu-ranking + width 100% + + td + vertical-align middle + +.osu-ranking-pp, +.osu-ranking-accuracy + text-align right \ No newline at end of file diff --git a/pages/users/users.go b/pages/users/users.go index 09a3b989..919af4af 100644 --- a/pages/users/users.go +++ b/pages/users/users.go @@ -27,7 +27,11 @@ func Osu(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) } - return ctx.HTML(components.Users(users)) + if len(users) > 50 { + users = users[:50] + } + + return ctx.HTML(components.OsuRankingList(users)) } // Staff ... diff --git a/pages/users/users.pixy b/pages/users/users.pixy index 5dbfd80a..af6f65d8 100644 --- a/pages/users/users.pixy +++ b/pages/users/users.pixy @@ -1,24 +1,16 @@ component Users(users []*arn.User) h1.page-title Users - .tabs - a.tab.action(href="/users", data-action="diff", data-trigger="click") - Icon("users") - span.tab-text Active - - a.tab.action(href="/users/anime/watching", data-action="diff", data-trigger="click") - Icon("tv") - span.tab-text Watching - - a.tab.action(href="/users/osu", data-action="diff", data-trigger="click") - Icon("gamepad") - span.tab-text Osu - - a.tab.action(href="/users/staff", data-action="diff", data-trigger="click") - Icon("user-secret") - span.tab-text Staff + UsersTabs .user-avatars each user in users .mountable - Avatar(user) \ No newline at end of file + Avatar(user) + +component UsersTabs + .tabs + Tab("Active", "users", "/users") + Tab("Watching", "tv", "/users/anime/watching") + Tab("Osu", "gamepad", "/users/osu") + Tab("Staff", "user-secret", "/users/staff") \ No newline at end of file From a9324b474019ea5b361471720a8a55ca1a183774 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 7 Oct 2017 21:04:32 +0200 Subject: [PATCH 406/527] Updated to latest Aero --- pages/shop/shop.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy index 81239903..750616a4 100644 --- a/pages/shop/shop.pixy +++ b/pages/shop/shop.pixy @@ -21,7 +21,7 @@ component ShopItem(item *arn.Item) Icon(item.Icon) span= item.Name //- span.shop-item-duration= " " + duration - .shop-item-description!= aero.Markdown(item.Description) + .shop-item-description!= markdown.Render(item.Description) .buttons.shop-buttons button.shop-button-buy.action(data-item-id=item.ID, data-item-name=item.Name, data-price=item.Price, data-trigger="click", data-action="buyItem") span.shop-item-price= item.Price From b8f52e1217e4a6c6f4f40377dc2131bb58634c6d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 7 Oct 2017 23:24:09 +0200 Subject: [PATCH 407/527] Improved admin stuff --- main.go | 1 + mixins/Sidebar.pixy | 8 ++++++-- pages/admin/admin.pixy | 1 + pages/admin/purchases.go | 36 ++++++++++++++++++++++++++++++++++++ pages/admin/purchases.pixy | 20 ++++++++++++++++++++ pages/editor/editor.pixy | 6 +++--- pages/shop/history.pixy | 15 +++++++++------ tests.go | 1 + 8 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 pages/admin/purchases.go create mode 100644 pages/admin/purchases.pixy diff --git a/main.go b/main.go index e7ae17b5..94aee30f 100644 --- a/main.go +++ b/main.go @@ -136,6 +136,7 @@ func configure(app *aero.Application) *aero.Application { // Admin app.Ajax("/admin", admin.Get) app.Ajax("/admin/webdev", admin.WebDev) + app.Ajax("/admin/purchases", admin.PurchaseHistory) // Editor app.Ajax("/editor", editor.Get) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index bd54c66e..0a53a96b 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -32,8 +32,12 @@ component Sidebar(user *arn.User) Icon("search") FuzzySearch - if user != nil && (user.Role == "admin" || user.Role == "editor") - SidebarButton("Admin", "/admin", "wrench") + if user != nil + if user.Role == "admin" + SidebarButton("Admin", "/admin", "wrench") + + if user.Role == "editor" + SidebarButton("Editor", "/editor", "pencil") SidebarButton("Help", "/thread/I3MMiOtzR", "question-circle") diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index 1bb6596e..aa6cd7f1 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -2,6 +2,7 @@ component AdminTabs .tabs Tab("Server", "server", "/admin") Tab("WebDev", "html5", "/admin/webdev") + Tab("Purchases", "shopping-cart", "/admin/purchases") a.tab.ajax(href="/editor", aria-label="Editor") Icon("pencil") diff --git a/pages/admin/purchases.go b/pages/admin/purchases.go new file mode 100644 index 00000000..4cf1b70b --- /dev/null +++ b/pages/admin/purchases.go @@ -0,0 +1,36 @@ +package admin + +import ( + "net/http" + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// PurchaseHistory ... +func PurchaseHistory(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + if user.Role != "admin" { + return ctx.Error(http.StatusUnauthorized, "Not authorized", nil) + } + + purchases, err := arn.AllPurchases() + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching shop item data", err) + } + + sort.Slice(purchases, func(i, j int) bool { + return purchases[i].Date > purchases[j].Date + }) + + return ctx.HTML(components.GlobalPurchaseHistory(purchases)) +} diff --git a/pages/admin/purchases.pixy b/pages/admin/purchases.pixy new file mode 100644 index 00000000..fff793d6 --- /dev/null +++ b/pages/admin/purchases.pixy @@ -0,0 +1,20 @@ +component GlobalPurchaseHistory(purchases []*arn.Purchase) + AdminTabs + + h1.page-title All Purchases + + table + thead + tr.mountable + th User + th Icon + th Item + th.history-quantity Quantity + th.history-price Price + th.history-date Date + tbody + each purchase in purchases + tr.shop-item.mountable(data-item-id=purchase.ItemID) + td + a.ajax(href=purchase.User().Link())= purchase.User().Nick + PurchaseInfo(purchase) \ No newline at end of file diff --git a/pages/editor/editor.pixy b/pages/editor/editor.pixy index c4af0c40..ac9599b9 100644 --- a/pages/editor/editor.pixy +++ b/pages/editor/editor.pixy @@ -11,6 +11,6 @@ component EditorTabs Tab("Shoboi", "calendar", "/editor/shoboi") Tab("AniList", "list", "/editor/anilist") - a.tab.ajax(href="/admin", aria-label="Admin") - Icon("wrench") - span.tab-text Admin \ No newline at end of file + //- a.tab.ajax(href="/admin", aria-label="Admin") + //- Icon("wrench") + //- span.tab-text Admin \ No newline at end of file diff --git a/pages/shop/history.pixy b/pages/shop/history.pixy index 9bd2b2f3..f11e42e2 100644 --- a/pages/shop/history.pixy +++ b/pages/shop/history.pixy @@ -14,9 +14,12 @@ component PurchaseHistory(purchases []*arn.Purchase, user *arn.User) tbody each purchase in purchases tr.shop-item.mountable(data-item-id=purchase.ItemID) - td.item-icon - Icon(purchase.Item().Icon) - td= purchase.Item().Name - td.history-quantity= purchase.Quantity - td.history-price= purchase.Price - td.history-date.utc-date(data-date=purchase.Date) \ No newline at end of file + PurchaseInfo(purchase) + +component PurchaseInfo(purchase *arn.Purchase) + td.item-icon + Icon(purchase.Item().Icon) + td= purchase.Item().Name + td.history-quantity= purchase.Quantity + td.history-price= purchase.Price + td.history-date.utc-date(data-date=purchase.Date) \ No newline at end of file diff --git a/tests.go b/tests.go index 07253ae9..0b226fd3 100644 --- a/tests.go +++ b/tests.go @@ -242,6 +242,7 @@ var routeTests = map[string][]string{ "/anime/:id/edit": nil, "/new/thread": nil, "/new/soundtrack": nil, + "/admin/purchases": nil, "/editor/anilist": nil, "/editor/shoboi": nil, "/user": nil, From 405999d0427d9152de7fa72d6435bf424483183b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 8 Oct 2017 09:03:55 +0200 Subject: [PATCH 408/527] Fixed class diffing --- scripts/Diff.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 0fc7ffcb..38b96155 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -99,13 +99,18 @@ export class Diff { let classesA = elemA.classList let classesB = elemB.classList + let removeClasses: string[] = [] for(let className of classesA) { if(!classesB.contains(className) && !Diff.persistentClasses.has(className)) { - classesA.remove(className) + removeClasses.push(className) } } + for(let className of removeClasses) { + classesA.remove(className) + } + for(let className of classesB) { if(!classesA.contains(className)) { classesA.add(className) From 4272f57397eaa77b731ca8091ac561ef38f90a65 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 9 Oct 2017 08:18:28 +0200 Subject: [PATCH 409/527] Update info on Chrome extension --- pages/embed/embed-pro-notice.pixy | 8 ++++++++ pages/embed/embed.go | 5 +++++ patches/add-shop-items/add-shop-items.go | 4 ++++ 3 files changed, 17 insertions(+) create mode 100644 pages/embed/embed-pro-notice.pixy diff --git a/pages/embed/embed-pro-notice.pixy b/pages/embed/embed-pro-notice.pixy new file mode 100644 index 00000000..b0ac4249 --- /dev/null +++ b/pages/embed/embed-pro-notice.pixy @@ -0,0 +1,8 @@ +component EmbedProNotice(user *arn.User) + h1 notify.moe is in a financial crisis right now + p + spa If we don't get the necessary funding to keep it alive, the site needs to shut down. The developer works 12 hours per day on this project and doesn't receive a single cent. Please help us fund the project and get yourself a + a(href="https://notify.moe/shop", target="_blank") PRO account + span to support the site. It only costs about 2.6 USD per month. Read more about it + a(href="https://notify.moe/thread/A9nC8uakR", target="_blank") here + span . \ No newline at end of file diff --git a/pages/embed/embed.go b/pages/embed/embed.go index 8010d842..5eb51d0b 100644 --- a/pages/embed/embed.go +++ b/pages/embed/embed.go @@ -2,6 +2,7 @@ package embed import ( "net/http" + "time" "github.com/aerogo/aero" "github.com/animenotifier/notify.moe/components" @@ -16,6 +17,10 @@ func Get(ctx *aero.Context) string { return utils.AllowEmbed(ctx, ctx.HTML(components.Login())) } + if !user.IsPro() && user.TimeSinceRegistered() > 14*24*time.Hour { + return utils.AllowEmbed(ctx, ctx.HTML(components.EmbedProNotice(user))) + } + animeList := user.AnimeList() if animeList == nil { diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index af303e4b..8c44fd5d 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -14,6 +14,7 @@ var items = []*arn.Item{ Includes: * Special highlight on the forums +* Chrome extension for quick list access * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions @@ -34,6 +35,7 @@ Includes: Includes: * Special highlight on the forums +* Chrome extension for quick list access * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions @@ -54,6 +56,7 @@ Includes: Includes: * Special highlight on the forums +* Chrome extension for quick list access * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions @@ -74,6 +77,7 @@ Includes: Includes: * Special highlight on the forums +* Chrome extension for quick list access * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions From ff52fe0fa521658df85b8f17b39002f570e58878 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 9 Oct 2017 11:05:25 +0200 Subject: [PATCH 410/527] Minor changes --- pages/users/osu.pixy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pages/users/osu.pixy b/pages/users/osu.pixy index 653e72ce..8e6805ba 100644 --- a/pages/users/osu.pixy +++ b/pages/users/osu.pixy @@ -5,9 +5,10 @@ component OsuRankingList(users []*arn.User) table.osu-ranking-list thead - tr + tr.mountable th # th Player + th Name th.osu-ranking-pp Performance th.osu-ranking-accuracy Accuracy tbody @@ -16,6 +17,8 @@ component OsuRankingList(users []*arn.User) td= toString(index + 1) + "." td Avatar(user) + td + a.ajax(href=user.Link())= user.Nick td.osu-ranking-pp= toString(int(user.Accounts.Osu.PP + 0.5)) + " pp" td.osu-ranking-accuracy= fmt.Sprintf("%.1f", user.Accounts.Osu.Accuracy) + "%" \ No newline at end of file From be3dede5a9a6d1c8a6e18565cdd1a751edfb20e5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 9 Oct 2017 11:14:49 +0200 Subject: [PATCH 411/527] Updated Readme --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bb3c0450..9ae1eb37 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Because we made a notifier that takes your watching list, checks it against exte ## So it's just a notifier? -In the past it was, but we're growing bigger by establishing a database that combines information from multiple sources and also growing as a community. Many of us are hanging out on Discord and you are welcome to join us. We also have our own anime lists now due to popular request of adding episode progress changes to our browser extension. +In the past it was, but not anymore. We're growing bigger by establishing a database that combines information from multiple sources and also growing as a community. Many of us are hanging out on Discord and you are welcome to join us. We also have our own anime lists now due to popular request of adding episode progress changes to our browser extension. ## What does the current feature set look like? @@ -31,9 +31,17 @@ In the past it was, but we're growing bigger by establishing a database that com * Forums * Responsive layout (looks good on 1080p and on mobile devices) +## Can I follow the project on social media? + +* [Facebook](https://www.facebook.com/animenotifier) +* [Twitter](https://twitter.com/animenotifier) +* [Google+](https://plus.google.com/+AnimeReleaseNotifierOfficial) +* [GitHub](https://github.com/animenotifier/notify.moe) +* [Discord](https://discord.gg/0kimAmMCeXGXuzNF) + ## How do I enable notifications? -Use a browser that supports push notifications (Chrome or Firefox). Then go to your [settings](/settings) and click "Enable notifications". This might take a while, especially on mobile devices. After that you can press "Send test notification". If you get a notification saying "Yay, it works!" then everything's fine. The real thing looks like this: +Use a browser that supports push notifications (Chrome or Firefox). Then go to your [settings](https://notify.moe/settings) and click "Enable notifications". This might take a while, especially on mobile devices. After that you can press "Send test notification". If you get a notification saying "Yay, it works!" then everything's fine. The real thing looks like this: ![Anime Notifications](https://puu.sh/wKpcm/304a4441a0.png) @@ -79,6 +87,14 @@ A quick access to your watching list: You need to use [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). +## What is offline mode? + +This website / app is accessible even when you go offline. You can keep browsing the pages you visited earlier which is especially useful for mobile phones or when you're traveling with an unstable connection. Feel free to try it by disabling your WiFi and opening the site while offline. + +## Do I need to keep the site open to receive notifications? + +No, you can close the site and still receive notifications after you enabled them. + ## What are the community rules for conversations on the forum? * Be respectful to each other. @@ -99,6 +115,12 @@ To use an importer, enter your nickname for the site you want to import from and ![Anime list import](https://puu.sh/wM4dP/11d43e5f71.png) +## What does following a person do? + +You will be able to see their progress and ratings on anime pages: + +![Anime pages friends](https://puu.sh/wPfE2/d65ef4f771.png) + ## How do I install the site as an Android app? This website uses a modern technology that allows you to install websites as local apps. To install notify.moe as a mobile app, do the following: @@ -112,7 +134,7 @@ You need to enable notifications on each device separately. To receive notificat ## How do I install the site as a PC/desktop app? -In Chrome, open the top right menu and go to **More tools > Add to desktop**. +In Chrome, open the top right menu and go to **More tools > Add to desktop**. Make sure that "Open as window" is checked. ![Anime Notifier desktop app](https://puu.sh/wM4pB/542add3113.png) @@ -123,19 +145,56 @@ At the time of writing this, you get notified when: * A new episode from your watching list is released on twist.moe * Somebody replies in a thread you have participated in * Somebody likes your post +* You get a new follower ## How do notifications work from a technical perspective? There are many, many ways how notifications can be implemented from a technical standpoint. There is e.g. "polling" which means that an app periodically checks external sites and tells you when something new is available. We are not using polling because these periodic checks can quickly drain your battery on a mobile phone. We are using so-called "push notifications" instead. The advantage of push notifications is that your mobile phone or desktop PC doesn't have to do periodic checks anymore - instead the website will send new episode releases to all of your registered devices. This consumes less CPU/network resources and is much more battery friendly for mobile devices. -## Is this website well-optimized for performance? +## How can I confirm I'm a PRO user now? -You are free to [judge it yourself](https://twitter.com/eduardurbach/status/885631801171091460). +Go to your [settings](https://notify.moe/settings), it should show you the remaining duration for your [PRO](https://notify.moe/shop) account. + +## Is this website well-optimized? ![Anime Notifier - Lighthouse](https://pbs.twimg.com/media/DEplUsNXgAEF-UT.jpg:large) ![Anime Notifier - PageSpeed](https://pbs.twimg.com/media/DEplXmpWsAAPzb6.jpg:large) +## Is this website secure? + +* The site is not storing passwords which means there is no password that could be stolen +* The site uses HTTPS, CSP and CSS hashing to improve overall security +* The site functionality is 99.9% server-sided which is a requirement for any security related app +* The site is using only the most modern and secure SSL ciphers + +## Is this website mobile-friendly? + +Yes, we have a dynamic layout that works on everything from 320p to full HD (1080p). Larger sizes should work well due to automatic layout. On smartphones you can use the sidebar by sliding with your finger to the right side. + +## Which platforms and browsers do you officially support? + +OS: + +* Windows +* Linux +* Mac + +Browsers: + +* Chrome +* Firefox +* Safari + +The most modern browser is [without question](https://html5test.com/compare/browser/chrome-58/firefox-53/safari-10.2.html) Chrome and I highly recommend everyone to switch to Chrome if you're not using it already. Chrome has WebP support which *drastically* reduces page loading times and also lazy loading support which loads images only when they appear in your current viewport, reducing both your bandwidth and your initial loading times. + +Firefox and Safari are supported but I do not recommend using them. See these for more information: + +* [WebP support](http://caniuse.com/#feat=webp) +* [Push notifications](http://caniuse.com/#feat=push-api) +* [Intersection Observer support](http://caniuse.com/#feat=intersectionobserver) (lazy loading) +* [RequestIdleCallback](http://caniuse.com/#feat=requestidlecallback) (defer unimportant requests to idle times) + ## Can you tell me more about the history of this software? From a technological standpoint we went through quite a few different approaches: @@ -149,9 +208,13 @@ From a technological standpoint we went through quite a few different approaches Since 2014 it's been just me, though I do plan to start a company and hire talented people to help me out with this project once the stars align. +## Is there an API for this site? + +Yes, the [API](https://notify.moe/api) is an on-going effort and subject to change. + ## Can I show my support for this site? Do you accept donations? -I'm planning to add "pro accounts" for an extended feature set. You do not have to donate without getting something back, instead I'd rather be happy to see you profit from the donation as well. It would be my dream to work on this full-time. +I recently added [PRO](https://notify.moe/shop) accounts for an extended feature set. You do not have to donate without getting something back, instead I'd rather be happy to see you profit from the donation as well. It would be my dream to work on this full-time. ## Can I help with coding or change stuff as this is Open Source? From 6e4897f435fff504990e9be4f461f52fab0aac9a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 9 Oct 2017 15:47:40 +0200 Subject: [PATCH 412/527] Refactor --- main.go | 9 ++- mixins/Input.pixy | 29 ++++++-- pages/animelistitem/animelistitem.pixy | 4 +- pages/animelistitem/animelistitem.scarlet | 2 +- pages/dashboard/dashboard.pixy | 44 +++++------ pages/listimport/listimport.pixy | 6 +- pages/newsoundtrack/newsoundtrack.pixy | 16 ++-- pages/newthread/newthread.pixy | 6 +- pages/settings/settings.pixy | 26 +++---- pages/settings/settings.scarlet | 2 +- pages/soundtrack/edit.go | 74 +++++++++++++++++++ pages/soundtrack/soundtrack.go | 2 +- pages/soundtrack/soundtrack.pixy | 7 +- pages/soundtracks/soundtracks.go | 8 +- pages/soundtracks/soundtracks.pixy | 2 +- patches/add-draft-index/add-draft-index.go | 22 ++++++ .../reset-inventories/reset-inventories.go | 2 +- .../update-soundtracks/update-soundtracks.go | 11 +++ scripts/Actions.ts | 9 +++ styles/tags.scarlet | 24 ++++++ styles/widgets.scarlet | 6 +- 21 files changed, 236 insertions(+), 75 deletions(-) create mode 100644 pages/soundtrack/edit.go create mode 100644 patches/add-draft-index/add-draft-index.go create mode 100644 patches/update-soundtracks/update-soundtracks.go create mode 100644 styles/tags.scarlet diff --git a/main.go b/main.go index 94aee30f..ff6132eb 100644 --- a/main.go +++ b/main.go @@ -83,12 +83,9 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/forum/:tag", forum.Get) app.Ajax("/thread/:id", threads.Get) app.Ajax("/post/:id", posts.Get) - app.Ajax("/soundtrack/:id", soundtrack.Get) app.Ajax("/character/:id", character.Get) app.Ajax("/new/thread", newthread.Get) - app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/settings", settings.Get) - app.Ajax("/soundtracks", soundtracks.Get) app.Ajax("/artworks", artworks.Get) app.Ajax("/amvs", amvs.Get) app.Ajax("/users", users.Active) @@ -99,6 +96,12 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/statistics/anime", statistics.Anime) app.Ajax("/login", login.Get) + // Soundtracks + app.Ajax("/soundtracks", soundtracks.Get) + app.Ajax("/new/soundtrack", newsoundtrack.Get) + app.Ajax("/soundtrack/:id", soundtrack.Get) + app.Ajax("/soundtrack/:id/edit", soundtrack.Edit) + // User profiles app.Ajax("/user", user.Get) app.Ajax("/user/:nick", profile.Get) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index 766b67af..9a9d6ba2 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -1,19 +1,32 @@ component InputText(id string, value string, label string, placeholder string) - .widget-input + .widget-section label(for=id)= label + ":" - input.widget-element.action(id=id, data-field=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") + input.widget-ui-element.action(id=id, data-field=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") component InputTextArea(id string, value string, label string, placeholder string) - .widget-input + .widget-section label(for=id)= label + ":" - textarea.widget-element.action(id=id, data-field=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")= value + textarea.widget-ui-element.action(id=id, data-field=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")= value component InputNumber(id string, value float64, label string, placeholder string, min string, max string, step string) - .widget-input + .widget-section label(for=id)= label + ":" - input.widget-element.action(id=id, data-field=id, type="number", value=value, min=min, max=max, step=step, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") + input.widget-ui-element.action(id=id, data-field=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, values []string) - .widget-input + .widget-section label(for=id)= label + ":" - select.widget-element.action(id=id, data-field=id, value=value, title=placeholder, data-action="save", data-trigger="change") + select.widget-ui-element.action(id=id, data-field=id, value=value, title=placeholder, data-action="save", data-trigger="change") + +component InputTags(id string, value []string, label string) + .widget-section + label(for=id)= label + ":" + .tags(id=id) + each tag in value + .tag + span.tag-title= tag + .tag-remove.action(data-action="removeTag", data-trigger="click", data-tag=tag) x + + button.tag-add + RawIcon("plus") + \ No newline at end of file diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 3b9ced93..02e2ff47 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -7,9 +7,9 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. .anime-list-item-episodes-edit InputNumber("Episodes", float64(item.Episodes), "Episodes", "Number of episodes you watched", "0", arn.EpisodeCountMax(anime.EpisodeCount), "1") - .widget-input.anime-list-item-status-edit + .widget-section.anime-list-item-status-edit label(for="Status") Status: - select.widget-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") + select.widget-ui-element.action(id="Status", data-field="Status", value=item.Status, data-action="save", data-trigger="change") option(value=arn.AnimeListStatusWatching) Watching option(value=arn.AnimeListStatusCompleted) Completed option(value=arn.AnimeListStatusPlanned) Plan to watch diff --git a/pages/animelistitem/animelistitem.scarlet b/pages/animelistitem/animelistitem.scarlet index a515568c..9fa47362 100644 --- a/pages/animelistitem/animelistitem.scarlet +++ b/pages/animelistitem/animelistitem.scarlet @@ -14,5 +14,5 @@ horizontal-wrap justify-content space-between width 100% - .widget-input + .widget-section max-width 20% \ No newline at end of file diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 37753eb9..abf934f4 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -7,16 +7,16 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound for i := 0; i <= 4; i++ if i < len(schedule) - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text a.schedule-item-link.ajax(href=schedule[i].Anime.Link()) Icon("calendar-o") .schedule-item-title= schedule[i].Anime.Title.Canonical .spacer .schedule-item-date.utc-airing-date(data-start-date=schedule[i].Episode.AiringDate.Start, data-end-date=schedule[i].Episode.AiringDate.End, data-episode-number=schedule[i].Episode.Number) else - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text Icon("calendar-o") span ... @@ -24,8 +24,8 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound h3.widget-title Forums each post in posts - a.widget-element.ajax(href=post.Thread().Link()) - .widget-element-text + a.widget-ui-element.ajax(href=post.Thread().Link()) + .widget-ui-element-text Icon(arn.GetForumIcon(post.Thread().Tags[0])) span= post.Thread().Title @@ -33,8 +33,8 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound h3.widget-title Artworks for i := 1; i <= 5; i++ - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text Icon("paint-brush") span ... @@ -43,13 +43,13 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound for i := 0; i <= 4; i++ if i < len(soundTracks) - a.widget-element.ajax(href=soundTracks[i].Link()) - .widget-element-text + a.widget-ui-element.ajax(href=soundTracks[i].Link()) + .widget-ui-element-text Icon("music") span(title=soundTracks[i].Media[0].Title)= soundTracks[i].Anime()[0].Title.Canonical else - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text Icon("music") span ... @@ -57,8 +57,8 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound h3.widget-title AMVs for i := 1; i <= 5; i++ - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text Icon("video-camera") span ... @@ -66,8 +66,8 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound h3.widget-title Reviews for i := 1; i <= 5; i++ - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text Icon("book") span ... @@ -75,8 +75,8 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound h3.widget-title Groups for i := 1; i <= 5; i++ - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text Icon("group") span ... @@ -85,13 +85,13 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound for i := 0; i <= 4; i++ if i < len(following) - a.widget-element.ajax(href="/+" + following[i].Nick) - .widget-element-text + a.widget-ui-element.ajax(href="/+" + following[i].Nick) + .widget-ui-element-text Icon("address-card") span= following[i].Nick else - .widget-element - .widget-element-text + .widget-ui-element + .widget-ui-element-text Icon("address-card") span ... diff --git a/pages/listimport/listimport.pixy b/pages/listimport/listimport.pixy index 17a65ab6..aff1e117 100644 --- a/pages/listimport/listimport.pixy +++ b/pages/listimport/listimport.pixy @@ -2,21 +2,21 @@ component ImportLists(user *arn.User) if user.Accounts.AniList.Nick != "" label AniList: - .widget-input + .widget-section a.button.mountable.ajax(href="/import/anilist/animelist") Icon("download") span Import AniList if user.Accounts.Kitsu.Nick != "" label Kitsu: - .widget-input + .widget-section a.button.mountable.ajax(href="/import/kitsu/animelist") Icon("download") span Import Kitsu if user.Accounts.MyAnimeList.Nick != "" label MyAnimeList: - .widget-input + .widget-section a.button.mountable.ajax(href="/import/myanimelist/animelist") Icon("download") span Import MyAnimeList \ No newline at end of file diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy index 9d29aea0..80dbd8cd 100644 --- a/pages/newsoundtrack/newsoundtrack.pixy +++ b/pages/newsoundtrack/newsoundtrack.pixy @@ -3,21 +3,21 @@ component NewSoundTrack(user *arn.User) .widget h1 New soundtrack - .widget-input + .widget-section label(for="soundcloud-link") Soundcloud link: - input#soundcloud-link.widget-element(type="text", placeholder="https://soundcloud.com/abc/123") + input#soundcloud-link.widget-ui-element(type="text", placeholder="https://soundcloud.com/abc/123") - .widget-input + .widget-section label(for="youtube-link") Youtube link: - input#youtube-link.widget-element(type="text", placeholder="https://www.youtube.com/watch?v=123") + input#youtube-link.widget-ui-element(type="text", placeholder="https://www.youtube.com/watch?v=123") - .widget-input + .widget-section label(for="anime-link") Anime link: - input#anime-link.widget-element(type="text", placeholder="https://notify.moe/anime/123") + input#anime-link.widget-ui-element(type="text", placeholder="https://notify.moe/anime/123") - .widget-input + .widget-section label(for="osu-link") Osu beatmap (optional): - input#osu-link.widget-element(type="text", placeholder="https://osu.ppy.sh/s/123") + input#osu-link.widget-ui-element(type="text", placeholder="https://osu.ppy.sh/s/123") .buttons button.action(data-action="createSoundTrack", data-trigger="click") diff --git a/pages/newthread/newthread.pixy b/pages/newthread/newthread.pixy index 9ddebcbe..cd28f085 100644 --- a/pages/newthread/newthread.pixy +++ b/pages/newthread/newthread.pixy @@ -3,11 +3,11 @@ component NewThread(user *arn.User) .widget-form .widget - input#title.widget-element(type="text", placeholder="Title") + input#title.widget-ui-element(type="text", placeholder="Title") - textarea#text.widget-element(placeholder="Content") + textarea#text.widget-ui-element(placeholder="Content") - select#tag.widget-element(value="general") + select#tag.widget-ui-element(value="general") option(value="general") General option(value="news") News option(value="anime") Anime diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 32bb50f4..1195d8da 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -26,19 +26,19 @@ component Settings(user *arn.User) Icon("bell") span Notifications - #enable-notifications.widget-input + #enable-notifications.widget-section label Enable: button.action(data-action="enableNotifications", data-trigger="click") Icon("toggle-off") span Enable notifications - #disable-notifications.widget-input + #disable-notifications.widget-section label Disable: button.action(data-action="disableNotifications", data-trigger="click") Icon("toggle-on") span Disable notifications - #test-notification.widget-input + #test-notification.widget-section label Test: button.action(data-action="testNotification", data-trigger="click") Icon("paper-plane") @@ -49,7 +49,7 @@ component Settings(user *arn.User) Icon("user-plus") span Connect - .widget-input.social-account + .widget-section.social-account label(for="google") Google: a#google.button.social-account-button(href="/auth/google") @@ -60,7 +60,7 @@ component Settings(user *arn.User) Icon("circle-o") span Not connected - .widget-input.social-account + .widget-section.social-account label(for="facebook") Facebook: a#facebook.button.social-account-button(href="/auth/facebook") @@ -84,7 +84,7 @@ component Settings(user *arn.User) Icon("upload") span Export - .widget-input + .widget-section label JSON: a.button(href="/api/animelist/" + user.ID) Icon("upload") @@ -95,19 +95,19 @@ component Settings(user *arn.User) Icon("puzzle-piece") span Apps - .widget-input + .widget-section label Chrome Extension: button.action(data-action="installExtension", data-trigger="click") Icon("chrome") span Get the Chrome Extension - .widget-input + .widget-section label Desktop App: button.action(data-action="installApp", data-trigger="click") Icon("desktop") span Get the Desktop App - .widget-input + .widget-section label Android App: a.button(href="https://www.youtube.com/watch?v=opyt4cw0ep8", target="_blank", rel="noopener") Icon("android") @@ -118,9 +118,9 @@ component Settings(user *arn.User) Icon("picture-o") span Avatar - .widget-input + .widget-section label(for="Avatar.Source") Source: - select.widget-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change") + select.widget-ui-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change") option(value="") Automatic option(value="Gravatar") Gravatar option(value="URL") Link @@ -143,7 +143,7 @@ component Settings(user *arn.User) span PRO if user.IsPro() - .widget-input + .widget-section label span Your PRO account expires in span.utc-date(data-date=user.ProExpires) @@ -152,7 +152,7 @@ component Settings(user *arn.User) Icon("star") span Extend PRO account duration else - .widget-input + .widget-section label Would you like to support the site development? a.button.ajax(href="/shop") Icon("star") diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet index 85f1c180..cb0cb0ca 100644 --- a/pages/settings/settings.scarlet +++ b/pages/settings/settings.scarlet @@ -1,4 +1,4 @@ -.widget-input +.widget-section button, .button margin-bottom 1rem diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go new file mode 100644 index 00000000..9571056e --- /dev/null +++ b/pages/soundtrack/edit.go @@ -0,0 +1,74 @@ +package soundtrack + +import ( + "bytes" + "net/http" + "reflect" + "strings" + + "github.com/animenotifier/notify.moe/components" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" +) + +// Edit track. +func Edit(ctx *aero.Context) string { + id := ctx.Get("id") + track, err := arn.GetSoundTrack(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Track not found", err) + } + + ctx.Data = &arn.OpenGraph{ + Tags: map[string]string{ + "og:title": track.Media[0].Title, + "og:image": track.MainAnime().Image.Large, + "og:url": "https://" + ctx.App.Config.Domain + track.Link(), + "og:site_name": "notify.moe", + "og:type": "music.song", + }, + } + + return ctx.HTML(EditForm(track, "Edit soundtrack")) +} + +// EditForm ... +func EditForm(obj interface{}, title string) string { + t := reflect.TypeOf(obj).Elem() + v := reflect.ValueOf(obj).Elem() + lowerCaseTypeName := strings.ToLower(t.Name()) + id := reflect.Indirect(v.FieldByName("ID")) + + var b bytes.Buffer + b.WriteString(`
    `) + b.WriteString(`
    `) + b.WriteString(`

    `) + b.WriteString(title) + b.WriteString(`

    `) + + // Fields + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if field.Anonymous || field.Tag.Get("editable") != "true" { + continue + } + + fieldValue := reflect.Indirect(v.FieldByName(field.Name)) + + switch field.Type.String() { + case "string": + b.WriteString(components.InputText(field.Name, fieldValue.String(), field.Name, "")) + case "[]string": + b.WriteString(components.InputTags(field.Name, fieldValue.Interface().([]string), field.Name)) + default: + panic("No edit form implementation for " + field.Name + " with type " + field.Type.String()) + } + } + + b.WriteString("
    ") + b.WriteString("
    ") + return b.String() +} diff --git a/pages/soundtrack/soundtrack.go b/pages/soundtrack/soundtrack.go index f1f13266..86adaa19 100644 --- a/pages/soundtrack/soundtrack.go +++ b/pages/soundtrack/soundtrack.go @@ -20,7 +20,7 @@ func Get(ctx *aero.Context) string { ctx.Data = &arn.OpenGraph{ Tags: map[string]string{ "og:title": track.Media[0].Title, - "og:image": track.Anime()[0].Image.Large, + "og:image": track.MainAnime().Image.Large, "og:url": "https://" + ctx.App.Config.Domain + track.Link(), "og:site_name": "notify.moe", "og:type": "music.song", diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index 37e9237c..99bf4654 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -1,5 +1,8 @@ component Track(track *arn.SoundTrack) - h1= track.Media[0].Title + h1= track.Title .sound-tracks - SoundTrackAllMedia(track) \ No newline at end of file + SoundTrackAllMedia(track) + + p + a.ajax(href=track.Link() + "/edit") Edit \ No newline at end of file diff --git a/pages/soundtracks/soundtracks.go b/pages/soundtracks/soundtracks.go index 1d1f6c3b..93c8c7f6 100644 --- a/pages/soundtracks/soundtracks.go +++ b/pages/soundtracks/soundtracks.go @@ -10,9 +10,11 @@ import ( const maxTracks = 9 -// Get renders the music page. +// Get renders the soundtracks page. func Get(ctx *aero.Context) string { - tracks, err := arn.AllSoundTracks() + tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + return !track.IsDraft + }) if err != nil { return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) @@ -24,5 +26,5 @@ func Get(ctx *aero.Context) string { tracks = tracks[:maxTracks] } - return ctx.HTML(components.Music(tracks)) + return ctx.HTML(components.SoundTracks(tracks)) } diff --git a/pages/soundtracks/soundtracks.pixy b/pages/soundtracks/soundtracks.pixy index fb81d56d..82bbdf01 100644 --- a/pages/soundtracks/soundtracks.pixy +++ b/pages/soundtracks/soundtracks.pixy @@ -1,4 +1,4 @@ -component Music(tracks []*arn.SoundTrack) +component SoundTracks(tracks []*arn.SoundTrack) h1 Soundtracks .music-buttons diff --git a/patches/add-draft-index/add-draft-index.go b/patches/add-draft-index/add-draft-index.go new file mode 100644 index 00000000..260f4810 --- /dev/null +++ b/patches/add-draft-index/add-draft-index.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Addind draft indices") + + // Iterate over the stream + for user := range arn.MustStreamUsers() { + fmt.Println(user.Nick) + + draftIndex := arn.NewDraftIndex(user.ID) + arn.PanicOnError(draftIndex.Save()) + } + + color.Green("Finished.") +} diff --git a/patches/reset-inventories/reset-inventories.go b/patches/reset-inventories/reset-inventories.go index 6ab25c83..5e4e8149 100644 --- a/patches/reset-inventories/reset-inventories.go +++ b/patches/reset-inventories/reset-inventories.go @@ -34,7 +34,7 @@ func main() { fmt.Println(user.Nick) inventory := arn.NewInventory(user.ID) - err = arn.DB.Set("Inventory", inventory.UserID, inventory) + err = inventory.Save() if err != nil { color.Red(err.Error()) diff --git a/patches/update-soundtracks/update-soundtracks.go b/patches/update-soundtracks/update-soundtracks.go new file mode 100644 index 00000000..833156c2 --- /dev/null +++ b/patches/update-soundtracks/update-soundtracks.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + for track := range arn.MustStreamSoundTracks() { + arn.PanicOnError(track.Save()) + } +} diff --git a/scripts/Actions.ts b/scripts/Actions.ts index bcba55c4..01f413fa 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -365,6 +365,15 @@ export function buyItem(arn: AnimeNotifier, button: HTMLElement) { .then(() => arn.loading(false)) } +// Remove tag +export function removeTag(arn: AnimeNotifier, element: HTMLElement) { + let tag = element.dataset.tag + + // arn.loading(true) + + alert("Remove " + tag) +} + // Chrome extension installation export function installExtension(arn: AnimeNotifier, button: HTMLElement) { let browser: any = window["chrome"] diff --git a/styles/tags.scarlet b/styles/tags.scarlet new file mode 100644 index 00000000..45366e80 --- /dev/null +++ b/styles/tags.scarlet @@ -0,0 +1,24 @@ +.tags + horizontal-wrap + +.tag + ui-element + padding 0.4rem 0.8rem + margin 0.4rem + +.tag-input + horizontal + + button + margin-left 0.8rem + +.tag-remove + display inline-block + margin-left 0.4rem + opacity 0.5 + + :hover + cursor pointer + +.tag-add + margin 0.4rem !important \ No newline at end of file diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index f60c3534..f21dc066 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -20,7 +20,7 @@ margin-bottom 1rem overflow hidden -.widget-element +.widget-ui-element vertical-wrap ui-element transition border transition-speed ease, background transition-speed ease, transform transition-speed ease, transform color ease @@ -29,14 +29,14 @@ width 100% // max-width 700px -.widget-element-text +.widget-ui-element-text horizontal clip-long-text justify-content flex-start align-items center width 100% -.widget-input +.widget-section vertical width 100% From 6d8700e166ec46079c434dc8486b33020c3853ce Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 9 Oct 2017 16:23:18 +0200 Subject: [PATCH 413/527] Minor updates --- pages/dashboard/dashboard.pixy | 2 +- pages/soundtrack/edit.go | 6 +++++- pages/soundtrack/soundtrack.go | 2 +- sw/service-worker.ts | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index abf934f4..d2494772 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -46,7 +46,7 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound a.widget-ui-element.ajax(href=soundTracks[i].Link()) .widget-ui-element-text Icon("music") - span(title=soundTracks[i].Media[0].Title)= soundTracks[i].Anime()[0].Title.Canonical + span(title=soundTracks[i].Title)= soundTracks[i].MainAnime().Title.Canonical else .widget-ui-element .widget-ui-element-text diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index 9571056e..e6f8929e 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -23,7 +23,7 @@ func Edit(ctx *aero.Context) string { ctx.Data = &arn.OpenGraph{ Tags: map[string]string{ - "og:title": track.Media[0].Title, + "og:title": track.Title, "og:image": track.MainAnime().Image.Large, "og:url": "https://" + ctx.App.Config.Domain + track.Link(), "og:site_name": "notify.moe", @@ -63,6 +63,10 @@ func EditForm(obj interface{}, title string) string { b.WriteString(components.InputText(field.Name, fieldValue.String(), field.Name, "")) case "[]string": b.WriteString(components.InputTags(field.Name, fieldValue.Interface().([]string), field.Name)) + case "[]*arn.ExternalMedia": + for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { + b.WriteString(EditForm(fieldValue.Index(sliceIndex).Interface(), "External Media")) + } default: panic("No edit form implementation for " + field.Name + " with type " + field.Type.String()) } diff --git a/pages/soundtrack/soundtrack.go b/pages/soundtrack/soundtrack.go index 86adaa19..afc492c8 100644 --- a/pages/soundtrack/soundtrack.go +++ b/pages/soundtrack/soundtrack.go @@ -19,7 +19,7 @@ func Get(ctx *aero.Context) string { ctx.Data = &arn.OpenGraph{ Tags: map[string]string{ - "og:title": track.Media[0].Title, + "og:title": track.Title, "og:image": track.MainAnime().Image.Large, "og:url": "https://" + ctx.App.Config.Domain + track.Link(), "og:site_name": "notify.moe", diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 6f800784..f48c7eca 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -1,6 +1,6 @@ // pack:ignore -const CACHE = "v-1" +const CACHE = "v-2" const RELOADS = new Map>() const ETAGS = new Map() const CACHEREFRESH = new Map>() From 5c3f3dab1b53f66e04a7f31e7b0c57e5b5839d11 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 10 Oct 2017 11:38:31 +0200 Subject: [PATCH 414/527] Improved EditForm --- pages/soundtrack/edit.go | 60 +++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index e6f8929e..2277905d 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -2,6 +2,7 @@ package soundtrack import ( "bytes" + "fmt" "net/http" "reflect" "strings" @@ -38,8 +39,8 @@ func Edit(ctx *aero.Context) string { func EditForm(obj interface{}, title string) string { t := reflect.TypeOf(obj).Elem() v := reflect.ValueOf(obj).Elem() - lowerCaseTypeName := strings.ToLower(t.Name()) id := reflect.Indirect(v.FieldByName("ID")) + lowerCaseTypeName := strings.ToLower(t.Name()) var b bytes.Buffer b.WriteString(`
    `) @@ -48,31 +49,46 @@ func EditForm(obj interface{}, title string) string { b.WriteString(title) b.WriteString(`
  • `) + RenderObject(&b, obj, "") + + b.WriteString("") + b.WriteString("") + + return b.String() +} + +// RenderObject ... +func RenderObject(b *bytes.Buffer, obj interface{}, idPrefix string) { + t := reflect.TypeOf(obj).Elem() + v := reflect.ValueOf(obj).Elem() + // Fields for i := 0; i < t.NumField(); i++ { field := t.Field(i) + RenderField(b, &v, field, idPrefix) + } +} - if field.Anonymous || field.Tag.Get("editable") != "true" { - continue - } - - fieldValue := reflect.Indirect(v.FieldByName(field.Name)) - - switch field.Type.String() { - case "string": - b.WriteString(components.InputText(field.Name, fieldValue.String(), field.Name, "")) - case "[]string": - b.WriteString(components.InputTags(field.Name, fieldValue.Interface().([]string), field.Name)) - case "[]*arn.ExternalMedia": - for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { - b.WriteString(EditForm(fieldValue.Index(sliceIndex).Interface(), "External Media")) - } - default: - panic("No edit form implementation for " + field.Name + " with type " + field.Type.String()) - } +// RenderField ... +func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, idPrefix string) { + if field.Anonymous || field.Tag.Get("editable") != "true" { + return } - b.WriteString("") - b.WriteString("") - return b.String() + fieldValue := reflect.Indirect(v.FieldByName(field.Name)) + + switch field.Type.String() { + case "string": + b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, "")) + case "[]string": + b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name)) + case "[]*arn.ExternalMedia": + for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { + arrayObj := fieldValue.Index(sliceIndex).Interface() + arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex) + RenderObject(b, arrayObj, arrayIDPrefix) + } + default: + panic("No edit form implementation for " + idPrefix + field.Name + " with type " + field.Type.String()) + } } From 46a3715bae6e892f02cf585219e64f043a56c951 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 10 Oct 2017 12:14:52 +0200 Subject: [PATCH 415/527] Add media button --- pages/soundtrack/edit.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index 2277905d..50aa2b5d 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -88,6 +89,8 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex) RenderObject(b, arrayObj, arrayIDPrefix) } + + b.WriteString(`
    `) default: panic("No edit form implementation for " + idPrefix + field.Name + " with type " + field.Type.String()) } From 789265736290c3ca5a6cf8f9518c2b4663f9bc53 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 11 Oct 2017 10:37:33 +0200 Subject: [PATCH 416/527] Refactor --- jobs/jobs.go | 29 +++++++++---------- .../refresh-track-titles.go | 16 +++++----- main_test.go | 8 ++--- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/jobs/jobs.go b/jobs/jobs.go index 416a5aba..d05f663e 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -23,21 +23,20 @@ var colorPool = []*color.Color{ } var jobs = map[string]time.Duration{ - "forum-activity": 1 * time.Minute, - "active-users": 5 * time.Minute, - "anime-ratings": 10 * time.Minute, - "airing-anime": 10 * time.Minute, - "statistics": 15 * time.Minute, - "popular-anime": 20 * time.Minute, - "avatars": 1 * time.Hour, - "test": 1 * time.Hour, - "twist": 2 * time.Hour, - "search-index": 2 * time.Hour, - "refresh-episodes": 10 * time.Hour, - "refresh-track-titles": 10 * time.Hour, - "refresh-osu": 12 * time.Hour, - "sync-anime": 12 * time.Hour, - "sync-shoboi": 24 * time.Hour, + "forum-activity": 1 * time.Minute, + "active-users": 5 * time.Minute, + "anime-ratings": 10 * time.Minute, + "airing-anime": 10 * time.Minute, + "statistics": 15 * time.Minute, + "popular-anime": 20 * time.Minute, + "avatars": 1 * time.Hour, + "test": 1 * time.Hour, + "twist": 2 * time.Hour, + "search-index": 2 * time.Hour, + "refresh-episodes": 10 * time.Hour, + "refresh-osu": 12 * time.Hour, + "sync-anime": 12 * time.Hour, + "sync-shoboi": 24 * time.Hour, } func main() { diff --git a/jobs/refresh-track-titles/refresh-track-titles.go b/jobs/refresh-track-titles/refresh-track-titles.go index 7bd9dd15..e6ed3d7f 100644 --- a/jobs/refresh-track-titles/refresh-track-titles.go +++ b/jobs/refresh-track-titles/refresh-track-titles.go @@ -24,14 +24,14 @@ func main() { } func sync(track *arn.SoundTrack) { - for _, media := range track.Media { - media.RefreshMetaData() - println(media.Service, media.Title) - } + // for _, media := range track.Media { + // media.RefreshMetaData() + // println(media.Service, media.Title) + // } - err := track.Save() + // err := track.Save() - if err != nil { - panic(err) - } + // if err != nil { + // panic(err) + // } } diff --git a/main_test.go b/main_test.go index 80f99509..b5931ec8 100644 --- a/main_test.go +++ b/main_test.go @@ -37,23 +37,23 @@ func TestRoutes(t *testing.T) { func TestInterfaceImplementations(t *testing.T) { // API interfaces var creatable = reflect.TypeOf((*api.Creatable)(nil)).Elem() - var updatable = reflect.TypeOf((*api.Updatable)(nil)).Elem() + var editable = reflect.TypeOf((*api.Editable)(nil)).Elem() var actionable = reflect.TypeOf((*api.Actionable)(nil)).Elem() var collection = reflect.TypeOf((*api.Collection)(nil)).Elem() // Required interface implementations var interfaceImplementations = map[string][]reflect.Type{ "User": []reflect.Type{ - updatable, + editable, }, "Thread": []reflect.Type{ creatable, - updatable, + editable, actionable, }, "Post": []reflect.Type{ creatable, - updatable, + editable, actionable, }, "SoundTrack": []reflect.Type{ From 36cceb3c36e274b08dbaae672df26323da67cf25 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 11 Oct 2017 12:45:04 +0200 Subject: [PATCH 417/527] Cleanup --- main_test.go | 1 + tests.go | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/main_test.go b/main_test.go index b5931ec8..a20c3ca3 100644 --- a/main_test.go +++ b/main_test.go @@ -58,6 +58,7 @@ func TestInterfaceImplementations(t *testing.T) { }, "SoundTrack": []reflect.Type{ creatable, + editable, }, "Analytics": []reflect.Type{ creatable, diff --git a/tests.go b/tests.go index 0b226fd3..29875659 100644 --- a/tests.go +++ b/tests.go @@ -214,10 +214,6 @@ var routeTests = map[string][]string{ "/_/search/dragon", }, - "/dark-flame-master": []string{ - "/dark-flame-master", - }, - // Disable these tests because they require authorization "/auth/google": nil, "/auth/google/callback": nil, @@ -245,6 +241,7 @@ var routeTests = map[string][]string{ "/admin/purchases": nil, "/editor/anilist": nil, "/editor/shoboi": nil, + "/dark-flame-master": nil, "/user": nil, "/settings": nil, "/shop": nil, From b7f2ee786f465131f7d984b048af4a81940d7def Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 11 Oct 2017 15:49:16 +0200 Subject: [PATCH 418/527] Refactor --- pages/editanime/editanime.pixy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/editanime/editanime.pixy b/pages/editanime/editanime.pixy index b40e541c..2ad96b86 100644 --- a/pages/editanime/editanime.pixy +++ b/pages/editanime/editanime.pixy @@ -4,8 +4,8 @@ component EditAnime(anime *arn.Anime) .widget-form.mountable .widget(data-api="/api/anime/" + anime.ID) h3.widget-title Mappings - InputText("Custom:ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on cal.syoboi.jp") - InputText("Custom:AniListID", anime.GetMapping("anilist/anime"), "AniList ID", "ID on anilist.co") + InputText("Virtual:ShoboiID", anime.GetMapping("shoboi/anime"), "Shoboi TID", "TID on cal.syoboi.jp") + InputText("Virtual:AniListID", anime.GetMapping("anilist/anime"), "AniList ID", "ID on anilist.co") .buttons a.button.ajax(href="/anime/" + anime.ID) From 77d458eb13c88d3a83e3fab3cc32cb72e1f96bd7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 11 Oct 2017 22:54:26 +0200 Subject: [PATCH 419/527] Refactor --- pages/animelist/animelist.pixy | 2 +- pages/animelistitem/animelistitem.pixy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index 0ec5615b..b0aea99a 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -42,7 +42,7 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User table.anime-list tbody each item in animeList.Items - tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/update/" + item.AnimeID) + tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/field/Items[AnimeID=\"" + item.AnimeID + "\"]") td.anime-list-item-image-container a.ajax(href=item.Anime().Link()) img.anime-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 02e2ff47..078b5bd6 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -1,6 +1,6 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.Anime) .widget-form.mountable - .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 + "/field/Items[AnimeID=\"" + anime.ID + "\"]") h1= anime.Title.Canonical .anime-list-item-progress-edit From 6d0b2ccdf6fd6d2aa3e737d193decd6dc6b57d2e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 12 Oct 2017 12:07:17 +0200 Subject: [PATCH 420/527] Added array operations --- mixins/Input.pixy | 6 +++--- pages/soundtrack/edit.go | 8 +++++++- scripts/Actions.ts | 25 +++++++++++++++++++------ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index 9a9d6ba2..10511072 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -22,11 +22,11 @@ component InputTags(id string, value []string, label string) .widget-section label(for=id)= label + ":" .tags(id=id) - each tag in value + for index, tag := range value .tag span.tag-title= tag - .tag-remove.action(data-action="removeTag", data-trigger="click", data-tag=tag) x + .tag-remove.action(data-action="arrayRemove", data-trigger="click", data-field=id, data-index=index) x - button.tag-add + button.tag-add.action(data-action="arrayAppend", data-trigger="click", data-field=id) RawIcon("plus") \ No newline at end of file diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index 50aa2b5d..4b1bcd24 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "reflect" + "strconv" "strings" "github.com/animenotifier/notify.moe/components" @@ -88,9 +89,14 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i arrayObj := fieldValue.Index(sliceIndex).Interface() arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex) RenderObject(b, arrayObj, arrayIDPrefix) + + // Remove button + b.WriteString(`
    `) } - b.WriteString(`
    `) + b.WriteString(`
    `) default: panic("No edit form implementation for " + idPrefix + field.Name + " with type " + field.Type.String()) } diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 01f413fa..8ee8c49b 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -365,13 +365,26 @@ export function buyItem(arn: AnimeNotifier, button: HTMLElement) { .then(() => arn.loading(false)) } -// Remove tag -export function removeTag(arn: AnimeNotifier, element: HTMLElement) { - let tag = element.dataset.tag - - // arn.loading(true) +// Append new element to array +export function arrayAppend(arn: AnimeNotifier, element: HTMLElement) { + let field = element.dataset.field + let object = element.dataset.object ? JSON.parse(element.dataset.object) : {} + let apiEndpoint = arn.findAPIEndpoint(element) - alert("Remove " + tag) + arn.post(apiEndpoint + "/field/" + field + "/append", object) + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// Remove element from array +export function arrayRemove(arn: AnimeNotifier, element: HTMLElement) { + let field = element.dataset.field + let index = element.dataset.index + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint + "/field/" + field + "/remove/" + index, "") + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) } // Chrome extension installation From 37a9e6cbf4e728d47c6876a778645d8432042d0d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 12 Oct 2017 17:52:46 +0200 Subject: [PATCH 421/527] Improved soundtrack editing --- mixins/Input.pixy | 5 +- mixins/SoundTrack.pixy | 28 +++++++---- pages/settings/settings.scarlet | 7 ++- pages/soundtrack/edit.go | 18 +++++-- pages/soundtrack/soundtrack.pixy | 29 ++++++++++-- scripts/AnimeNotifier.ts | 80 +++++++++++++++++++++----------- styles/images.scarlet | 4 +- styles/include/config.scarlet | 2 +- styles/tags.scarlet | 30 ++++++------ styles/video.scarlet | 5 +- sw/service-worker.ts | 2 +- 11 files changed, 142 insertions(+), 68 deletions(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index 10511072..2c0b93d1 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -24,8 +24,9 @@ component InputTags(id string, value []string, label string) .tags(id=id) for index, tag := range value .tag - span.tag-title= tag - .tag-remove.action(data-action="arrayRemove", data-trigger="click", data-field=id, data-index=index) x + span.tag-title.action(contenteditable="true", data-trigger="focusout", data-action="save", data-field=id + "[" + strconv.Itoa(index) + "]")= tag + button.tag-remove.action(data-action="arrayRemove", data-trigger="click", data-field=id, data-index=index) + RawIcon("trash") button.tag-add.action(data-action="arrayAppend", data-trigger="click", data-field=id) RawIcon("plus") diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index 41931775..b4748642 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -7,13 +7,25 @@ component SoundTrackAllMedia(track *arn.SoundTrack) component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia) .sound-track.mountable(id=track.ID) - .sound-track-content + SoundTrackContent(track, media) + SoundTrackFooter(track) + +component SoundTrackContent(track *arn.SoundTrack, media *arn.ExternalMedia) + .sound-track-content + if track.MainAnime() != nil a.sound-track-anime-link.ajax(href="/anime/" + track.MainAnime().ID) img.sound-track-anime-image.lazy(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical) - - iframe.lazy(data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") - .sound-track-footer - a.ajax(href=track.Link()) - Icon("music") - span posted by - a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick + " " \ No newline at end of file + + ExternalMedia(media) + +component SoundTrackFooter(track *arn.SoundTrack) + .sound-track-footer + if track.Title == "" + a.ajax(href=track.Link() + "/edit") untitled + else + a.ajax(href=track.Link())= track.Title + span posted by + a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick + " " + +component ExternalMedia(media *arn.ExternalMedia) + iframe.lazy(data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") \ No newline at end of file diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet index cb0cb0ca..5edda9ce 100644 --- a/pages/settings/settings.scarlet +++ b/pages/settings/settings.scarlet @@ -1,7 +1,6 @@ -.widget-section - button, - .button - margin-bottom 1rem +.widget-section > button, +.widget-section > .button + margin-bottom 1rem .avatar-preview margin 0 auto \ No newline at end of file diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index 4b1bcd24..d15a8fac 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -27,14 +27,17 @@ func Edit(ctx *aero.Context) string { ctx.Data = &arn.OpenGraph{ Tags: map[string]string{ "og:title": track.Title, - "og:image": track.MainAnime().Image.Large, "og:url": "https://" + ctx.App.Config.Domain + track.Link(), "og:site_name": "notify.moe", "og:type": "music.song", }, } - return ctx.HTML(EditForm(track, "Edit soundtrack")) + if track.MainAnime() != nil { + ctx.Data.(*arn.OpenGraph).Tags["og:image"] = track.MainAnime().Image.Large + } + + return ctx.HTML(components.SoundTrackTabs(track) + EditForm(track, "Edit soundtrack")) } // EditForm ... @@ -43,10 +46,11 @@ func EditForm(obj interface{}, title string) string { v := reflect.ValueOf(obj).Elem() id := reflect.Indirect(v.FieldByName("ID")) lowerCaseTypeName := strings.ToLower(t.Name()) + endpoint := `/api/` + lowerCaseTypeName + `/` + id.String() var b bytes.Buffer b.WriteString(`
    `) - b.WriteString(`
    `) + b.WriteString(`
    `) b.WriteString(`

    `) b.WriteString(title) b.WriteString(`

    `) @@ -86,14 +90,22 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name)) case "[]*arn.ExternalMedia": for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { + b.WriteString(`
    `) + b.WriteString(`
    ` + strconv.Itoa(sliceIndex+1) + ". " + field.Name + `
    `) + arrayObj := fieldValue.Index(sliceIndex).Interface() arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex) RenderObject(b, arrayObj, arrayIDPrefix) + // Preview + b.WriteString(components.ExternalMedia(fieldValue.Index(sliceIndex).Interface().(*arn.ExternalMedia))) + // Remove button b.WriteString(`
    `) + + b.WriteString(`
    `) } b.WriteString(`
    `) diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index 99bf4654..995a693c 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -1,8 +1,29 @@ component Track(track *arn.SoundTrack) - h1= track.Title + SoundTrackTabs(track) + + if track.Title == "" + h1 untitled + else + h1= track.Title .sound-tracks SoundTrackAllMedia(track) - - p - a.ajax(href=track.Link() + "/edit") Edit \ No newline at end of file + + .footer.text-center + if track.EditedBy != "" + span Edited + span.utc-date(data-date=track.Edited) + span by + span= track.EditedByUser().Nick + else + span Posted + span.utc-date(data-date=track.Created) + span by + span= track.CreatedByUser().Nick + + span . + +component SoundTrackTabs(track *arn.SoundTrack) + .tabs + Tab("Soundtrack", "music", track.Link()) + Tab("Edit", "pencil", track.Link() + "/edit") \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e8641956..d65d6db4 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -25,8 +25,8 @@ export class AnimeNotifier { mainPageLoaded: boolean lastReloadContentPath: string - imageFound: MutationQueue - imageNotFound: MutationQueue + elementFound: MutationQueue + elementNotFound: MutationQueue unmount: MutationQueue constructor(app: Application) { @@ -34,13 +34,13 @@ export class AnimeNotifier { this.user = null this.title = "Anime Notifier" - this.imageFound = new MutationQueue(elem => elem.classList.add("image-found")) - this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found")) + this.elementFound = new MutationQueue(elem => elem.classList.add("element-found")) + this.elementNotFound = new MutationQueue(elem => elem.classList.add("element-not-found")) this.unmount = new MutationQueue(elem => elem.classList.remove("mounted")) // These classes will never be removed on DOM diffs Diff.persistentClasses.add("mounted") - Diff.persistentClasses.add("image-found") + Diff.persistentClasses.add("element-found") // Never remove src property on diffs Diff.persistentAttributes.add("src") @@ -134,7 +134,7 @@ export class AnimeNotifier { this.contentLoadedActions = Promise.all([ Promise.resolve().then(() => this.mountMountables()), - Promise.resolve().then(() => this.lazyLoadImages()), + Promise.resolve().then(() => this.lazyLoad()), Promise.resolve().then(() => this.displayLocalDates()), Promise.resolve().then(() => this.setSelectBoxValue()), Promise.resolve().then(() => this.assignActions()), @@ -500,37 +500,59 @@ export class AnimeNotifier { } } - lazyLoadImages() { + lazyLoad() { for(let element of findAll("lazy")) { - this.lazyLoadImage(element as HTMLImageElement) + switch(element.tagName) { + case "IMG": + this.lazyLoadImage(element as HTMLImageElement) + break + + case "IFRAME": + this.lazyLoadIFrame(element as HTMLIFrameElement) + break + } } } - lazyLoadImage(img: HTMLImageElement) { + lazyLoadImage(element: HTMLImageElement) { // Once the image becomes visible, load it - img["became visible"] = () => { + element["became visible"] = () => { // Replace URL with WebP if supported - if(this.webpEnabled && img.dataset.webp) { - let dot = img.dataset.src.lastIndexOf(".") - img.src = img.dataset.src.substring(0, dot) + ".webp" + if(this.webpEnabled && element.dataset.webp) { + let dot = element.dataset.src.lastIndexOf(".") + element.src = element.dataset.src.substring(0, dot) + ".webp" } else { - img.src = img.dataset.src + element.src = element.dataset.src } - if(img.naturalWidth === 0) { - img.onload = () => { - this.imageFound.queue(img) + if(element.naturalWidth === 0) { + element.onload = () => { + this.elementFound.queue(element) } - img.onerror = () => { - this.imageNotFound.queue(img) + element.onerror = () => { + this.elementNotFound.queue(element) } } else { - this.imageFound.queue(img) + this.elementFound.queue(element) } } - this.visibilityObserver.observe(img) + this.visibilityObserver.observe(element) + } + + lazyLoadIFrame(element: HTMLIFrameElement) { + // Once the iframe becomes visible, load it + element["became visible"] = () => { + // If the source is already set correctly, don't set it again to avoid iframe flickering. + if(element.src !== element.dataset.src) { + element.src = element.dataset.src + } + + this.elementFound.queue(element) + } + + this.visibilityObserver.observe(element) } mountMountables() { @@ -752,14 +774,18 @@ export class AnimeNotifier { return } - // Disallow Enter key in contenteditables - if(activeElement.getAttribute("contenteditable") === "true" && e.keyCode == 13) { - if("blur" in activeElement) { - activeElement["blur"]() + // Ignore hotkeys on contentEditable elements + if(activeElement.getAttribute("contenteditable") === "true") { + // Disallow Enter key in contenteditables and make it blur the element instead + if(e.keyCode == 13) { + if("blur" in activeElement) { + activeElement["blur"]() + } + + e.preventDefault() + e.stopPropagation() } - e.preventDefault() - e.stopPropagation() return } diff --git a/styles/images.scarlet b/styles/images.scarlet index bf3ea806..8bc0fc41 100644 --- a/styles/images.scarlet +++ b/styles/images.scarlet @@ -2,10 +2,10 @@ visibility hidden opacity 0 -.image-found +.element-found visibility visible opacity 1 !important -.image-not-found +.element-not-found visibility hidden opacity 0 !important \ No newline at end of file diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 7636e3bb..0da623d1 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -62,8 +62,8 @@ content-padding = 1.6rem content-padding-top = 1.6rem content-line-height = 1.7em hover-line-size = 3px -nav-height = 3.11rem typography-margin = 0.4rem +// nav-height = 3.11rem // Timings fade-speed = 250ms diff --git a/styles/tags.scarlet b/styles/tags.scarlet index 45366e80..7c0638a1 100644 --- a/styles/tags.scarlet +++ b/styles/tags.scarlet @@ -1,24 +1,24 @@ +mixin tag-dimensions + padding 0.4rem 0.8rem + margin 0.4rem + height 40px + .tags horizontal-wrap .tag ui-element - padding 0.4rem 0.8rem - margin 0.4rem - -.tag-input - horizontal - - button - margin-left 0.8rem + tag-dimensions + margin-right 0 + border-right none + border-top-right-radius 0 + border-bottom-right-radius 0 .tag-remove - display inline-block - margin-left 0.4rem - opacity 0.5 - - :hover - cursor pointer + tag-dimensions + margin-left 0 + border-top-left-radius 0 + border-bottom-left-radius 0 .tag-add - margin 0.4rem !important \ No newline at end of file + tag-dimensions \ No newline at end of file diff --git a/styles/video.scarlet b/styles/video.scarlet index b6070365..3e84a471 100644 --- a/styles/video.scarlet +++ b/styles/video.scarlet @@ -1,6 +1,9 @@ +iframe + min-height 200px + .video-container width 100% .video width 100% - height calc(100vh - nav-height) \ No newline at end of file + height 100vh \ No newline at end of file diff --git a/sw/service-worker.ts b/sw/service-worker.ts index f48c7eca..456a6645 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -1,6 +1,6 @@ // pack:ignore -const CACHE = "v-2" +const CACHE = "v-3" const RELOADS = new Map>() const ETAGS = new Map() const CACHEREFRESH = new Map>() From 0021ff17dfc4c65073564adada829ab94003958a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 13 Oct 2017 00:44:19 +0200 Subject: [PATCH 422/527] Mostly cosmetic changes --- pages/soundtrack/edit.go | 2 ++ pages/soundtrack/soundtrack.pixy | 45 +++++++++++++++------------ pages/soundtrack/soundtrack.scarlet | 6 ++++ pages/soundtracks/soundtracks.scarlet | 1 - styles/widgets.scarlet | 22 ++++++------- 5 files changed, 44 insertions(+), 32 deletions(-) create mode 100644 pages/soundtrack/soundtrack.scarlet diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index d15a8fac..0246fa0d 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -49,8 +49,10 @@ func EditForm(obj interface{}, title string) string { endpoint := `/api/` + lowerCaseTypeName + `/` + id.String() var b bytes.Buffer + b.WriteString(`
    `) b.WriteString(`
    `) + b.WriteString(`

    `) b.WriteString(title) b.WriteString(`

    `) diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index 995a693c..6c7a57dc 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -1,27 +1,32 @@ component Track(track *arn.SoundTrack) SoundTrackTabs(track) - if track.Title == "" - h1 untitled - else - h1= track.Title - - .sound-tracks - SoundTrackAllMedia(track) - - .footer.text-center - if track.EditedBy != "" - span Edited - span.utc-date(data-date=track.Edited) - span by - span= track.EditedByUser().Nick + .sound-track-full-page + if track.Title == "" + h1.mountable untitled else - span Posted - span.utc-date(data-date=track.Created) - span by - span= track.CreatedByUser().Nick - - span . + h1.mountable= track.Title + + .widget-form.sound-track-media-list + each media in track.Media + .widget.mountable + h3.widget-title= media.Service + .sound-track-media + ExternalMedia(media) + + .footer.text-center.mountable + if track.EditedBy != "" + span Edited + span.utc-date(data-date=track.Edited) + span by + span= track.EditedByUser().Nick + else + span Posted + span.utc-date(data-date=track.Created) + span by + span= track.CreatedByUser().Nick + + span . component SoundTrackTabs(track *arn.SoundTrack) .tabs diff --git a/pages/soundtrack/soundtrack.scarlet b/pages/soundtrack/soundtrack.scarlet new file mode 100644 index 00000000..b82c9ba3 --- /dev/null +++ b/pages/soundtrack/soundtrack.scarlet @@ -0,0 +1,6 @@ +.sound-track-media-list + vertical + +.sound-track-media + iframe + width 100% \ No newline at end of file diff --git a/pages/soundtracks/soundtracks.scarlet b/pages/soundtracks/soundtracks.scarlet index 226833be..f4a06f33 100644 --- a/pages/soundtracks/soundtracks.scarlet +++ b/pages/soundtracks/soundtracks.scarlet @@ -8,7 +8,6 @@ flex-basis 500px padding 1rem - .sound-track-content horizontal diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index f21dc066..abbbecfe 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -20,6 +20,17 @@ margin-bottom 1rem overflow hidden +.widget-section + vertical + width 100% + +.widget-title + text-align left + padding-bottom 0.5rem + border-bottom 1px solid rgba(0, 0, 0, 0.1) + // We need !important here to overwrite the h3:first-child rule + margin 1rem 0 !important + .widget-ui-element vertical-wrap ui-element @@ -36,17 +47,6 @@ align-items center width 100% -.widget-section - vertical - width 100% - -.widget-title - text-align left - padding-bottom 0.5rem - border-bottom 1px solid rgba(0, 0, 0, 0.1) - // We need !important here to overwrite the h3:first-child rule - margin 1rem 0 !important - .widget-form width 100% max-width 650px From 892cb3eb3194407bb7981b18667a8dfd964ceb6e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 13 Oct 2017 13:55:33 +0200 Subject: [PATCH 423/527] Refactor --- main_test.go | 53 ------------------------- pages/anime/anime.go | 6 +-- pages/anime/anime.pixy | 2 +- pages/animelistitem/animelistitem.pixy | 4 +- pages/profile/profile.pixy | 4 +- scripts/Actions.ts | 54 ++++---------------------- scripts/AnimeNotifier.ts | 2 +- 7 files changed, 17 insertions(+), 108 deletions(-) diff --git a/main_test.go b/main_test.go index a20c3ca3..f5b0c65b 100644 --- a/main_test.go +++ b/main_test.go @@ -1,15 +1,11 @@ package main import ( - "errors" "net/http" "net/http/httptest" - "reflect" "testing" "github.com/aerogo/aero" - "github.com/aerogo/api" - "github.com/animenotifier/arn" "github.com/fatih/color" ) @@ -33,52 +29,3 @@ func TestRoutes(t *testing.T) { } } } - -func TestInterfaceImplementations(t *testing.T) { - // API interfaces - var creatable = reflect.TypeOf((*api.Creatable)(nil)).Elem() - var editable = reflect.TypeOf((*api.Editable)(nil)).Elem() - var actionable = reflect.TypeOf((*api.Actionable)(nil)).Elem() - var collection = reflect.TypeOf((*api.Collection)(nil)).Elem() - - // Required interface implementations - var interfaceImplementations = map[string][]reflect.Type{ - "User": []reflect.Type{ - editable, - }, - "Thread": []reflect.Type{ - creatable, - editable, - actionable, - }, - "Post": []reflect.Type{ - creatable, - editable, - actionable, - }, - "SoundTrack": []reflect.Type{ - creatable, - editable, - }, - "Analytics": []reflect.Type{ - creatable, - }, - "AnimeList": []reflect.Type{ - collection, - }, - "PushSubscriptions": []reflect.Type{ - collection, - }, - "UserFollows": []reflect.Type{ - collection, - }, - } - - for typeName, interfaces := range interfaceImplementations { - for _, requiredInterface := range interfaces { - if !reflect.PtrTo(arn.DB.Type(typeName)).Implements(requiredInterface) { - panic(errors.New(typeName + " does not implement interface " + requiredInterface.Name())) - } - } - } -} diff --git a/pages/anime/anime.go b/pages/anime/anime.go index ea537484..1339dfef 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -51,13 +51,13 @@ func Get(ctx *aero.Context) string { for i := range friends { j := i - deleted friendAnimeList := friends[j].AnimeList() - obj, err := friendAnimeList.Get(anime.ID) + friendAnimeListItem := friendAnimeList.Find(anime.ID) - if err != nil { + if friendAnimeListItem == nil { friends = friends[:j+copy(friends[j:], friends[j+1:])] deleted++ } else { - friendsAnimeListItems[friends[j]] = obj.(*arn.AnimeListItem) + friendsAnimeListItems[friends[j]] = friendAnimeListItem } } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 4b06618d..3efbc5e1 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -31,7 +31,7 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* Icon("pencil") span Edit in collection else - button.action(data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID, data-user-id=user.ID, data-user-nick=user.Nick) + button.action(data-api="/api/animelist/" + user.ID, data-action="arrayAppend", data-trigger="click", data-field="Items", data-object="{\"AnimeID\": \"" + anime.ID + "\"}") Icon("plus") span Add to collection diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 078b5bd6..97659fe7 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -27,12 +27,12 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. InputTextArea("Notes", item.Notes, "Notes", "Your notes") .buttons.mountable - a.ajax.button(href="/+" + viewUser.Nick + "/animelist/" + item.Status) + a.ajax.button(href="/animelist/" + item.Status) Icon("list") span View collection a.ajax.button(href=anime.Link()) Icon("search-plus") span View anime - button.action(data-action="removeAnimeFromCollection", data-trigger="click", data-anime-id=anime.ID, data-user-id=viewUser.ID, data-user-nick=viewUser.Nick) + button.action(data-action="removeAnimeFromCollection", data-trigger="click", data-api="/api/animelist/" + viewUser.ID, data-field="Items", data-index="AnimeID=\"" + anime.ID + "\"", data-nick=viewUser.Nick) Icon("trash") span Remove from collection \ No newline at end of file diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 135ce40e..9d9eeb61 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -54,11 +54,11 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) .profile-actions if user.ID != viewUser.ID if !user.Follows().Contains(viewUser.ID) - button.profile-action.action(data-action="followUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/add", data-view-user-id=viewUser.ID) + button.profile-action.action(data-action="followUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/add/" + viewUser.ID) Icon("user-plus") span Follow else - button.profile-action.action(data-action="unfollowUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/remove", data-view-user-id=viewUser.ID) + button.profile-action.action(data-action="unfollowUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/remove/" + viewUser.ID) Icon("user-times") span Unfollow diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 8ee8c49b..3367a659 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -5,7 +5,7 @@ import { findAll } from "./Utils" // Follow user export function followUser(arn: AnimeNotifier, elem: HTMLElement) { - return arn.post(elem.dataset.api, elem.dataset.viewUserId) + return arn.post(elem.dataset.api, "") .then(() => arn.reloadContent()) .then(() => arn.statusMessage.showInfo("You are now following " + arn.app.find("nick").innerText + ".")) .catch(err => arn.statusMessage.showError(err)) @@ -13,7 +13,7 @@ export function followUser(arn: AnimeNotifier, elem: HTMLElement) { // Unfollow user export function unfollowUser(arn: AnimeNotifier, elem: HTMLElement) { - return arn.post(elem.dataset.api, elem.dataset.viewUserId) + return arn.post(elem.dataset.api, "") .then(() => arn.reloadContent()) .then(() => arn.statusMessage.showInfo("You stopped following " + arn.app.find("nick").innerText + ".")) .catch(err => arn.statusMessage.showError(err)) @@ -254,52 +254,14 @@ export function testNotification(arn: AnimeNotifier) { }) } -// Add anime to collection -export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) { - button.innerText = "Adding..." - arn.loading(true) - - let {animeId, userId, userNick} = button.dataset - - fetch("/api/animelist/" + userId + "/add", { - method: "POST", - body: animeId, - credentials: "same-origin" - }) - .then(response => response.text()) - .then(body => { - if(body !== "ok") { - throw body - } - - return arn.reloadContent() - }) - .catch(err => arn.statusMessage.showError(err)) - .then(() => arn.loading(false)) -} - // Remove anime from collection -export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElement) { - button.innerText = "Removing..." - arn.loading(true) +export function removeAnimeFromCollection(arn: AnimeNotifier, element: HTMLElement) { + let {field, index, nick} = element.dataset + let apiEndpoint = arn.findAPIEndpoint(element) - let {animeId, userId, userNick} = button.dataset - - fetch("/api/animelist/" + userId + "/remove", { - method: "POST", - body: animeId, - credentials: "same-origin" - }) - .then(response => response.text()) - .then(body => { - if(body !== "ok") { - throw body - } - - return arn.app.load("/+" + userNick + "/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value) - }) + arn.post(apiEndpoint + "/field/" + field + "/remove/" + index, "") + .then(() => arn.app.load("/+" + nick + "/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value)) .catch(err => arn.statusMessage.showError(err)) - .then(() => arn.loading(false)) } // Charge up @@ -368,7 +330,7 @@ export function buyItem(arn: AnimeNotifier, button: HTMLElement) { // Append new element to array export function arrayAppend(arn: AnimeNotifier, element: HTMLElement) { let field = element.dataset.field - let object = element.dataset.object ? JSON.parse(element.dataset.object) : {} + let object = element.dataset.object || "" let apiEndpoint = arn.findAPIEndpoint(element) arn.post(apiEndpoint + "/field/" + field + "/append", object) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index d65d6db4..71ee9dcc 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -665,7 +665,7 @@ export class AnimeNotifier { .catch(console.error) } - post(url, body) { + post(url: string, body: any) { if(typeof body !== "string") { body = JSON.stringify(body) } From db15a0eb72a4f71f1bee8c14ae304081126a94e6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 13 Oct 2017 14:40:29 +0200 Subject: [PATCH 424/527] Fixed link --- scripts/Actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 3367a659..8c5c4ec7 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -260,7 +260,7 @@ export function removeAnimeFromCollection(arn: AnimeNotifier, element: HTMLEleme let apiEndpoint = arn.findAPIEndpoint(element) arn.post(apiEndpoint + "/field/" + field + "/remove/" + index, "") - .then(() => arn.app.load("/+" + nick + "/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value)) + .then(() => arn.app.load("/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value)) .catch(err => arn.statusMessage.showError(err)) } From 1126e6ae6b3de49f70a44a62da5654748014e10a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 14 Oct 2017 12:45:22 +0200 Subject: [PATCH 425/527] Refactor --- pages/anime/anime.pixy | 2 +- pages/animelist/animelist.scarlet | 2 ++ pages/animelistitem/animelistitem.pixy | 2 +- scripts/Actions.ts | 50 +++++++++++++++----------- scripts/AnimeNotifier.ts | 18 +++++++--- scripts/Application.ts | 4 --- 6 files changed, 46 insertions(+), 32 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 3efbc5e1..647087c8 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -31,7 +31,7 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* Icon("pencil") span Edit in collection else - button.action(data-api="/api/animelist/" + user.ID, data-action="arrayAppend", data-trigger="click", data-field="Items", data-object="{\"AnimeID\": \"" + anime.ID + "\"}") + button.action(data-api="/api/animelist/" + user.ID, data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID) Icon("plus") span Add to collection diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 73104afe..59310476 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -38,6 +38,7 @@ :hover .plus-episode opacity 1 + pointer-events all .anime-list-item-episodes-watched flex 0.4 @@ -48,6 +49,7 @@ display inline-block cursor pointer opacity 0 + pointer-events none margin-left 1px transition opacity transition-speed ease diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 97659fe7..5313971e 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -33,6 +33,6 @@ component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn. a.ajax.button(href=anime.Link()) Icon("search-plus") span View anime - button.action(data-action="removeAnimeFromCollection", data-trigger="click", data-api="/api/animelist/" + viewUser.ID, data-field="Items", data-index="AnimeID=\"" + anime.ID + "\"", data-nick=viewUser.Nick) + button.action(data-action="removeAnimeFromCollection", data-trigger="click", data-api="/api/animelist/" + viewUser.ID, data-anime-id=anime.ID, data-nick=viewUser.Nick) Icon("trash") span Remove from collection \ No newline at end of file diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 8c5c4ec7..eb8a8e53 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -26,12 +26,14 @@ export function toggleSidebar(arn: AnimeNotifier) { // Save new data from an input field export function save(arn: AnimeNotifier, input: HTMLElement) { - arn.loading(true) - - let isContentEditable = input.isContentEditable let obj = {} + let isContentEditable = input.isContentEditable let value = isContentEditable ? input.innerText : (input as HTMLInputElement).value + if(value === undefined) { + return + } + if((input as HTMLInputElement).type === "number" || input.dataset.type === "number") { if(input.getAttribute("step") === "1" || input.dataset.step === "1") { obj[input.dataset.field] = parseInt(value) @@ -50,21 +52,9 @@ export function save(arn: AnimeNotifier, input: HTMLElement) { let apiEndpoint = arn.findAPIEndpoint(input) - fetch(apiEndpoint, { - method: "POST", - body: JSON.stringify(obj), - credentials: "same-origin" - }) - .then(response => response.text()) - .then(body => { - if(body !== "ok") { - throw body - } - }) + arn.post(apiEndpoint, obj) .catch(err => arn.statusMessage.showError(err)) .then(() => { - arn.loading(false) - if(isContentEditable) { input.contentEditable = "true" } else { @@ -82,6 +72,10 @@ export function closeStatusMessage(arn: AnimeNotifier) { // Increase episode export function increaseEpisode(arn: AnimeNotifier, element: HTMLElement) { + if(arn.isLoading) { + return + } + let prev = element.previousSibling as HTMLElement let episodes = parseInt(prev.innerText) prev.innerText = String(episodes + 1) @@ -254,12 +248,26 @@ export function testNotification(arn: AnimeNotifier) { }) } -// Remove anime from collection -export function removeAnimeFromCollection(arn: AnimeNotifier, element: HTMLElement) { - let {field, index, nick} = element.dataset - let apiEndpoint = arn.findAPIEndpoint(element) +// Add anime to collection +export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) { + button.innerText = "Adding..." + + let {animeId} = button.dataset + let apiEndpoint = arn.findAPIEndpoint(button) - arn.post(apiEndpoint + "/field/" + field + "/remove/" + index, "") + arn.post(apiEndpoint + "/add/" + animeId, "") + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// Remove anime from collection +export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElement) { + button.innerText = "Removing..." + + let {animeId, nick} = button.dataset + let apiEndpoint = arn.findAPIEndpoint(button) + + arn.post(apiEndpoint + "/remove/" + animeId, "") .then(() => arn.app.load("/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value)) .catch(err => arn.statusMessage.showError(err)) } diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 71ee9dcc..538d1b99 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -23,6 +23,7 @@ export class AnimeNotifier { touchController: TouchController sideBar: SideBar mainPageLoaded: boolean + isLoading: boolean lastReloadContentPath: string elementFound: MutationQueue @@ -33,6 +34,7 @@ export class AnimeNotifier { this.app = app this.user = null this.title = "Anime Notifier" + this.isLoading = true this.elementFound = new MutationQueue(elem => elem.classList.add("element-found")) this.elementNotFound = new MutationQueue(elem => elem.classList.add("element-not-found")) @@ -123,9 +125,9 @@ export class AnimeNotifier { // Sidebar control this.sideBar = new SideBar(this.app.find("sidebar")) - - // Let"s start - this.app.run() + + // Loading + this.loading(false) } onContentLoaded() { @@ -446,8 +448,10 @@ export class AnimeNotifier { .then(() => this.loading(false)) // Because our loading element gets reset due to full page diff } - loading(isLoading: boolean) { - if(isLoading) { + loading(newState: boolean) { + this.isLoading = newState + + if(this.isLoading) { document.documentElement.style.cursor = "progress" this.app.loading.classList.remove(this.app.fadeOutClass) } else { @@ -666,6 +670,10 @@ export class AnimeNotifier { } post(url: string, body: any) { + if(this.isLoading) { + return Promise.resolve() + } + if(typeof body !== "string") { body = JSON.stringify(body) } diff --git a/scripts/Application.ts b/scripts/Application.ts index 3c16c134..2b8c395d 100644 --- a/scripts/Application.ts +++ b/scripts/Application.ts @@ -30,10 +30,6 @@ export class Application { }) } - run() { - this.loading.classList.add(this.fadeOutClass) - } - find(id: string): HTMLElement { return document.getElementById(id) } From 97f02b0f41bc5d222c748f6440202eb45abe0af6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 14 Oct 2017 14:23:12 +0200 Subject: [PATCH 426/527] Fixed sidebar style --- styles/sidebar.scarlet | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/sidebar.scarlet b/styles/sidebar.scarlet index 7b1ffa08..856a493b 100644 --- a/styles/sidebar.scarlet +++ b/styles/sidebar.scarlet @@ -22,6 +22,7 @@ sidebar-spacing-y = 0.7rem horizontal justify-content center margin 0.8rem 0 + flex-shrink 0 > 800px #sidebar From 78915bd6019c6616412e8b293714eeab36e887cf Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 14 Oct 2017 15:41:31 +0200 Subject: [PATCH 427/527] Updated to latest Aero --- pages/paypal/paypal.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/paypal/paypal.go b/pages/paypal/paypal.go index e3afe009..2c5d188f 100644 --- a/pages/paypal/paypal.go +++ b/pages/paypal/paypal.go @@ -17,7 +17,11 @@ func CreatePayment(ctx *aero.Context) string { return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) } - amount := string(ctx.RequestBody()) + amount, err := ctx.Request().Body().String() + + if err != nil { + return ctx.Error(http.StatusBadRequest, "Could not read amount", err) + } // Verify amount switch amount { From c0b28c9b0a077b5e2679f8e2cb12ca240d2bfe53 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 14 Oct 2017 17:18:32 +0200 Subject: [PATCH 428/527] Minor change --- main.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index ff6132eb..79744cc8 100644 --- a/main.go +++ b/main.go @@ -178,10 +178,12 @@ func configure(app *aero.Application) *aero.Application { app.Rewrite(rewrite) // Middleware - app.Use(middleware.Firewall()) - app.Use(middleware.Log()) - app.Use(middleware.Session()) - app.Use(middleware.UserInfo()) + app.Use( + middleware.Firewall(), + middleware.Log(), + middleware.Session(), + middleware.UserInfo() + ) // API arn.API.Install(app) From d1c26252ac1b29cd78c708f9d2636a948ce60635 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 15 Oct 2017 20:19:45 +0200 Subject: [PATCH 429/527] Improved soundtracks --- main.go | 2 +- mixins/Input.pixy | 4 +++- pages/anime/anime.go | 4 +++- pages/profile/tracks.go | 4 +++- pages/soundtrack/edit.go | 10 ++++++++- pages/soundtrack/soundtrack.go | 5 ++++- pages/soundtrack/soundtrack.pixy | 6 ++++++ pages/soundtracks/soundtracks.go | 2 +- pages/soundtracks/soundtracks.pixy | 2 +- scripts/Actions.ts | 34 ++++++++++++++++++------------ scripts/AnimeNotifier.ts | 13 +++++++----- 11 files changed, 60 insertions(+), 26 deletions(-) diff --git a/main.go b/main.go index 79744cc8..d74a4c3a 100644 --- a/main.go +++ b/main.go @@ -182,7 +182,7 @@ func configure(app *aero.Application) *aero.Application { middleware.Firewall(), middleware.Log(), middleware.Session(), - middleware.UserInfo() + middleware.UserInfo(), ) // API diff --git a/mixins/Input.pixy b/mixins/Input.pixy index 2c0b93d1..df1a39e8 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -18,7 +18,7 @@ component InputSelection(id string, value string, label string, placeholder stri label(for=id)= label + ":" select.widget-ui-element.action(id=id, data-field=id, value=value, title=placeholder, data-action="save", data-trigger="change") -component InputTags(id string, value []string, label string) +component InputTags(id string, value []string, label string, tooltip string) .widget-section label(for=id)= label + ":" .tags(id=id) @@ -30,4 +30,6 @@ component InputTags(id string, value []string, label string) button.tag-add.action(data-action="arrayAppend", data-trigger="click", data-field=id) RawIcon("plus") + + p!= tooltip \ No newline at end of file diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 1339dfef..d256057e 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -23,7 +23,9 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime not found", err) } - tracks, err := arn.GetSoundTracksByTag("anime:" + anime.ID) + tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + return !track.IsDraft && len(track.Media) > 0 && arn.Contains(track.Tags, "anime:"+anime.ID) + }) if err != nil { return ctx.Error(http.StatusNotFound, "Error fetching soundtracks", err) diff --git a/pages/profile/tracks.go b/pages/profile/tracks.go index 2b723403..083f3ae9 100644 --- a/pages/profile/tracks.go +++ b/pages/profile/tracks.go @@ -19,7 +19,9 @@ func GetSoundTracksByUser(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "User not found", err) } - tracks, err := arn.GetSoundTracksByUser(viewUser) + tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + return !track.IsDraft && len(track.Media) > 0 && track.CreatedBy == viewUser.ID + }) if err != nil { return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index 0246fa0d..dd72b80d 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -89,7 +89,15 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i case "string": b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, "")) case "[]string": - b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name)) + b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name, field.Tag.Get("tooltip"))) + case "bool": + if field.Name == "IsDraft" { + if fieldValue.Bool() { + b.WriteString(`
    `) + } else { + b.WriteString(`
    `) + } + } case "[]*arn.ExternalMedia": for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { b.WriteString(`
    `) diff --git a/pages/soundtrack/soundtrack.go b/pages/soundtrack/soundtrack.go index afc492c8..5a5c8e75 100644 --- a/pages/soundtrack/soundtrack.go +++ b/pages/soundtrack/soundtrack.go @@ -20,12 +20,15 @@ func Get(ctx *aero.Context) string { ctx.Data = &arn.OpenGraph{ Tags: map[string]string{ "og:title": track.Title, - "og:image": track.MainAnime().Image.Large, "og:url": "https://" + ctx.App.Config.Domain + track.Link(), "og:site_name": "notify.moe", "og:type": "music.song", }, } + if track.MainAnime() != nil { + ctx.Data.(*arn.OpenGraph).Tags["og:image"] = track.MainAnime().Image.Large + } + return ctx.HTML(components.Track(track)) } diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index 6c7a57dc..453ee76b 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -13,6 +13,12 @@ component Track(track *arn.SoundTrack) h3.widget-title= media.Service .sound-track-media ExternalMedia(media) + + .widget.mountable + h3.widget-title Tags + ul + each tag in track.Tags + li= tag .footer.text-center.mountable if track.EditedBy != "" diff --git a/pages/soundtracks/soundtracks.go b/pages/soundtracks/soundtracks.go index 93c8c7f6..11dee8b2 100644 --- a/pages/soundtracks/soundtracks.go +++ b/pages/soundtracks/soundtracks.go @@ -13,7 +13,7 @@ const maxTracks = 9 // Get renders the soundtracks page. func Get(ctx *aero.Context) string { tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { - return !track.IsDraft + return !track.IsDraft && len(track.Media) > 0 }) if err != nil { diff --git a/pages/soundtracks/soundtracks.pixy b/pages/soundtracks/soundtracks.pixy index 82bbdf01..0d175189 100644 --- a/pages/soundtracks/soundtracks.pixy +++ b/pages/soundtracks/soundtracks.pixy @@ -2,7 +2,7 @@ component SoundTracks(tracks []*arn.SoundTrack) h1 Soundtracks .music-buttons - a.button.ajax(href="/new/soundtrack") + a.button.action(data-action="newSoundTrack", data-trigger="click") Icon("plus") span Add soundtrack diff --git a/scripts/Actions.ts b/scripts/Actions.ts index eb8a8e53..65ec77fb 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -195,21 +195,29 @@ export function createThread(arn: AnimeNotifier) { .catch(err => arn.statusMessage.showError(err)) } -// Create soundtrack -export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { - let soundcloud = arn.app.find("soundcloud-link") as HTMLInputElement - let youtube = arn.app.find("youtube-link") as HTMLInputElement - let anime = arn.app.find("anime-link") as HTMLInputElement - let osu = arn.app.find("osu-link") as HTMLInputElement +// New soundtrack +export function newSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { + arn.post("/api/new/soundtrack", "") + .then(response => response.json()) + .then(response => console.log(response)) + .catch(err => arn.statusMessage.showError(err)) +} - let soundtrack = { - soundcloud: soundcloud.value, - youtube: youtube.value, - tags: [anime.value, osu.value], - } +// Publish +export function publish(arn: AnimeNotifier, button: HTMLButtonElement) { + let endpoint = arn.findAPIEndpoint(button) - arn.post("/api/new/soundtrack", soundtrack) - .then(() => arn.app.load("/soundtracks")) + arn.post(endpoint + "/publish", "") + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// Unpublish +export function unpublish(arn: AnimeNotifier, button: HTMLButtonElement) { + let endpoint = arn.findAPIEndpoint(button) + + arn.post(endpoint + "/unpublish", "") + .then(() => arn.reloadContent()) .catch(err => arn.statusMessage.showError(err)) } diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 538d1b99..14e6f79d 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -671,7 +671,7 @@ export class AnimeNotifier { post(url: string, body: any) { if(this.isLoading) { - return Promise.resolve() + return Promise.resolve(null) } if(typeof body !== "string") { @@ -685,13 +685,16 @@ export class AnimeNotifier { body, credentials: "same-origin" }) - .then(response => response.text()) - .then(body => { + .then(response => { this.loading(false) - if(body !== "ok") { - throw body + if(response.status === 200) { + return Promise.resolve(response) } + + return response.text().then(err => { + throw err + }) }) .catch(err => { this.loading(false) From e635be5291be3d835dddf949523fc8558a55ff16 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 15 Oct 2017 20:26:50 +0200 Subject: [PATCH 430/527] Fixed dashboard --- pages/dashboard/dashboard.go | 4 +++- pages/dashboard/dashboard.pixy | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index ae7bfae0..1559ff10 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -60,7 +60,9 @@ func Get(ctx *aero.Context) string { } }, func() { var err error - soundTracks, err = arn.AllSoundTracks() + soundTracks, err = arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + return !track.IsDraft && len(track.Media) > 0 + }) if err != nil { return diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index d2494772..95bbebeb 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -46,7 +46,10 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound a.widget-ui-element.ajax(href=soundTracks[i].Link()) .widget-ui-element-text Icon("music") - span(title=soundTracks[i].Title)= soundTracks[i].MainAnime().Title.Canonical + if soundTracks[i].Title == "" + span untitled + else + span= soundTracks[i].Title else .widget-ui-element .widget-ui-element-text From 68901ac25ecc4e88b287562a6012f4e76aad11c5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 15 Oct 2017 21:11:12 +0200 Subject: [PATCH 431/527] Added soundtrack drafts --- pages/soundtrack/edit.go | 5 +++-- pages/soundtracks/soundtracks.go | 5 ++++- pages/soundtracks/soundtracks.pixy | 14 ++++++++++---- scripts/Actions.ts | 8 ++++++-- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index dd72b80d..94569065 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -94,9 +94,10 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i if field.Name == "IsDraft" { if fieldValue.Bool() { b.WriteString(`
    `) - } else { - b.WriteString(`
    `) } + // else { + // b.WriteString(`
    `) + // } } case "[]*arn.ExternalMedia": for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { diff --git a/pages/soundtracks/soundtracks.go b/pages/soundtracks/soundtracks.go index 11dee8b2..bac4310c 100644 --- a/pages/soundtracks/soundtracks.go +++ b/pages/soundtracks/soundtracks.go @@ -6,12 +6,15 @@ import ( "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" ) const maxTracks = 9 // Get renders the soundtracks page. func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { return !track.IsDraft && len(track.Media) > 0 }) @@ -26,5 +29,5 @@ func Get(ctx *aero.Context) string { tracks = tracks[:maxTracks] } - return ctx.HTML(components.SoundTracks(tracks)) + return ctx.HTML(components.SoundTracks(tracks, user)) } diff --git a/pages/soundtracks/soundtracks.pixy b/pages/soundtracks/soundtracks.pixy index 0d175189..7791a875 100644 --- a/pages/soundtracks/soundtracks.pixy +++ b/pages/soundtracks/soundtracks.pixy @@ -1,10 +1,16 @@ -component SoundTracks(tracks []*arn.SoundTrack) +component SoundTracks(tracks []*arn.SoundTrack, user *arn.User) h1 Soundtracks .music-buttons - a.button.action(data-action="newSoundTrack", data-trigger="click") - Icon("plus") - span Add soundtrack + if user != nil + if user.DraftIndex().SoundTrackID == "" + button.action(data-action="newSoundTrack", data-trigger="click") + Icon("plus") + span Add soundtrack + else + a.button.ajax(href="/soundtrack/" + user.DraftIndex().SoundTrackID + "/edit") + Icon("pencil") + span Edit draft .sound-tracks each track in tracks diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 65ec77fb..de2db6d8 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -199,7 +199,7 @@ export function createThread(arn: AnimeNotifier) { export function newSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { arn.post("/api/new/soundtrack", "") .then(response => response.json()) - .then(response => console.log(response)) + .then(track => arn.app.load(`/soundtrack/${track.id}/edit`)) .catch(err => arn.statusMessage.showError(err)) } @@ -208,7 +208,7 @@ export function publish(arn: AnimeNotifier, button: HTMLButtonElement) { let endpoint = arn.findAPIEndpoint(button) arn.post(endpoint + "/publish", "") - .then(() => arn.reloadContent()) + .then(() => arn.app.load(arn.app.currentPath.replace("/edit", ""))) .catch(err => arn.statusMessage.showError(err)) } @@ -356,6 +356,10 @@ export function arrayAppend(arn: AnimeNotifier, element: HTMLElement) { // Remove element from array export function arrayRemove(arn: AnimeNotifier, element: HTMLElement) { + if(!confirm("Are you sure you want to remove this element?")) { + return + } + let field = element.dataset.field let index = element.dataset.index let apiEndpoint = arn.findAPIEndpoint(element) From 7336fa2035826096c7f13a69d9164b0ce4a119f2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 16 Oct 2017 01:07:08 +0200 Subject: [PATCH 432/527] New statistics --- jobs/statistics/statistics.go | 18 +++++++++++++++++- main_test.go | 4 ++-- tests.go | 16 ---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index 81d7a61e..984324db 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -43,6 +43,8 @@ func getUserStats() []*arn.PieChart { os := stats{} notifications := stats{} avatar := stats{} + ip := stats{} + pro := stats{} for _, info := range analytics { user, err := arn.GetUser(info.UserID) @@ -52,7 +54,7 @@ func getUserStats() []*arn.PieChart { continue } - pixelRatio[fmt.Sprintf("%.1f", info.Screen.PixelRatio)]++ + pixelRatio[fmt.Sprintf("%.0f", info.Screen.PixelRatio)]++ size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) screenSize[size]++ @@ -94,6 +96,18 @@ func getUserStats() []*arn.PieChart { } else { avatar[user.Avatar.Source]++ } + + if arn.IsIPv6(user.IP) { + ip["IPv6"]++ + } else { + ip["IPv4"]++ + } + + if user.IsPro() { + pro["PRO account"]++ + } else { + pro["Free account"]++ + } } println("Finished user statistics") @@ -107,6 +121,8 @@ func getUserStats() []*arn.PieChart { arn.NewPieChart("Notifications", notifications), arn.NewPieChart("Gender", gender), arn.NewPieChart("Pixel ratio", pixelRatio), + arn.NewPieChart("IP version", ip), + arn.NewPieChart("PRO accounts", pro), } } diff --git a/main_test.go b/main_test.go index f5b0c65b..57037505 100644 --- a/main_test.go +++ b/main_test.go @@ -1,12 +1,12 @@ package main import ( + "fmt" "net/http" "net/http/httptest" "testing" "github.com/aerogo/aero" - "github.com/fatih/color" ) func TestRoutes(t *testing.T) { @@ -24,7 +24,7 @@ func TestRoutes(t *testing.T) { app.Handler().ServeHTTP(responseRecorder, request) if status := responseRecorder.Code; status != http.StatusOK { - color.Red("%s | Wrong status code | %v instead of %v", example, status, http.StatusOK) + panic(fmt.Errorf("%s | Wrong status code | %v instead of %v", example, status, http.StatusOK)) } } } diff --git a/tests.go b/tests.go index 29875659..51d3579c 100644 --- a/tests.go +++ b/tests.go @@ -100,14 +100,6 @@ var routeTests = map[string][]string{ "/api/animelist/4J6qpK1ve", }, - "/api/animelist/:id/get/:item": []string{ - "/api/animelist/4J6qpK1ve/get/7929", - }, - - "/api/animelist/:id/get/:item/:property": []string{ - "/api/animelist/4J6qpK1ve/get/7929/Episodes", - }, - "/api/settings/:id": []string{ "/api/settings/4J6qpK1ve", }, @@ -144,14 +136,6 @@ var routeTests = map[string][]string{ "/api/soundtrack/h0ac8sKkg", }, - "/api/soundcloudtosoundtrack/:id": []string{ - "/api/soundcloudtosoundtrack/145918628", - }, - - "/api/youtubetosoundtrack/:id": []string{ - "/api/youtubetosoundtrack/hU2wqJuOIp4", - }, - "/api/userfollows/:id": []string{ "/api/userfollows/4J6qpK1ve", }, From 126835629652cd8ec5d67d36136169cc526a8f23 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 16 Oct 2017 01:11:47 +0200 Subject: [PATCH 433/527] Minor changes --- jobs/statistics/statistics.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go index 984324db..deb9d5e1 100644 --- a/jobs/statistics/statistics.go +++ b/jobs/statistics/statistics.go @@ -104,9 +104,9 @@ func getUserStats() []*arn.PieChart { } if user.IsPro() { - pro["PRO account"]++ + pro["PRO accounts"]++ } else { - pro["Free account"]++ + pro["Free accounts"]++ } } From ed82c712204e198e102682c6376f4ee578f37979 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 16 Oct 2017 01:26:41 +0200 Subject: [PATCH 434/527] Added anime links to soundtracks --- pages/profile/watching.scarlet | 9 +-------- pages/soundtrack/soundtrack.pixy | 8 ++++++++ pages/soundtrack/soundtrack.scarlet | 11 ++++++++++- styles/include/mixins.scarlet | 10 ++++++++++ 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 0e37f03a..3cd77f89 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -6,11 +6,4 @@ margin 0.25rem .profile-watching-list-item-image - width 55px !important - height 78px !important - border-radius 2px - filter none - transition filter transition-speed ease, opacity transition-speed ease - - :hover - filter saturate(1.3) \ No newline at end of file + anime-cover-image-mini \ No newline at end of file diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index 453ee76b..37640406 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -14,6 +14,14 @@ component Track(track *arn.SoundTrack) .sound-track-media ExternalMedia(media) + .widget.mountable + h3.widget-title Anime + + .sound-track-anime-list + each anime in track.Anime() + a.sound-track-anime-list-item.ajax(href=anime.Link(), title=anime.Title.Canonical) + img.sound-track-anime-list-item-image.lazy(data-src=anime.Image.Tiny, alt=anime.Title.Canonical) + .widget.mountable h3.widget-title Tags ul diff --git a/pages/soundtrack/soundtrack.scarlet b/pages/soundtrack/soundtrack.scarlet index b82c9ba3..692df7bc 100644 --- a/pages/soundtrack/soundtrack.scarlet +++ b/pages/soundtrack/soundtrack.scarlet @@ -3,4 +3,13 @@ .sound-track-media iframe - width 100% \ No newline at end of file + width 100% + +.sound-track-anime-list + horizontal-wrap + +.sound-track-anime-list-item + // + +.sound-track-anime-list-item-image + anime-cover-image-mini \ No newline at end of file diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 0f8294ad..417c1f49 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -42,6 +42,16 @@ mixin clip-long-text white-space nowrap text-overflow ellipsis +mixin anime-cover-image-mini + width 55px !important + height 78px !important + border-radius 2px + filter none + transition filter transition-speed ease, opacity transition-speed ease + + :hover + filter saturate(1.3) + mixin bg-dark-up background-color transparent :hover From 4a6f54ec52c3a63cd243627f42031cb2bfaeb5be Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 16 Oct 2017 01:35:28 +0200 Subject: [PATCH 435/527] Improved tag rendering --- mixins/Input.pixy | 2 +- pages/soundtrack/soundtrack.pixy | 4 ++-- styles/tags.scarlet | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index df1a39e8..a85ac9c2 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -23,7 +23,7 @@ component InputTags(id string, value []string, label string, tooltip string) label(for=id)= label + ":" .tags(id=id) for index, tag := range value - .tag + .tag.tag-edit span.tag-title.action(contenteditable="true", data-trigger="focusout", data-action="save", data-field=id + "[" + strconv.Itoa(index) + "]")= tag button.tag-remove.action(data-action="arrayRemove", data-trigger="click", data-field=id, data-index=index) RawIcon("trash") diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index 37640406..cbf7f071 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -24,9 +24,9 @@ component Track(track *arn.SoundTrack) .widget.mountable h3.widget-title Tags - ul + .tags each tag in track.Tags - li= tag + .tag= tag .footer.text-center.mountable if track.EditedBy != "" diff --git a/styles/tags.scarlet b/styles/tags.scarlet index 7c0638a1..7ddebfea 100644 --- a/styles/tags.scarlet +++ b/styles/tags.scarlet @@ -10,6 +10,8 @@ mixin tag-dimensions ui-element tag-dimensions margin-right 0 + +.tag-edit border-right none border-top-right-radius 0 border-bottom-right-radius 0 From fb04540c295881aee2e3dea6ab30fa72c533ab52 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 16 Oct 2017 10:22:22 +0200 Subject: [PATCH 436/527] Added post time to soundtracks page --- mixins/SoundTrack.pixy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index b4748642..2494b28a 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -24,7 +24,9 @@ component SoundTrackFooter(track *arn.SoundTrack) a.ajax(href=track.Link() + "/edit") untitled else a.ajax(href=track.Link())= track.Title - span posted by + span posted + span.utc-date(data-date=track.Created) + span by a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick + " " component ExternalMedia(media *arn.ExternalMedia) From 8fdeb97de9b7a0af5d7be00b0ed22821a7cb643a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 16 Oct 2017 11:53:47 +0200 Subject: [PATCH 437/527] Removed flickering on iframes --- main.go | 1 + mixins/LoadMore.pixy | 4 ++++ pages/forum/forum.pixy | 4 +--- pages/soundtracks/soundtracks.go | 33 ++++++++++++++++++++++++++++++ pages/soundtracks/soundtracks.pixy | 12 ++++++++--- scripts/Actions.ts | 14 +++++++++++++ scripts/AnimeNotifier.ts | 2 +- scripts/Diff.ts | 14 ++++++------- 8 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 mixins/LoadMore.pixy diff --git a/main.go b/main.go index d74a4c3a..44437560 100644 --- a/main.go +++ b/main.go @@ -98,6 +98,7 @@ func configure(app *aero.Application) *aero.Application { // Soundtracks app.Ajax("/soundtracks", soundtracks.Get) + app.Ajax("/soundtracks/from/:index", soundtracks.From) app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/soundtrack/:id", soundtrack.Get) app.Ajax("/soundtrack/:id/edit", soundtrack.Edit) diff --git a/mixins/LoadMore.pixy b/mixins/LoadMore.pixy new file mode 100644 index 00000000..928ba6ee --- /dev/null +++ b/mixins/LoadMore.pixy @@ -0,0 +1,4 @@ +component LoadMore + button.action(data-action="loadMore", data-trigger="click") + Icon("refresh") + span Load more \ No newline at end of file diff --git a/pages/forum/forum.pixy b/pages/forum/forum.pixy index caffb037..bf0ddc2c 100644 --- a/pages/forum/forum.pixy +++ b/pages/forum/forum.pixy @@ -9,9 +9,7 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) Icon("plus") span New thread if len(threads) == threadsPerPage - button - Icon("refresh") - span Load more + LoadMore component ThreadList(threads []*arn.Thread) if len(threads) == 0 diff --git a/pages/soundtracks/soundtracks.go b/pages/soundtracks/soundtracks.go index bac4310c..0ada1abd 100644 --- a/pages/soundtracks/soundtracks.go +++ b/pages/soundtracks/soundtracks.go @@ -2,6 +2,7 @@ package soundtracks import ( "net/http" + "strconv" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -31,3 +32,35 @@ func Get(ctx *aero.Context) string { return ctx.HTML(components.SoundTracks(tracks, user)) } + +// From renders the soundtracks from the given index. +func From(ctx *aero.Context) string { + user := utils.GetUser(ctx) + index, err := ctx.GetInt("index") + + if err != nil { + return ctx.Error(http.StatusBadRequest, "Invalid start index", err) + } + + tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + return !track.IsDraft && len(track.Media) > 0 + }) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) + } + + if index < 0 || index >= len(tracks) { + return ctx.Error(http.StatusBadRequest, "Invalid start index (maximum is "+strconv.Itoa(len(tracks))+")", nil) + } + + arn.SortSoundTracksLatestFirst(tracks) + + tracks = tracks[index:] + + if len(tracks) > maxTracks { + tracks = tracks[:maxTracks] + } + + return ctx.HTML(components.SoundTracksScrollable(tracks, user)) +} diff --git a/pages/soundtracks/soundtracks.pixy b/pages/soundtracks/soundtracks.pixy index 7791a875..0ccf177a 100644 --- a/pages/soundtracks/soundtracks.pixy +++ b/pages/soundtracks/soundtracks.pixy @@ -12,6 +12,12 @@ component SoundTracks(tracks []*arn.SoundTrack, user *arn.User) Icon("pencil") span Edit draft - .sound-tracks - each track in tracks - SoundTrack(track) \ No newline at end of file + #load-more-target.sound-tracks + SoundTracksScrollable(tracks, user) + + //- .buttons + //- LoadMore + +component SoundTracksScrollable(tracks []*arn.SoundTrack, user *arn.User) + each track in tracks + SoundTrack(track) \ No newline at end of file diff --git a/scripts/Actions.ts b/scripts/Actions.ts index de2db6d8..342f1640 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -369,6 +369,20 @@ export function arrayRemove(arn: AnimeNotifier, element: HTMLElement) { .catch(err => arn.statusMessage.showError(err)) } +// Load more +export function loadMore(arn: AnimeNotifier, element: HTMLElement) { + let target = arn.app.find("load-more-target") + let index = "9" + + fetch("/_" + arn.app.currentPath + "/from/" + index) + .then(response => response.text()) + .then(body => { + target.innerHTML += body + arn.app.emit("DOMContentLoaded") + }) + .catch(err => arn.statusMessage.showError(err)) +} + // Chrome extension installation export function installExtension(arn: AnimeNotifier, button: HTMLElement) { let browser: any = window["chrome"] diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 14e6f79d..088c6303 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -549,7 +549,7 @@ export class AnimeNotifier { // Once the iframe becomes visible, load it element["became visible"] = () => { // If the source is already set correctly, don't set it again to avoid iframe flickering. - if(element.src !== element.dataset.src) { + if(element.src !== element.dataset.src && element.src !== (window.location.protocol + element.dataset.src)) { element.src = element.dataset.src } diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 38b96155..de3bdcfc 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -91,12 +91,12 @@ export class Diff { continue } - if(attrib.name === "class") { - // If the class is exactly the same, skip this attribute. - if(elemA.getAttribute("class") === attrib.value) { - continue - } + // If the attribute value is exactly the same, skip this attribute. + if(elemA.getAttribute(attrib.name) === attrib.value) { + continue + } + if(attrib.name === "class") { let classesA = elemA.classList let classesB = elemB.classList let removeClasses: string[] = [] @@ -119,8 +119,8 @@ export class Diff { continue } - - elemA.setAttribute(attrib.name, elemB.getAttribute(attrib.name)) + + elemA.setAttribute(attrib.name, attrib.value) } // Special case: Apply state of input elements From 555582836c424578f3e72934aa6075c3e013c3a1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 16 Oct 2017 12:56:46 +0200 Subject: [PATCH 438/527] Added infinite scrolling --- mixins/LoadMore.pixy | 4 +-- pages/forum/forum.pixy | 4 +-- pages/soundtracks/soundtracks.go | 24 ++++++++++++----- pages/soundtracks/soundtracks.pixy | 7 ++--- scripts/Actions.ts | 42 +++++++++++++++++++++++++++--- scripts/AnimeNotifier.ts | 5 ++++ scripts/InfiniteScroller.ts | 25 ++++++++++++++++++ 7 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 scripts/InfiniteScroller.ts diff --git a/mixins/LoadMore.pixy b/mixins/LoadMore.pixy index 928ba6ee..94337ed8 100644 --- a/mixins/LoadMore.pixy +++ b/mixins/LoadMore.pixy @@ -1,4 +1,4 @@ -component LoadMore - button.action(data-action="loadMore", data-trigger="click") +component LoadMore(index int) + button#load-more-button.action(data-action="loadMore", data-trigger="click", data-index=index) Icon("refresh") span Load more \ No newline at end of file diff --git a/pages/forum/forum.pixy b/pages/forum/forum.pixy index bf0ddc2c..093fb157 100644 --- a/pages/forum/forum.pixy +++ b/pages/forum/forum.pixy @@ -8,8 +8,8 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) button#new-thread.action(data-action="load", data-trigger="click", data-url="/new/thread") Icon("plus") span New thread - if len(threads) == threadsPerPage - LoadMore + //- if len(threads) == threadsPerPage + //- LoadMore component ThreadList(threads []*arn.Thread) if len(threads) == 0 diff --git a/pages/soundtracks/soundtracks.go b/pages/soundtracks/soundtracks.go index 0ada1abd..ce2052ec 100644 --- a/pages/soundtracks/soundtracks.go +++ b/pages/soundtracks/soundtracks.go @@ -10,7 +10,7 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -const maxTracks = 9 +const maxTracks = 12 // Get renders the soundtracks page. func Get(ctx *aero.Context) string { @@ -30,7 +30,7 @@ func Get(ctx *aero.Context) string { tracks = tracks[:maxTracks] } - return ctx.HTML(components.SoundTracks(tracks, user)) + return ctx.HTML(components.SoundTracks(tracks, maxTracks, user)) } // From renders the soundtracks from the given index. @@ -42,7 +42,7 @@ func From(ctx *aero.Context) string { return ctx.Error(http.StatusBadRequest, "Invalid start index", err) } - tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + allTracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { return !track.IsDraft && len(track.Media) > 0 }) @@ -50,17 +50,27 @@ func From(ctx *aero.Context) string { return ctx.Error(http.StatusInternalServerError, "Error fetching soundtracks", err) } - if index < 0 || index >= len(tracks) { - return ctx.Error(http.StatusBadRequest, "Invalid start index (maximum is "+strconv.Itoa(len(tracks))+")", nil) + if index < 0 || index >= len(allTracks) { + return ctx.Error(http.StatusBadRequest, "Invalid start index (maximum is "+strconv.Itoa(len(allTracks))+")", nil) } - arn.SortSoundTracksLatestFirst(tracks) + arn.SortSoundTracksLatestFirst(allTracks) - tracks = tracks[index:] + tracks := allTracks[index:] if len(tracks) > maxTracks { tracks = tracks[:maxTracks] } + nextIndex := index + maxTracks + + if nextIndex >= len(allTracks) { + // End of data - no more scrolling + ctx.Response().Header().Set("X-LoadMore-Index", "-1") + } else { + // Send the index for the next request + ctx.Response().Header().Set("X-LoadMore-Index", strconv.Itoa(nextIndex)) + } + return ctx.HTML(components.SoundTracksScrollable(tracks, user)) } diff --git a/pages/soundtracks/soundtracks.pixy b/pages/soundtracks/soundtracks.pixy index 0ccf177a..e05647d1 100644 --- a/pages/soundtracks/soundtracks.pixy +++ b/pages/soundtracks/soundtracks.pixy @@ -1,4 +1,4 @@ -component SoundTracks(tracks []*arn.SoundTrack, user *arn.User) +component SoundTracks(tracks []*arn.SoundTrack, tracksPerPage int, user *arn.User) h1 Soundtracks .music-buttons @@ -15,8 +15,9 @@ component SoundTracks(tracks []*arn.SoundTrack, user *arn.User) #load-more-target.sound-tracks SoundTracksScrollable(tracks, user) - //- .buttons - //- LoadMore + if len(tracks) == tracksPerPage + .buttons + LoadMore(tracksPerPage) component SoundTracksScrollable(tracks []*arn.SoundTrack, user *arn.User) each track in tracks diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 342f1640..2c70ac74 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -370,17 +370,51 @@ export function arrayRemove(arn: AnimeNotifier, element: HTMLElement) { } // Load more -export function loadMore(arn: AnimeNotifier, element: HTMLElement) { +export function loadMore(arn: AnimeNotifier, button: HTMLButtonElement) { + // Prevent firing this event multiple times + if(arn.isLoading || button.disabled) { + return + } + + arn.loading(true) + button.disabled = true + let target = arn.app.find("load-more-target") - let index = "9" + let index = button.dataset.index fetch("/_" + arn.app.currentPath + "/from/" + index) + .then(response => { + let newIndex = response.headers.get("X-LoadMore-Index") + + // End of data? + if(newIndex === "-1") { + button.classList.add("hidden") + } else { + button.dataset.index = newIndex + } + + return response + }) .then(response => response.text()) .then(body => { - target.innerHTML += body - arn.app.emit("DOMContentLoaded") + let tmp = document.createElement(target.tagName) + tmp.innerHTML = body + + let children = [...tmp.childNodes] + + window.requestAnimationFrame(() => { + for(let child of children) { + target.appendChild(child) + } + + arn.app.emit("DOMContentLoaded") + }) }) .catch(err => arn.statusMessage.showError(err)) + .then(() => { + arn.loading(false) + button.disabled = false + }) } // Chrome extension installation diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 088c6303..23605e29 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -9,6 +9,7 @@ import { PushManager } from "./PushManager" import { TouchController } from "./TouchController" import { Analytics } from "./Analytics" import { SideBar } from "./SideBar" +import { InfiniteScroller } from "./InfiniteScroller" export class AnimeNotifier { app: Application @@ -22,6 +23,7 @@ export class AnimeNotifier { pushManager: PushManager touchController: TouchController sideBar: SideBar + infiniteScroller: InfiniteScroller mainPageLoaded: boolean isLoading: boolean lastReloadContentPath: string @@ -125,6 +127,9 @@ export class AnimeNotifier { // Sidebar control this.sideBar = new SideBar(this.app.find("sidebar")) + + // Infinite scrolling + this.infiniteScroller = new InfiniteScroller(this.app.content.parentElement, 100) // Loading this.loading(false) diff --git a/scripts/InfiniteScroller.ts b/scripts/InfiniteScroller.ts new file mode 100644 index 00000000..c2e387c5 --- /dev/null +++ b/scripts/InfiniteScroller.ts @@ -0,0 +1,25 @@ +export class InfiniteScroller { + container: HTMLElement + threshold: number + + constructor(container, threshold) { + this.container = container + this.threshold = threshold + + this.container.addEventListener("scroll", e => { + if(this.container.scrollTop + this.container.clientHeight >= this.container.scrollHeight - threshold) { + this.loadMore() + } + }) + } + + loadMore() { + let button = document.getElementById("load-more-button") + + if(!button) { + return + } + + button.click() + } +} \ No newline at end of file From 56815fcb228f1c60fe58d6efca0897b10128d610 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 01:54:54 +0200 Subject: [PATCH 439/527] Added patch to list season with links --- patches/show-season/show-season.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 patches/show-season/show-season.go diff --git a/patches/show-season/show-season.go b/patches/show-season/show-season.go new file mode 100644 index 00000000..f0ee77b2 --- /dev/null +++ b/patches/show-season/show-season.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + + "github.com/animenotifier/arn" +) + +func main() { + for anime := range arn.MustStreamAnime() { + if anime.NSFW == 1 || anime.Status != "current" || anime.StartDate == "" || anime.StartDate < "2017-09" || anime.StartDate > "2017-10-17" { + continue + } + + fmt.Printf("* [%s](/anime/%s)\n", anime.Title.Canonical, anime.ID) + } +} From d65f3fd7bb226b15d989df51ca07ed6cff1ba92b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 11:27:15 +0200 Subject: [PATCH 440/527] Refactor actions --- scripts/Actions.ts | 444 +--------------------------- scripts/Actions/AnimeList.ts | 25 ++ scripts/Actions/Diff.ts | 14 + scripts/Actions/FollowUser.ts | 17 ++ scripts/Actions/Forum.ts | 78 +++++ scripts/Actions/InfiniteScroller.ts | 49 +++ scripts/Actions/Install.ts | 12 + scripts/Actions/Like.ts | 19 ++ scripts/Actions/Notifications.ts | 20 ++ scripts/Actions/Publish.ts | 19 ++ scripts/Actions/Search.ts | 17 ++ scripts/Actions/Serialization.ts | 80 +++++ scripts/Actions/Shop.ts | 64 ++++ scripts/Actions/SideBar.ts | 6 + scripts/Actions/SoundTrack.ts | 9 + scripts/Actions/StatusMessage.ts | 6 + 16 files changed, 450 insertions(+), 429 deletions(-) create mode 100644 scripts/Actions/AnimeList.ts create mode 100644 scripts/Actions/Diff.ts create mode 100644 scripts/Actions/FollowUser.ts create mode 100644 scripts/Actions/Forum.ts create mode 100644 scripts/Actions/InfiniteScroller.ts create mode 100644 scripts/Actions/Install.ts create mode 100644 scripts/Actions/Like.ts create mode 100644 scripts/Actions/Notifications.ts create mode 100644 scripts/Actions/Publish.ts create mode 100644 scripts/Actions/Search.ts create mode 100644 scripts/Actions/Serialization.ts create mode 100644 scripts/Actions/Shop.ts create mode 100644 scripts/Actions/SideBar.ts create mode 100644 scripts/Actions/SoundTrack.ts create mode 100644 scripts/Actions/StatusMessage.ts diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 2c70ac74..60d4a79e 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -1,429 +1,15 @@ -import { Application } from "./Application" -import { AnimeNotifier } from "./AnimeNotifier" -import { Diff } from "./Diff" -import { findAll } from "./Utils" - -// Follow user -export function followUser(arn: AnimeNotifier, elem: HTMLElement) { - return arn.post(elem.dataset.api, "") - .then(() => arn.reloadContent()) - .then(() => arn.statusMessage.showInfo("You are now following " + arn.app.find("nick").innerText + ".")) - .catch(err => arn.statusMessage.showError(err)) -} - -// Unfollow user -export function unfollowUser(arn: AnimeNotifier, elem: HTMLElement) { - return arn.post(elem.dataset.api, "") - .then(() => arn.reloadContent()) - .then(() => arn.statusMessage.showInfo("You stopped following " + arn.app.find("nick").innerText + ".")) - .catch(err => arn.statusMessage.showError(err)) -} - -// Toggle sidebar -export function toggleSidebar(arn: AnimeNotifier) { - arn.app.find("sidebar").classList.toggle("sidebar-visible") -} - -// Save new data from an input field -export function save(arn: AnimeNotifier, input: HTMLElement) { - let obj = {} - let isContentEditable = input.isContentEditable - let value = isContentEditable ? input.innerText : (input as HTMLInputElement).value - - if(value === undefined) { - return - } - - if((input as HTMLInputElement).type === "number" || input.dataset.type === "number") { - if(input.getAttribute("step") === "1" || input.dataset.step === "1") { - obj[input.dataset.field] = parseInt(value) - } else { - obj[input.dataset.field] = parseFloat(value) - } - } else { - obj[input.dataset.field] = value - } - - if(isContentEditable) { - input.contentEditable = "false" - } else { - (input as HTMLInputElement).disabled = true - } - - let apiEndpoint = arn.findAPIEndpoint(input) - - arn.post(apiEndpoint, obj) - .catch(err => arn.statusMessage.showError(err)) - .then(() => { - if(isContentEditable) { - input.contentEditable = "true" - } else { - (input as HTMLInputElement).disabled = false - } - - return arn.reloadContent() - }) -} - -// Close status message -export function closeStatusMessage(arn: AnimeNotifier) { - arn.statusMessage.close() -} - -// Increase episode -export function increaseEpisode(arn: AnimeNotifier, element: HTMLElement) { - if(arn.isLoading) { - return - } - - let prev = element.previousSibling as HTMLElement - let episodes = parseInt(prev.innerText) - prev.innerText = String(episodes + 1) - save(arn, prev) -} - -// Load -export function load(arn: AnimeNotifier, element: HTMLElement) { - let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") - arn.app.load(url) -} - -// Soon -export function soon() { - alert("Coming Soon™") -} - -// Diff -export function diff(arn: AnimeNotifier, element: HTMLElement) { - let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") - - arn.diff(url).then(() => arn.scrollTo(element)) -} - -// Edit post -export function editPost(arn: AnimeNotifier, element: HTMLElement) { - let postId = element.dataset.id - - let render = arn.app.find("render-" + postId) - let toolbar = arn.app.find("toolbar-" + postId) - let title = arn.app.find("title-" + postId) - let source = arn.app.find("source-" + postId) - let edit = arn.app.find("edit-toolbar-" + postId) - - render.classList.toggle("hidden") - toolbar.classList.toggle("hidden") - source.classList.toggle("hidden") - edit.classList.toggle("hidden") - - if(title) { - title.classList.toggle("hidden") - } -} - -// Save post -export function savePost(arn: AnimeNotifier, element: HTMLElement) { - let postId = element.dataset.id - let source = arn.app.find("source-" + postId) as HTMLTextAreaElement - let title = arn.app.find("title-" + postId) as HTMLInputElement - let text = source.value - - let updates: any = { - Text: text, - } - - // Add title for threads only - if(title) { - updates.Title = title.value - } - - let apiEndpoint = arn.findAPIEndpoint(element) - - arn.post(apiEndpoint, updates) - .then(() => arn.reloadContent()) - .catch(err => arn.statusMessage.showError(err)) -} - -// like -export function like(arn: AnimeNotifier, element: HTMLElement) { - let apiEndpoint = arn.findAPIEndpoint(element) - - arn.post(apiEndpoint + "/like", null) - .then(() => arn.reloadContent()) - .catch(err => arn.statusMessage.showError(err)) -} - -// unlike -export function unlike(arn: AnimeNotifier, element: HTMLElement) { - let apiEndpoint = arn.findAPIEndpoint(element) - - arn.post(apiEndpoint + "/unlike", null) - .then(() => arn.reloadContent()) - .catch(err => arn.statusMessage.showError(err)) -} - -// Forum reply -export function forumReply(arn: AnimeNotifier) { - let textarea = arn.app.find("new-reply") as HTMLTextAreaElement - let thread = arn.app.find("thread") - - let post = { - text: textarea.value, - threadId: thread.dataset.id, - tags: [] - } - - arn.post("/api/new/post", post) - .then(() => arn.reloadContent()) - .then(() => textarea.value = "") - .catch(err => arn.statusMessage.showError(err)) -} - -// Create thread -export function createThread(arn: AnimeNotifier) { - let title = arn.app.find("title") as HTMLInputElement - let text = arn.app.find("text") as HTMLTextAreaElement - let category = arn.app.find("tag") as HTMLInputElement - - let thread = { - title: title.value, - text: text.value, - tags: [category.value] - } - - arn.post("/api/new/thread", thread) - .then(() => arn.app.load("/forum/" + thread.tags[0])) - .catch(err => arn.statusMessage.showError(err)) -} - -// New soundtrack -export function newSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { - arn.post("/api/new/soundtrack", "") - .then(response => response.json()) - .then(track => arn.app.load(`/soundtrack/${track.id}/edit`)) - .catch(err => arn.statusMessage.showError(err)) -} - -// Publish -export function publish(arn: AnimeNotifier, button: HTMLButtonElement) { - let endpoint = arn.findAPIEndpoint(button) - - arn.post(endpoint + "/publish", "") - .then(() => arn.app.load(arn.app.currentPath.replace("/edit", ""))) - .catch(err => arn.statusMessage.showError(err)) -} - -// Unpublish -export function unpublish(arn: AnimeNotifier, button: HTMLButtonElement) { - let endpoint = arn.findAPIEndpoint(button) - - arn.post(endpoint + "/unpublish", "") - .then(() => arn.reloadContent()) - .catch(err => arn.statusMessage.showError(err)) -} - -// Search -export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { - if(e.ctrlKey || e.altKey) { - return - } - - let term = search.value - - if(!term || term.length < 2) { - arn.app.content.innerHTML = "Please enter at least 2 characters to start searching." - return - } - - arn.diff("/search/" + term) -} - -// Enable notifications -export async function enableNotifications(arn: AnimeNotifier, button: HTMLElement) { - await arn.pushManager.subscribe(arn.user.dataset.id) - arn.updatePushUI() -} - -// Disable notifications -export async function disableNotifications(arn: AnimeNotifier, button: HTMLElement) { - await arn.pushManager.unsubscribe(arn.user.dataset.id) - arn.updatePushUI() -} - -// Test notification -export function testNotification(arn: AnimeNotifier) { - fetch("/api/test/notification", { - credentials: "same-origin" - }) -} - -// Add anime to collection -export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) { - button.innerText = "Adding..." - - let {animeId} = button.dataset - let apiEndpoint = arn.findAPIEndpoint(button) - - arn.post(apiEndpoint + "/add/" + animeId, "") - .then(() => arn.reloadContent()) - .catch(err => arn.statusMessage.showError(err)) -} - -// Remove anime from collection -export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElement) { - button.innerText = "Removing..." - - let {animeId, nick} = button.dataset - let apiEndpoint = arn.findAPIEndpoint(button) - - arn.post(apiEndpoint + "/remove/" + animeId, "") - .then(() => arn.app.load("/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value)) - .catch(err => arn.statusMessage.showError(err)) -} - -// Charge up -export function chargeUp(arn: AnimeNotifier, button: HTMLElement) { - let amount = button.dataset.amount - - arn.loading(true) - arn.statusMessage.showInfo("Creating PayPal transaction... This might take a few seconds.") - - fetch("/api/paypal/payment/create", { - method: "POST", - body: amount, - credentials: "same-origin" - }) - .then(response => response.json()) - .then(payment => { - if(!payment || !payment.links) { - throw "Error creating PayPal payment" - } - - console.log(payment) - let link = payment.links.find(link => link.rel === "approval_url") - - if(!link) { - throw "Error finding PayPal payment link" - } - - arn.statusMessage.showInfo("Redirecting to PayPal...", 5000) - - let url = link.href - window.location.href = url - }) - .catch(err => arn.statusMessage.showError(err)) - .then(() => arn.loading(false)) -} - -// Buy item -export function buyItem(arn: AnimeNotifier, button: HTMLElement) { - let itemId = button.dataset.itemId - let itemName = button.dataset.itemName - let price = button.dataset.price - - if(!confirm(`Would you like to buy ${itemName} for ${price} gems?`)) { - return - } - - arn.loading(true) - - fetch(`/api/shop/buy/${itemId}/1`, { - method: "POST", - credentials: "same-origin" - }) - .then(response => response.text()) - .then(body => { - if(body !== "ok") { - throw body - } - - return arn.reloadContent() - }) - .then(() => arn.statusMessage.showInfo(`You bought ${itemName} for ${price} gems. Check out your inventory to confirm the purchase.`, 4000)) - .catch(err => arn.statusMessage.showError(err)) - .then(() => arn.loading(false)) -} - -// Append new element to array -export function arrayAppend(arn: AnimeNotifier, element: HTMLElement) { - let field = element.dataset.field - let object = element.dataset.object || "" - let apiEndpoint = arn.findAPIEndpoint(element) - - arn.post(apiEndpoint + "/field/" + field + "/append", object) - .then(() => arn.reloadContent()) - .catch(err => arn.statusMessage.showError(err)) -} - -// Remove element from array -export function arrayRemove(arn: AnimeNotifier, element: HTMLElement) { - if(!confirm("Are you sure you want to remove this element?")) { - return - } - - let field = element.dataset.field - let index = element.dataset.index - let apiEndpoint = arn.findAPIEndpoint(element) - - arn.post(apiEndpoint + "/field/" + field + "/remove/" + index, "") - .then(() => arn.reloadContent()) - .catch(err => arn.statusMessage.showError(err)) -} - -// Load more -export function loadMore(arn: AnimeNotifier, button: HTMLButtonElement) { - // Prevent firing this event multiple times - if(arn.isLoading || button.disabled) { - return - } - - arn.loading(true) - button.disabled = true - - let target = arn.app.find("load-more-target") - let index = button.dataset.index - - fetch("/_" + arn.app.currentPath + "/from/" + index) - .then(response => { - let newIndex = response.headers.get("X-LoadMore-Index") - - // End of data? - if(newIndex === "-1") { - button.classList.add("hidden") - } else { - button.dataset.index = newIndex - } - - return response - }) - .then(response => response.text()) - .then(body => { - let tmp = document.createElement(target.tagName) - tmp.innerHTML = body - - let children = [...tmp.childNodes] - - window.requestAnimationFrame(() => { - for(let child of children) { - target.appendChild(child) - } - - arn.app.emit("DOMContentLoaded") - }) - }) - .catch(err => arn.statusMessage.showError(err)) - .then(() => { - arn.loading(false) - button.disabled = false - }) -} - -// Chrome extension installation -export function installExtension(arn: AnimeNotifier, button: HTMLElement) { - let browser: any = window["chrome"] - browser.webstore.install() -} - -// Desktop app installation -export function installApp() { - alert("Open your browser menu > 'More tools' > 'Add to desktop' and enable 'Open as window'.") -} \ No newline at end of file +export * from "./Actions/AnimeList" +export * from "./Actions/Diff" +export * from "./Actions/FollowUser" +export * from "./Actions/Forum" +export * from "./Actions/InfiniteScroller" +export * from "./Actions/Install" +export * from "./Actions/Like" +export * from "./Actions/Notifications" +export * from "./Actions/Publish" +export * from "./Actions/Search" +export * from "./Actions/Serialization" +export * from "./Actions/Shop" +export * from "./Actions/SideBar" +export * from "./Actions/SoundTrack" +export * from "./Actions/StatusMessage" \ No newline at end of file diff --git a/scripts/Actions/AnimeList.ts b/scripts/Actions/AnimeList.ts new file mode 100644 index 00000000..06530483 --- /dev/null +++ b/scripts/Actions/AnimeList.ts @@ -0,0 +1,25 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Add anime to collection +export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) { + button.innerText = "Adding..." + + let {animeId} = button.dataset + let apiEndpoint = arn.findAPIEndpoint(button) + + arn.post(apiEndpoint + "/add/" + animeId, "") + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// Remove anime from collection +export function removeAnimeFromCollection(arn: AnimeNotifier, button: HTMLElement) { + button.innerText = "Removing..." + + let {animeId, nick} = button.dataset + let apiEndpoint = arn.findAPIEndpoint(button) + + arn.post(apiEndpoint + "/remove/" + animeId, "") + .then(() => arn.app.load("/animelist/" + (arn.app.find("Status") as HTMLSelectElement).value)) + .catch(err => arn.statusMessage.showError(err)) +} \ No newline at end of file diff --git a/scripts/Actions/Diff.ts b/scripts/Actions/Diff.ts new file mode 100644 index 00000000..66e35c81 --- /dev/null +++ b/scripts/Actions/Diff.ts @@ -0,0 +1,14 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Load +export function load(arn: AnimeNotifier, element: HTMLElement) { + let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") + arn.app.load(url) +} + +// Diff +export function diff(arn: AnimeNotifier, element: HTMLElement) { + let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") + + arn.diff(url).then(() => arn.scrollTo(element)) +} \ No newline at end of file diff --git a/scripts/Actions/FollowUser.ts b/scripts/Actions/FollowUser.ts new file mode 100644 index 00000000..096016a5 --- /dev/null +++ b/scripts/Actions/FollowUser.ts @@ -0,0 +1,17 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Follow user +export function followUser(arn: AnimeNotifier, elem: HTMLElement) { + return arn.post(elem.dataset.api, "") + .then(() => arn.reloadContent()) + .then(() => arn.statusMessage.showInfo("You are now following " + arn.app.find("nick").innerText + ".")) + .catch(err => arn.statusMessage.showError(err)) +} + +// Unfollow user +export function unfollowUser(arn: AnimeNotifier, elem: HTMLElement) { + return arn.post(elem.dataset.api, "") + .then(() => arn.reloadContent()) + .then(() => arn.statusMessage.showInfo("You stopped following " + arn.app.find("nick").innerText + ".")) + .catch(err => arn.statusMessage.showError(err)) +} \ No newline at end of file diff --git a/scripts/Actions/Forum.ts b/scripts/Actions/Forum.ts new file mode 100644 index 00000000..7bc14bce --- /dev/null +++ b/scripts/Actions/Forum.ts @@ -0,0 +1,78 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Edit post +export function editPost(arn: AnimeNotifier, element: HTMLElement) { + let postId = element.dataset.id + + let render = arn.app.find("render-" + postId) + let toolbar = arn.app.find("toolbar-" + postId) + let title = arn.app.find("title-" + postId) + let source = arn.app.find("source-" + postId) + let edit = arn.app.find("edit-toolbar-" + postId) + + render.classList.toggle("hidden") + toolbar.classList.toggle("hidden") + source.classList.toggle("hidden") + edit.classList.toggle("hidden") + + if(title) { + title.classList.toggle("hidden") + } +} + +// Save post +export function savePost(arn: AnimeNotifier, element: HTMLElement) { + let postId = element.dataset.id + let source = arn.app.find("source-" + postId) as HTMLTextAreaElement + let title = arn.app.find("title-" + postId) as HTMLInputElement + let text = source.value + + let updates: any = { + Text: text, + } + + // Add title for threads only + if(title) { + updates.Title = title.value + } + + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint, updates) + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// Forum reply +export function forumReply(arn: AnimeNotifier) { + let textarea = arn.app.find("new-reply") as HTMLTextAreaElement + let thread = arn.app.find("thread") + + let post = { + text: textarea.value, + threadId: thread.dataset.id, + tags: [] + } + + arn.post("/api/new/post", post) + .then(() => arn.reloadContent()) + .then(() => textarea.value = "") + .catch(err => arn.statusMessage.showError(err)) +} + +// Create thread +export function createThread(arn: AnimeNotifier) { + let title = arn.app.find("title") as HTMLInputElement + let text = arn.app.find("text") as HTMLTextAreaElement + let category = arn.app.find("tag") as HTMLInputElement + + let thread = { + title: title.value, + text: text.value, + tags: [category.value] + } + + arn.post("/api/new/thread", thread) + .then(() => arn.app.load("/forum/" + thread.tags[0])) + .catch(err => arn.statusMessage.showError(err)) +} \ No newline at end of file diff --git a/scripts/Actions/InfiniteScroller.ts b/scripts/Actions/InfiniteScroller.ts new file mode 100644 index 00000000..213ced6e --- /dev/null +++ b/scripts/Actions/InfiniteScroller.ts @@ -0,0 +1,49 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Load more +export function loadMore(arn: AnimeNotifier, button: HTMLButtonElement) { + // Prevent firing this event multiple times + if(arn.isLoading || button.disabled) { + return + } + + arn.loading(true) + button.disabled = true + + let target = arn.app.find("load-more-target") + let index = button.dataset.index + + fetch("/_" + arn.app.currentPath + "/from/" + index) + .then(response => { + let newIndex = response.headers.get("X-LoadMore-Index") + + // End of data? + if(newIndex === "-1") { + button.classList.add("hidden") + } else { + button.dataset.index = newIndex + } + + return response + }) + .then(response => response.text()) + .then(body => { + let tmp = document.createElement(target.tagName) + tmp.innerHTML = body + + let children = [...tmp.childNodes] + + window.requestAnimationFrame(() => { + for(let child of children) { + target.appendChild(child) + } + + arn.app.emit("DOMContentLoaded") + }) + }) + .catch(err => arn.statusMessage.showError(err)) + .then(() => { + arn.loading(false) + button.disabled = false + }) +} \ No newline at end of file diff --git a/scripts/Actions/Install.ts b/scripts/Actions/Install.ts new file mode 100644 index 00000000..8903d1ef --- /dev/null +++ b/scripts/Actions/Install.ts @@ -0,0 +1,12 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Chrome extension installation +export function installExtension(arn: AnimeNotifier, button: HTMLElement) { + let browser: any = window["chrome"] + browser.webstore.install() +} + +// Desktop app installation +export function installApp() { + alert("Open your browser menu > 'More tools' > 'Add to desktop' and enable 'Open as window'.") +} \ No newline at end of file diff --git a/scripts/Actions/Like.ts b/scripts/Actions/Like.ts new file mode 100644 index 00000000..22783139 --- /dev/null +++ b/scripts/Actions/Like.ts @@ -0,0 +1,19 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// like +export function like(arn: AnimeNotifier, element: HTMLElement) { + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint + "/like", null) + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// unlike +export function unlike(arn: AnimeNotifier, element: HTMLElement) { + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint + "/unlike", null) + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} \ No newline at end of file diff --git a/scripts/Actions/Notifications.ts b/scripts/Actions/Notifications.ts new file mode 100644 index 00000000..10557525 --- /dev/null +++ b/scripts/Actions/Notifications.ts @@ -0,0 +1,20 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Enable notifications +export async function enableNotifications(arn: AnimeNotifier, button: HTMLElement) { + await arn.pushManager.subscribe(arn.user.dataset.id) + arn.updatePushUI() +} + +// Disable notifications +export async function disableNotifications(arn: AnimeNotifier, button: HTMLElement) { + await arn.pushManager.unsubscribe(arn.user.dataset.id) + arn.updatePushUI() +} + +// Test notification +export function testNotification(arn: AnimeNotifier) { + fetch("/api/test/notification", { + credentials: "same-origin" + }) +} \ No newline at end of file diff --git a/scripts/Actions/Publish.ts b/scripts/Actions/Publish.ts new file mode 100644 index 00000000..3e49721b --- /dev/null +++ b/scripts/Actions/Publish.ts @@ -0,0 +1,19 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Publish +export function publish(arn: AnimeNotifier, button: HTMLButtonElement) { + let endpoint = arn.findAPIEndpoint(button) + + arn.post(endpoint + "/publish", "") + .then(() => arn.app.load(arn.app.currentPath.replace("/edit", ""))) + .catch(err => arn.statusMessage.showError(err)) +} + +// Unpublish +export function unpublish(arn: AnimeNotifier, button: HTMLButtonElement) { + let endpoint = arn.findAPIEndpoint(button) + + arn.post(endpoint + "/unpublish", "") + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} \ No newline at end of file diff --git a/scripts/Actions/Search.ts b/scripts/Actions/Search.ts new file mode 100644 index 00000000..d1615cf3 --- /dev/null +++ b/scripts/Actions/Search.ts @@ -0,0 +1,17 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Search +export function search(arn: AnimeNotifier, search: HTMLInputElement, e: KeyboardEvent) { + if(e.ctrlKey || e.altKey) { + return + } + + let term = search.value + + if(!term || term.length < 2) { + arn.app.content.innerHTML = "Please enter at least 2 characters to start searching." + return + } + + arn.diff("/search/" + term) +} \ No newline at end of file diff --git a/scripts/Actions/Serialization.ts b/scripts/Actions/Serialization.ts new file mode 100644 index 00000000..c575cb4f --- /dev/null +++ b/scripts/Actions/Serialization.ts @@ -0,0 +1,80 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Save new data from an input field +export function save(arn: AnimeNotifier, input: HTMLElement) { + let obj = {} + let isContentEditable = input.isContentEditable + let value = isContentEditable ? input.innerText : (input as HTMLInputElement).value + + if(value === undefined) { + return + } + + if((input as HTMLInputElement).type === "number" || input.dataset.type === "number") { + if(input.getAttribute("step") === "1" || input.dataset.step === "1") { + obj[input.dataset.field] = parseInt(value) + } else { + obj[input.dataset.field] = parseFloat(value) + } + } else { + obj[input.dataset.field] = value + } + + if(isContentEditable) { + input.contentEditable = "false" + } else { + (input as HTMLInputElement).disabled = true + } + + let apiEndpoint = arn.findAPIEndpoint(input) + + arn.post(apiEndpoint, obj) + .catch(err => arn.statusMessage.showError(err)) + .then(() => { + if(isContentEditable) { + input.contentEditable = "true" + } else { + (input as HTMLInputElement).disabled = false + } + + return arn.reloadContent() + }) +} + +// Append new element to array +export function arrayAppend(arn: AnimeNotifier, element: HTMLElement) { + let field = element.dataset.field + let object = element.dataset.object || "" + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint + "/field/" + field + "/append", object) + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// Remove element from array +export function arrayRemove(arn: AnimeNotifier, element: HTMLElement) { + if(!confirm("Are you sure you want to remove this element?")) { + return + } + + let field = element.dataset.field + let index = element.dataset.index + let apiEndpoint = arn.findAPIEndpoint(element) + + arn.post(apiEndpoint + "/field/" + field + "/remove/" + index, "") + .then(() => arn.reloadContent()) + .catch(err => arn.statusMessage.showError(err)) +} + +// Increase episode +export function increaseEpisode(arn: AnimeNotifier, element: HTMLElement) { + if(arn.isLoading) { + return + } + + let prev = element.previousSibling as HTMLElement + let episodes = parseInt(prev.innerText) + prev.innerText = String(episodes + 1) + save(arn, prev) +} \ No newline at end of file diff --git a/scripts/Actions/Shop.ts b/scripts/Actions/Shop.ts new file mode 100644 index 00000000..5f7a8cf1 --- /dev/null +++ b/scripts/Actions/Shop.ts @@ -0,0 +1,64 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Charge up +export function chargeUp(arn: AnimeNotifier, button: HTMLElement) { + let amount = button.dataset.amount + + arn.loading(true) + arn.statusMessage.showInfo("Creating PayPal transaction... This might take a few seconds.") + + fetch("/api/paypal/payment/create", { + method: "POST", + body: amount, + credentials: "same-origin" + }) + .then(response => response.json()) + .then(payment => { + if(!payment || !payment.links) { + throw "Error creating PayPal payment" + } + + console.log(payment) + let link = payment.links.find(link => link.rel === "approval_url") + + if(!link) { + throw "Error finding PayPal payment link" + } + + arn.statusMessage.showInfo("Redirecting to PayPal...", 5000) + + let url = link.href + window.location.href = url + }) + .catch(err => arn.statusMessage.showError(err)) + .then(() => arn.loading(false)) +} + +// Buy item +export function buyItem(arn: AnimeNotifier, button: HTMLElement) { + let itemId = button.dataset.itemId + let itemName = button.dataset.itemName + let price = button.dataset.price + + if(!confirm(`Would you like to buy ${itemName} for ${price} gems?`)) { + return + } + + arn.loading(true) + + fetch(`/api/shop/buy/${itemId}/1`, { + method: "POST", + credentials: "same-origin" + }) + .then(response => response.text()) + .then(body => { + if(body !== "ok") { + throw body + } + + return arn.reloadContent() + }) + .then(() => arn.statusMessage.showInfo(`You bought ${itemName} for ${price} gems. Check out your inventory to confirm the purchase.`, 4000)) + .catch(err => arn.statusMessage.showError(err)) + .then(() => arn.loading(false)) +} \ No newline at end of file diff --git a/scripts/Actions/SideBar.ts b/scripts/Actions/SideBar.ts new file mode 100644 index 00000000..7547b701 --- /dev/null +++ b/scripts/Actions/SideBar.ts @@ -0,0 +1,6 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Toggle sidebar +export function toggleSidebar(arn: AnimeNotifier) { + arn.app.find("sidebar").classList.toggle("sidebar-visible") +} \ No newline at end of file diff --git a/scripts/Actions/SoundTrack.ts b/scripts/Actions/SoundTrack.ts new file mode 100644 index 00000000..1e435414 --- /dev/null +++ b/scripts/Actions/SoundTrack.ts @@ -0,0 +1,9 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// New soundtrack +export function newSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { + arn.post("/api/new/soundtrack", "") + .then(response => response.json()) + .then(track => arn.app.load(`/soundtrack/${track.id}/edit`)) + .catch(err => arn.statusMessage.showError(err)) +} \ No newline at end of file diff --git a/scripts/Actions/StatusMessage.ts b/scripts/Actions/StatusMessage.ts new file mode 100644 index 00000000..a5f6f014 --- /dev/null +++ b/scripts/Actions/StatusMessage.ts @@ -0,0 +1,6 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Close status message +export function closeStatusMessage(arn: AnimeNotifier) { + arn.statusMessage.close() +} \ No newline at end of file From 96c0d221928b785b4dfcb41df62968944cf498d9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 11:43:07 +0200 Subject: [PATCH 441/527] Implemented soundtrack deletion --- pages/soundtrack/edit.go | 15 ++++++++++++--- scripts/Actions.ts | 1 + scripts/Actions/Delete.ts | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 scripts/Actions/Delete.ts diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index 94569065..59c06a23 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -19,6 +19,7 @@ import ( func Edit(ctx *aero.Context) string { id := ctx.Get("id") track, err := arn.GetSoundTrack(id) + user := utils.GetUser(ctx) if err != nil { return ctx.Error(http.StatusNotFound, "Track not found", err) @@ -37,11 +38,11 @@ func Edit(ctx *aero.Context) string { ctx.Data.(*arn.OpenGraph).Tags["og:image"] = track.MainAnime().Image.Large } - return ctx.HTML(components.SoundTrackTabs(track) + EditForm(track, "Edit soundtrack")) + return ctx.HTML(components.SoundTrackTabs(track) + EditForm(track, "Edit soundtrack", user)) } // EditForm ... -func EditForm(obj interface{}, title string) string { +func EditForm(obj interface{}, title string, user *arn.User) string { t := reflect.TypeOf(obj).Elem() v := reflect.ValueOf(obj).Elem() id := reflect.Indirect(v.FieldByName("ID")) @@ -59,6 +60,12 @@ func EditForm(obj interface{}, title string) string { RenderObject(&b, obj, "") + if user != nil && (user.Role == "editor" || user.Role == "admin") { + b.WriteString(`
    `) + b.WriteString(``) + b.WriteString(`
    `) + } + b.WriteString("
    ") b.WriteString("
    ") @@ -119,7 +126,9 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i b.WriteString(`
    `) } - b.WriteString(`
    `) + b.WriteString(`
    `) + b.WriteString(``) + b.WriteString(`
    `) default: panic("No edit form implementation for " + idPrefix + field.Name + " with type " + field.Type.String()) } diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 60d4a79e..71138565 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -1,4 +1,5 @@ export * from "./Actions/AnimeList" +export * from "./Actions/Delete" export * from "./Actions/Diff" export * from "./Actions/FollowUser" export * from "./Actions/Forum" diff --git a/scripts/Actions/Delete.ts b/scripts/Actions/Delete.ts new file mode 100644 index 00000000..23559b91 --- /dev/null +++ b/scripts/Actions/Delete.ts @@ -0,0 +1,17 @@ +import { AnimeNotifier } from "../AnimeNotifier" + +// Delete +export function deleteObject(arn: AnimeNotifier, button: HTMLButtonElement) { + let confirmType = button.dataset.confirmType + let returnPath = button.dataset.returnPath + + if(!confirm(`Are you sure you want to delete this ${confirmType}?`)) { + return + } + + let endpoint = arn.findAPIEndpoint(button) + + arn.post(endpoint + "/delete", "") + .then(() => arn.app.load(returnPath)) + .catch(err => arn.statusMessage.showError(err)) +} \ No newline at end of file From 44b33b8215e09f530dc1812c415b513cbe404945 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 11:48:20 +0200 Subject: [PATCH 442/527] Stop caching infinite scrolling --- sw/service-worker.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 456a6645..2f4bfafb 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -5,9 +5,19 @@ const RELOADS = new Map>() const ETAGS = new Map() const CACHEREFRESH = new Map>() const EXCLUDECACHE = new Set([ + // API requests "/api/", + + // PayPal stuff "/paypal/", + + // List imports "/import/", + + // Infinite scrolling + "/from/", + + // Chrome extension "chrome-extension" ]) From dea7ab42fa5cb6b47896df23531a0ed57490749d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 12:55:54 +0200 Subject: [PATCH 443/527] Started working on media relations --- .../sync-media-relations.go | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 jobs/sync-media-relations/sync-media-relations.go diff --git a/jobs/sync-media-relations/sync-media-relations.go b/jobs/sync-media-relations/sync-media-relations.go new file mode 100644 index 00000000..49b5ee33 --- /dev/null +++ b/jobs/sync-media-relations/sync-media-relations.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/animenotifier/kitsu" + "github.com/fatih/color" +) + +func main() { + color.Yellow("Syncing media relations with Kitsu DB") + + kitsuMediaRelations := kitsu.StreamMediaRelations() + + for mediaRelation := range kitsuMediaRelations { + // We only care about anime for now + if mediaRelation.Relationships.Source.Data.Type != "anime" || mediaRelation.Relationships.Destination.Data.Type != "anime" { + continue + } + + relationType := strings.Replace(mediaRelation.Attributes.Role, "_", " ", -1) + + fmt.Printf( + "%s %s has a %s which is %s %s\n", + mediaRelation.Relationships.Source.Data.Type, + mediaRelation.Relationships.Source.Data.ID, + color.GreenString(relationType), + mediaRelation.Relationships.Destination.Data.Type, + mediaRelation.Relationships.Destination.Data.ID, + ) + } + + color.Green("Finished.") +} From 011ca84bd0feae0e5eea36b4d2d8b6942e330729 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 15:47:07 +0200 Subject: [PATCH 444/527] Save anime relations --- .../sync-media-relations.go | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/jobs/sync-media-relations/sync-media-relations.go b/jobs/sync-media-relations/sync-media-relations.go index 49b5ee33..bd7a5ecd 100644 --- a/jobs/sync-media-relations/sync-media-relations.go +++ b/jobs/sync-media-relations/sync-media-relations.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + "github.com/animenotifier/arn" + "github.com/animenotifier/kitsu" "github.com/fatih/color" ) @@ -12,6 +14,7 @@ func main() { color.Yellow("Syncing media relations with Kitsu DB") kitsuMediaRelations := kitsu.StreamMediaRelations() + relations := map[arn.AnimeID]*arn.AnimeRelations{} for mediaRelation := range kitsuMediaRelations { // We only care about anime for now @@ -20,15 +23,45 @@ func main() { } relationType := strings.Replace(mediaRelation.Attributes.Role, "_", " ", -1) + animeID := mediaRelation.Relationships.Source.Data.ID + destinationAnimeID := mediaRelation.Relationships.Destination.Data.ID fmt.Printf( - "%s %s has a %s which is %s %s\n", + "%s %s has %s which is %s %s\n", mediaRelation.Relationships.Source.Data.Type, - mediaRelation.Relationships.Source.Data.ID, + animeID, color.GreenString(relationType), mediaRelation.Relationships.Destination.Data.Type, - mediaRelation.Relationships.Destination.Data.ID, + destinationAnimeID, ) + + relationsList, found := relations[animeID] + + if !found { + relationsList = &arn.AnimeRelations{ + AnimeID: animeID, + Items: []*arn.AnimeRelation{}, + } + relations[animeID] = relationsList + } + + relationsList.Items = append(relationsList.Items, &arn.AnimeRelation{ + AnimeID: destinationAnimeID, + Type: relationType, + }) + + // for _, item := range relationsList.Items { + // fmt.Println("*", item.Type, item.AnimeID) + // } + } + + // Save relations map + for _, animeRelations := range relations { + err := animeRelations.Save() + + if err != nil { + color.Red(err.Error()) + } } color.Green("Finished.") From 07cb7dd64e1bf7b74e3323a5807d56ab7bca9b56 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 16:58:28 +0200 Subject: [PATCH 445/527] Implemented anime relations --- .../sync-media-relations.go | 14 ++++ pages/anime/anime.go | 12 ++++ pages/anime/anime.pixy | 70 +++++++++++-------- pages/anime/anime.scarlet | 12 +++- pages/anime/relation.scarlet | 24 +++++++ pages/profile/watching.scarlet | 4 +- pages/soundtrack/soundtrack.scarlet | 4 +- styles/include/mixins.scarlet | 5 +- 8 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 pages/anime/relation.scarlet diff --git a/jobs/sync-media-relations/sync-media-relations.go b/jobs/sync-media-relations/sync-media-relations.go index bd7a5ecd..71206674 100644 --- a/jobs/sync-media-relations/sync-media-relations.go +++ b/jobs/sync-media-relations/sync-media-relations.go @@ -26,6 +26,19 @@ func main() { animeID := mediaRelation.Relationships.Source.Data.ID destinationAnimeID := mediaRelation.Relationships.Destination.Data.ID + // Confirm that the anime IDs are valid + exists, _ := arn.DB.Exists("Anime", animeID) + + if !exists { + continue + } + + exists, _ = arn.DB.Exists("Anime", destinationAnimeID) + + if !exists { + continue + } + fmt.Printf( "%s %s has %s which is %s %s\n", mediaRelation.Relationships.Source.Data.Type, @@ -35,6 +48,7 @@ func main() { destinationAnimeID, ) + // Add anime to the global map relationsList, found := relations[animeID] if !found { diff --git a/pages/anime/anime.go b/pages/anime/anime.go index d256057e..308a09c0 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -2,6 +2,7 @@ package anime import ( "net/http" + "sort" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -66,6 +67,17 @@ func Get(ctx *aero.Context) string { arn.SortUsersLastSeen(friends) } + // Sort relations by start date + relations := anime.Relations() + + if relations != nil { + items := relations.Items + + sort.Slice(items, func(i, j int) bool { + return items[i].Anime().StartDate < items[j].Anime().StartDate + }) + } + // Open Graph description := anime.Summary diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 647087c8..15d77ac2 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -3,6 +3,9 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* if anime.Image.Small != "" .anime-image-container img.anime-cover-image(src=anime.Image.Small, alt=anime.Title.Canonical) + + if anime.StartDate != "" + .anime-start-date(title="Start date: " + anime.StartDate)= anime.StartDate[:4] .space @@ -35,21 +38,6 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* Icon("plus") span Add to collection - if len(friends) > 0 - h3.anime-section-name Friends - - .anime-friends - .user-avatars - each friend in friends - if friend.Nick != "" - if friend.IsActive() - .mountable - FriendEntry(friend, listItems) - else - .mountable - .inactive-user - FriendEntry(friend, listItems) - h3.anime-section-name Ratings .anime-rating-categories .anime-rating-category(title=toString(anime.Rating.Overall)) @@ -68,6 +56,32 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* .anime-rating-category-name Soundtrack Rating(anime.Rating.Soundtrack) + if len(friends) > 0 + h3.anime-section-name Friends + + .anime-friends + .user-avatars + each friend in friends + if friend.Nick != "" + if friend.IsActive() + .mountable + FriendEntry(friend, listItems) + else + .mountable + .inactive-user + FriendEntry(friend, listItems) + + if anime.Relations() != nil && len(anime.Relations().Items) > 0 + h3.anime-section-name Relations + .anime-relations + each relation in anime.Relations().Items + a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.Canonical) + img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.Canonical) + .anime-relation-type= relation.HumanReadableType() + .anime-relation-year + if relation.Anime().StartDate != "" + span= relation.Anime().StartDate[:4] + if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" h3.anime-section-name Video .anime-trailer.video-container @@ -156,19 +170,6 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* //- if providers.Nyaa && providers.Nyaa.episodes !== undefined //- span(class=providers.Nyaa.episodes === 0 ? "entry-error" : "entry-ok")= providers.Nyaa.episodes + " eps" - if len(tracks) > 0 - h3.anime-section-name Tracks - .sound-tracks - each track in tracks - SoundTrack(track) - - if anime.Characters() != nil && len(anime.Characters().Items) > 0 - h3.anime-section-name Characters - .characters - each character in anime.Characters().Items - if character.Character() != nil - Character(character.Character()) - h3.anime-section-name Popularity .anime-rating-categories .anime-rating-category @@ -187,6 +188,19 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* .anime-rating-category-name Dropped .anime-rating= anime.Popularity.Dropped + if len(tracks) > 0 + h3.anime-section-name Tracks + .sound-tracks + each track in tracks + SoundTrack(track) + + if anime.Characters() != nil && len(anime.Characters().Items) > 0 + h3.anime-section-name Characters + .characters + each character in anime.Characters().Items + if character.Character() != nil + Character(character.Character()) + if len(anime.Episodes().Items) > 0 if episodesReversed h3.anime-section-name Latest episodes diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 117b3536..a7635f19 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -9,9 +9,15 @@ font-weight bold .anime-image-container - horizontal - justify-content center - align-items flex-start + vertical + justify-content flex-start + align-items center + +.anime-start-date + font-size 0.7rem + line-height 1.7em + opacity 0.65 + margin-top 0.5rem .anime-cover-image width 142px diff --git a/pages/anime/relation.scarlet b/pages/anime/relation.scarlet new file mode 100644 index 00000000..aab9872c --- /dev/null +++ b/pages/anime/relation.scarlet @@ -0,0 +1,24 @@ +.anime-relations + horizontal-wrap + +.anime-relation + anime-mini-item + vertical + +.anime-relation-image + anime-mini-item-image + +.anime-relation-type, +.anime-relation-year + font-size 0.7rem + line-height 1.7em + text-align center + color text-color + opacity 0.8 + +.anime-relation-type + margin-top 0.2rem + +.anime-relation-year + font-size 0.6rem + opacity 0.65 \ No newline at end of file diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 3cd77f89..82b2bcd3 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -3,7 +3,7 @@ justify-content center .profile-watching-list-item - margin 0.25rem + anime-mini-item .profile-watching-list-item-image - anime-cover-image-mini \ No newline at end of file + anime-mini-item-image \ No newline at end of file diff --git a/pages/soundtrack/soundtrack.scarlet b/pages/soundtrack/soundtrack.scarlet index 692df7bc..a1ad6dc2 100644 --- a/pages/soundtrack/soundtrack.scarlet +++ b/pages/soundtrack/soundtrack.scarlet @@ -9,7 +9,7 @@ horizontal-wrap .sound-track-anime-list-item - // + anime-mini-item .sound-track-anime-list-item-image - anime-cover-image-mini \ No newline at end of file + anime-mini-item-image \ No newline at end of file diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 417c1f49..1249b597 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -42,7 +42,10 @@ mixin clip-long-text white-space nowrap text-overflow ellipsis -mixin anime-cover-image-mini +mixin anime-mini-item + margin 0.25rem + +mixin anime-mini-item-image width 55px !important height 78px !important border-radius 2px From c24d954f9ee55afb5afd24f8d6b7bfab8c732af3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 17:18:06 +0200 Subject: [PATCH 446/527] Added end date --- pages/anime/anime.pixy | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 15d77ac2..ec72f8c6 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -5,7 +5,11 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* img.anime-cover-image(src=anime.Image.Small, alt=anime.Title.Canonical) if anime.StartDate != "" - .anime-start-date(title="Start date: " + anime.StartDate)= anime.StartDate[:4] + .anime-start-date + span(title="Start date: " + anime.StartDate)= anime.StartDate[:4] + if anime.EndDate != "" && anime.StartDate[:4] != anime.EndDate[:4] + span - + span(title="End date: " + anime.EndDate)= anime.EndDate[:4] .space From e9b6cb3ac804526ab840c28fe4e30752f677d5fa Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 18:16:44 +0200 Subject: [PATCH 447/527] Ctrl + comma to open settings --- scripts/AnimeNotifier.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 23605e29..bbdce8fe 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -816,5 +816,14 @@ export class AnimeNotifier { e.stopPropagation() return } + + // Ctrl + , = Settings + if(e.ctrlKey && e.keyCode == 188) { + this.app.load("/settings") + + e.preventDefault() + e.stopPropagation() + return + } } } \ No newline at end of file From 63a421e6a28c01744db2969dde8453677f11e542 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 17 Oct 2017 23:17:04 +0200 Subject: [PATCH 448/527] Implemented groups --- main.go | 9 +- mixins/Sidebar.pixy | 3 + pages/group/edit.go | 32 +++++++ pages/group/group.go | 29 +++++++ pages/group/group.pixy | 15 ++++ pages/groups/groups.go | 25 ++++++ pages/groups/groups.pixy | 21 +++++ pages/newsoundtrack/newsoundtrack.go | 20 ----- pages/newsoundtrack/newsoundtrack.pixy | 25 ------ pages/soundtrack/edit.go | 101 +--------------------- pages/soundtracks/soundtracks.pixy | 2 +- scripts/Actions.ts | 3 +- scripts/Actions/{Delete.ts => Object.ts} | 10 +++ scripts/Actions/SoundTrack.ts | 9 -- utils/editform/editform.go | 102 +++++++++++++++++++++++ 15 files changed, 248 insertions(+), 158 deletions(-) create mode 100644 pages/group/edit.go create mode 100644 pages/group/group.go create mode 100644 pages/group/group.pixy create mode 100644 pages/groups/groups.go create mode 100644 pages/groups/groups.pixy delete mode 100644 pages/newsoundtrack/newsoundtrack.go delete mode 100644 pages/newsoundtrack/newsoundtrack.pixy rename scripts/Actions/{Delete.ts => Object.ts} (61%) delete mode 100644 scripts/Actions/SoundTrack.ts create mode 100644 utils/editform/editform.go diff --git a/main.go b/main.go index 44437560..ad60a955 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,8 @@ import ( "github.com/animenotifier/notify.moe/pages/explore" "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" + "github.com/animenotifier/notify.moe/pages/group" + "github.com/animenotifier/notify.moe/pages/groups" "github.com/animenotifier/notify.moe/pages/home" "github.com/animenotifier/notify.moe/pages/inventory" "github.com/animenotifier/notify.moe/pages/listimport" @@ -33,7 +35,6 @@ import ( "github.com/animenotifier/notify.moe/pages/listimport/listimportmyanimelist" "github.com/animenotifier/notify.moe/pages/login" "github.com/animenotifier/notify.moe/pages/me" - "github.com/animenotifier/notify.moe/pages/newsoundtrack" "github.com/animenotifier/notify.moe/pages/newthread" "github.com/animenotifier/notify.moe/pages/notifications" "github.com/animenotifier/notify.moe/pages/paypal" @@ -99,10 +100,14 @@ func configure(app *aero.Application) *aero.Application { // Soundtracks app.Ajax("/soundtracks", soundtracks.Get) app.Ajax("/soundtracks/from/:index", soundtracks.From) - app.Ajax("/new/soundtrack", newsoundtrack.Get) app.Ajax("/soundtrack/:id", soundtrack.Get) app.Ajax("/soundtrack/:id/edit", soundtrack.Edit) + // Groups + app.Ajax("/groups", groups.Get) + app.Ajax("/group/:id", group.Get) + app.Ajax("/group/:id/edit", group.Edit) + // User profiles app.Ajax("/user", user.Get) app.Ajax("/user/:nick", profile.Get) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 0a53a96b..237f062b 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -21,6 +21,9 @@ component Sidebar(user *arn.User) //- SidebarButton("Search", "/search", "search") if user != nil + if user.Role == "admin" + SidebarButton("Groups", "/groups", "users") + SidebarButton("Shop", "/shop", "shopping-cart") SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") diff --git a/pages/group/edit.go b/pages/group/edit.go new file mode 100644 index 00000000..efaa21e0 --- /dev/null +++ b/pages/group/edit.go @@ -0,0 +1,32 @@ +package group + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" + "github.com/animenotifier/notify.moe/utils/editform" +) + +// Edit ... +func Edit(ctx *aero.Context) string { + id := ctx.Get("id") + group, err := arn.GetGroup(id) + user := utils.GetUser(ctx) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Track not found", err) + } + + ctx.Data = &arn.OpenGraph{ + Tags: map[string]string{ + "og:title": group.Name, + "og:url": "https://" + ctx.App.Config.Domain + group.Link(), + "og:site_name": "notify.moe", + }, + } + + return ctx.HTML(components.GroupTabs(group) + editform.Render(group, "Edit group", user)) +} diff --git a/pages/group/group.go b/pages/group/group.go new file mode 100644 index 00000000..5946a495 --- /dev/null +++ b/pages/group/group.go @@ -0,0 +1,29 @@ +package group + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +// Get ... +func Get(ctx *aero.Context) string { + id := ctx.Get("id") + group, err := arn.GetGroup(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Group not found", err) + } + + ctx.Data = &arn.OpenGraph{ + Tags: map[string]string{ + "og:title": group.Name, + "og:url": "https://" + ctx.App.Config.Domain + group.Link(), + "og:site_name": "notify.moe", + }, + } + + return ctx.HTML(components.Group(group)) +} diff --git a/pages/group/group.pixy b/pages/group/group.pixy new file mode 100644 index 00000000..d4c29012 --- /dev/null +++ b/pages/group/group.pixy @@ -0,0 +1,15 @@ +component Group(group *arn.Group) + GroupTabs(group) + + if group.Name != "" + h1= group.Name + else + h1 untitled + + p= len(group.Members) + p= group.CreatedBy + +component GroupTabs(group *arn.Group) + .tabs + Tab("Group", "users", group.Link()) + Tab("Edit", "pencil", group.Link() + "/edit") \ No newline at end of file diff --git a/pages/groups/groups.go b/pages/groups/groups.go new file mode 100644 index 00000000..eb364d9d --- /dev/null +++ b/pages/groups/groups.go @@ -0,0 +1,25 @@ +package groups + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get ... +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + groups, err := arn.FilterGroups(func(group *arn.Group) bool { + return !group.IsDraft + }) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching groups", err) + } + + return ctx.HTML(components.Groups(groups, user)) +} diff --git a/pages/groups/groups.pixy b/pages/groups/groups.pixy new file mode 100644 index 00000000..6b6c8ad8 --- /dev/null +++ b/pages/groups/groups.pixy @@ -0,0 +1,21 @@ +component Groups(groups []*arn.Group, user *arn.User) + .tabs + Tab("Groups", "users", "/groups") + + h1.page-title Groups + + .buttons + if user != nil + if user.DraftIndex().GroupID == "" + button.action(data-action="newObject", data-trigger="click", data-type="group") + Icon("plus") + span New group + else + a.button.ajax(href="/group/" + user.DraftIndex().GroupID + "/edit") + Icon("pencil") + span Edit draft + + .groups + each group in groups + .group + h3= group.Name \ No newline at end of file diff --git a/pages/newsoundtrack/newsoundtrack.go b/pages/newsoundtrack/newsoundtrack.go deleted file mode 100644 index 5a3a7fb2..00000000 --- a/pages/newsoundtrack/newsoundtrack.go +++ /dev/null @@ -1,20 +0,0 @@ -package newsoundtrack - -import ( - "net/http" - - "github.com/aerogo/aero" - "github.com/animenotifier/notify.moe/components" - "github.com/animenotifier/notify.moe/utils" -) - -// Get forums page. -func Get(ctx *aero.Context) string { - user := utils.GetUser(ctx) - - if user == nil { - return ctx.Error(http.StatusBadRequest, "Not logged in", nil) - } - - return ctx.HTML(components.NewSoundTrack(user)) -} diff --git a/pages/newsoundtrack/newsoundtrack.pixy b/pages/newsoundtrack/newsoundtrack.pixy deleted file mode 100644 index 80dbd8cd..00000000 --- a/pages/newsoundtrack/newsoundtrack.pixy +++ /dev/null @@ -1,25 +0,0 @@ -component NewSoundTrack(user *arn.User) - .widget-form - .widget - h1 New soundtrack - - .widget-section - label(for="soundcloud-link") Soundcloud link: - input#soundcloud-link.widget-ui-element(type="text", placeholder="https://soundcloud.com/abc/123") - - .widget-section - label(for="youtube-link") Youtube link: - input#youtube-link.widget-ui-element(type="text", placeholder="https://www.youtube.com/watch?v=123") - - .widget-section - label(for="anime-link") Anime link: - input#anime-link.widget-ui-element(type="text", placeholder="https://notify.moe/anime/123") - - .widget-section - label(for="osu-link") Osu beatmap (optional): - input#osu-link.widget-ui-element(type="text", placeholder="https://osu.ppy.sh/s/123") - - .buttons - button.action(data-action="createSoundTrack", data-trigger="click") - Icon("check") - span Add soundtrack \ No newline at end of file diff --git a/pages/soundtrack/edit.go b/pages/soundtrack/edit.go index 59c06a23..f1ced793 100644 --- a/pages/soundtrack/edit.go +++ b/pages/soundtrack/edit.go @@ -1,15 +1,11 @@ package soundtrack import ( - "bytes" - "fmt" "net/http" - "reflect" - "strconv" - "strings" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" + "github.com/animenotifier/notify.moe/utils/editform" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -38,98 +34,5 @@ func Edit(ctx *aero.Context) string { ctx.Data.(*arn.OpenGraph).Tags["og:image"] = track.MainAnime().Image.Large } - return ctx.HTML(components.SoundTrackTabs(track) + EditForm(track, "Edit soundtrack", user)) -} - -// EditForm ... -func EditForm(obj interface{}, title string, user *arn.User) string { - t := reflect.TypeOf(obj).Elem() - v := reflect.ValueOf(obj).Elem() - id := reflect.Indirect(v.FieldByName("ID")) - lowerCaseTypeName := strings.ToLower(t.Name()) - endpoint := `/api/` + lowerCaseTypeName + `/` + id.String() - - var b bytes.Buffer - - b.WriteString(`
    `) - b.WriteString(`
    `) - - b.WriteString(`

    `) - b.WriteString(title) - b.WriteString(`

    `) - - RenderObject(&b, obj, "") - - if user != nil && (user.Role == "editor" || user.Role == "admin") { - b.WriteString(`
    `) - b.WriteString(``) - b.WriteString(`
    `) - } - - b.WriteString("
    ") - b.WriteString("
    ") - - return b.String() -} - -// RenderObject ... -func RenderObject(b *bytes.Buffer, obj interface{}, idPrefix string) { - t := reflect.TypeOf(obj).Elem() - v := reflect.ValueOf(obj).Elem() - - // Fields - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - RenderField(b, &v, field, idPrefix) - } -} - -// RenderField ... -func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, idPrefix string) { - if field.Anonymous || field.Tag.Get("editable") != "true" { - return - } - - fieldValue := reflect.Indirect(v.FieldByName(field.Name)) - - switch field.Type.String() { - case "string": - b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, "")) - case "[]string": - b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name, field.Tag.Get("tooltip"))) - case "bool": - if field.Name == "IsDraft" { - if fieldValue.Bool() { - b.WriteString(`
    `) - } - // else { - // b.WriteString(`
    `) - // } - } - case "[]*arn.ExternalMedia": - for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { - b.WriteString(`
    `) - b.WriteString(`
    ` + strconv.Itoa(sliceIndex+1) + ". " + field.Name + `
    `) - - arrayObj := fieldValue.Index(sliceIndex).Interface() - arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex) - RenderObject(b, arrayObj, arrayIDPrefix) - - // Preview - b.WriteString(components.ExternalMedia(fieldValue.Index(sliceIndex).Interface().(*arn.ExternalMedia))) - - // Remove button - b.WriteString(`
    `) - - b.WriteString(`
    `) - } - - b.WriteString(`
    `) - b.WriteString(``) - b.WriteString(`
    `) - default: - panic("No edit form implementation for " + idPrefix + field.Name + " with type " + field.Type.String()) - } + return ctx.HTML(components.SoundTrackTabs(track) + editform.Render(track, "Edit soundtrack", user)) } diff --git a/pages/soundtracks/soundtracks.pixy b/pages/soundtracks/soundtracks.pixy index e05647d1..a219f5d7 100644 --- a/pages/soundtracks/soundtracks.pixy +++ b/pages/soundtracks/soundtracks.pixy @@ -4,7 +4,7 @@ component SoundTracks(tracks []*arn.SoundTrack, tracksPerPage int, user *arn.Use .music-buttons if user != nil if user.DraftIndex().SoundTrackID == "" - button.action(data-action="newSoundTrack", data-trigger="click") + button.action(data-action="newObject", data-trigger="click", data-type="soundtrack") Icon("plus") span Add soundtrack else diff --git a/scripts/Actions.ts b/scripts/Actions.ts index 71138565..b5e4969a 100644 --- a/scripts/Actions.ts +++ b/scripts/Actions.ts @@ -1,5 +1,4 @@ export * from "./Actions/AnimeList" -export * from "./Actions/Delete" export * from "./Actions/Diff" export * from "./Actions/FollowUser" export * from "./Actions/Forum" @@ -7,10 +6,10 @@ export * from "./Actions/InfiniteScroller" export * from "./Actions/Install" export * from "./Actions/Like" export * from "./Actions/Notifications" +export * from "./Actions/Object" export * from "./Actions/Publish" export * from "./Actions/Search" export * from "./Actions/Serialization" export * from "./Actions/Shop" export * from "./Actions/SideBar" -export * from "./Actions/SoundTrack" export * from "./Actions/StatusMessage" \ No newline at end of file diff --git a/scripts/Actions/Delete.ts b/scripts/Actions/Object.ts similarity index 61% rename from scripts/Actions/Delete.ts rename to scripts/Actions/Object.ts index 23559b91..7dac7d59 100644 --- a/scripts/Actions/Delete.ts +++ b/scripts/Actions/Object.ts @@ -1,5 +1,15 @@ import { AnimeNotifier } from "../AnimeNotifier" +// New +export function newObject(arn: AnimeNotifier, button: HTMLButtonElement) { + let dataType = button.dataset.type + + arn.post(`/api/new/${dataType}`, "") + .then(response => response.json()) + .then(obj => arn.app.load(`/${dataType}/${obj.id}/edit`)) + .catch(err => arn.statusMessage.showError(err)) +} + // Delete export function deleteObject(arn: AnimeNotifier, button: HTMLButtonElement) { let confirmType = button.dataset.confirmType diff --git a/scripts/Actions/SoundTrack.ts b/scripts/Actions/SoundTrack.ts deleted file mode 100644 index 1e435414..00000000 --- a/scripts/Actions/SoundTrack.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AnimeNotifier } from "../AnimeNotifier" - -// New soundtrack -export function newSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement) { - arn.post("/api/new/soundtrack", "") - .then(response => response.json()) - .then(track => arn.app.load(`/soundtrack/${track.id}/edit`)) - .catch(err => arn.statusMessage.showError(err)) -} \ No newline at end of file diff --git a/utils/editform/editform.go b/utils/editform/editform.go new file mode 100644 index 00000000..c3a1c736 --- /dev/null +++ b/utils/editform/editform.go @@ -0,0 +1,102 @@ +package editform + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Render ... +func Render(obj interface{}, title string, user *arn.User) string { + t := reflect.TypeOf(obj).Elem() + v := reflect.ValueOf(obj).Elem() + id := reflect.Indirect(v.FieldByName("ID")) + lowerCaseTypeName := strings.ToLower(t.Name()) + endpoint := `/api/` + lowerCaseTypeName + `/` + id.String() + + var b bytes.Buffer + + b.WriteString(`
    `) + b.WriteString(`
    `) + + b.WriteString(`

    `) + b.WriteString(title) + b.WriteString(`

    `) + + RenderObject(&b, obj, "") + + if user != nil && (user.Role == "editor" || user.Role == "admin") { + b.WriteString(`
    `) + b.WriteString(`
    `) + b.WriteString(``) + b.WriteString(`
    `) + } + + b.WriteString("
    ") + b.WriteString("
    ") + + return b.String() +} + +// RenderObject ... +func RenderObject(b *bytes.Buffer, obj interface{}, idPrefix string) { + t := reflect.TypeOf(obj).Elem() + v := reflect.ValueOf(obj).Elem() + + // Fields + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + RenderField(b, &v, field, idPrefix) + } +} + +// RenderField ... +func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, idPrefix string) { + if field.Anonymous || field.Tag.Get("editable") != "true" { + return + } + + fieldValue := reflect.Indirect(v.FieldByName(field.Name)) + + switch field.Type.String() { + case "string": + b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, "")) + case "[]string": + b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name, field.Tag.Get("tooltip"))) + case "bool": + if field.Name == "IsDraft" { + return + } + case "[]*arn.ExternalMedia": + for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { + b.WriteString(`
    `) + b.WriteString(`
    ` + strconv.Itoa(sliceIndex+1) + ". " + field.Name + `
    `) + + arrayObj := fieldValue.Index(sliceIndex).Interface() + arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex) + RenderObject(b, arrayObj, arrayIDPrefix) + + // Preview + b.WriteString(components.ExternalMedia(fieldValue.Index(sliceIndex).Interface().(*arn.ExternalMedia))) + + // Remove button + b.WriteString(`
    `) + + b.WriteString(`
    `) + } + + b.WriteString(`
    `) + b.WriteString(``) + b.WriteString(`
    `) + default: + panic("No edit form implementation for " + idPrefix + field.Name + " with type " + field.Type.String()) + } +} From 217e35f752f5b37b22b608f0e9d9f0ea2ac88124 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 18 Oct 2017 12:02:48 +0200 Subject: [PATCH 449/527] Added group styles --- pages/groups/groups.pixy | 6 +++++- pages/groups/groups.scarlet | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 pages/groups/groups.scarlet diff --git a/pages/groups/groups.pixy b/pages/groups/groups.pixy index 6b6c8ad8..d1c49e6f 100644 --- a/pages/groups/groups.pixy +++ b/pages/groups/groups.pixy @@ -18,4 +18,8 @@ component Groups(groups []*arn.Group, user *arn.User) .groups each group in groups .group - h3= group.Name \ No newline at end of file + h3.group-name= group.Name + .group-tagline= group.Tagline + .group-member-count + Icon("user") + span= len(group.Members) \ No newline at end of file diff --git a/pages/groups/groups.scarlet b/pages/groups/groups.scarlet new file mode 100644 index 00000000..ec9a5d90 --- /dev/null +++ b/pages/groups/groups.scarlet @@ -0,0 +1,27 @@ +.groups + horizontal-wrap + justify-content space-around + +.group + vertical + ui-element + position relative + width 100% + max-width 500px + padding 0.5rem 1rem + margin calc(content-padding / 2) + +.group-name + horizontal + align-items center + +.group-tagline + opacity 0.6 + +.group-member-count + position absolute + top 0.5rem + right 1rem + text-align right + font-size 0.8rem + opacity 0.5 \ No newline at end of file From 6a92dc23bbae0a5a973bd4bf6f901eb38a01814e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 18 Oct 2017 18:18:15 +0200 Subject: [PATCH 450/527] Improved groups --- jobs/refresh-osu/refresh-osu.go | 10 ++++++++-- mixins/SoundTrack.pixy | 2 +- pages/groups/groups.go | 17 ++++++++++++++++- pages/groups/groups.pixy | 22 ++++++++++++++-------- pages/groups/groups.scarlet | 23 ++++++++++++++++++++--- pages/soundtrack/soundtrack.pixy | 2 +- styles/include/config.scarlet | 1 + styles/include/mixins.scarlet | 2 +- 8 files changed, 62 insertions(+), 17 deletions(-) diff --git a/jobs/refresh-osu/refresh-osu.go b/jobs/refresh-osu/refresh-osu.go index e2ac7d19..10568a66 100644 --- a/jobs/refresh-osu/refresh-osu.go +++ b/jobs/refresh-osu/refresh-osu.go @@ -12,11 +12,17 @@ func main() { ticker := time.NewTicker(500 * time.Millisecond) - for user := range arn.MustStreamUsers() { + allUsers, _ := arn.AllUsers() + + for _, user := range allUsers { // Get osu info if user.RefreshOsuInfo() == nil { arn.PrettyPrint(user.Accounts.Osu) - user.Save() + + // Fetch user again to prevent writing old data + updatedUser, _ := arn.GetUser(user.ID) + updatedUser.Accounts.Osu = user.Accounts.Osu + updatedUser.Save() } // Wait for rate limiter diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index 2494b28a..fa96421a 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -27,7 +27,7 @@ component SoundTrackFooter(track *arn.SoundTrack) span posted span.utc-date(data-date=track.Created) span by - a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick + " " + a.ajax(href=track.Creator().Link())= track.Creator().Nick + " " component ExternalMedia(media *arn.ExternalMedia) iframe.lazy(data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") \ No newline at end of file diff --git a/pages/groups/groups.go b/pages/groups/groups.go index eb364d9d..04082165 100644 --- a/pages/groups/groups.go +++ b/pages/groups/groups.go @@ -2,6 +2,7 @@ package groups import ( "net/http" + "sort" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -9,6 +10,8 @@ import ( "github.com/animenotifier/notify.moe/utils" ) +const groupsPerPage = 12 + // Get ... func Get(ctx *aero.Context) string { user := utils.GetUser(ctx) @@ -17,9 +20,21 @@ func Get(ctx *aero.Context) string { return !group.IsDraft }) + sort.Slice(groups, func(i, j int) bool { + if len(groups[i].Members) == len(groups[j].Members) { + return groups[i].Created > groups[j].Created + } + + return len(groups[i].Members) > len(groups[j].Members) + }) + + if len(groups) > groupsPerPage { + groups = groups[:groupsPerPage] + } + if err != nil { return ctx.Error(http.StatusInternalServerError, "Error fetching groups", err) } - return ctx.HTML(components.Groups(groups, user)) + return ctx.HTML(components.Groups(groups, groupsPerPage, user)) } diff --git a/pages/groups/groups.pixy b/pages/groups/groups.pixy index d1c49e6f..de4c9e74 100644 --- a/pages/groups/groups.pixy +++ b/pages/groups/groups.pixy @@ -1,4 +1,4 @@ -component Groups(groups []*arn.Group, user *arn.User) +component Groups(groups []*arn.Group, groupsPerPage int, user *arn.User) .tabs Tab("Groups", "users", "/groups") @@ -15,11 +15,17 @@ component Groups(groups []*arn.Group, user *arn.User) Icon("pencil") span Edit draft - .groups + #load-more-target.groups each group in groups - .group - h3.group-name= group.Name - .group-tagline= group.Tagline - .group-member-count - Icon("user") - span= len(group.Members) \ No newline at end of file + a.group.mountable.ajax(href=group.Link()) + img.group-image.lazy(data-src=group.ImageURL(), alt=group.Name) + .group-info + h3.group-name= group.Name + .group-tagline= group.Tagline + .group-member-count + Icon("user") + span= len(group.Members) + + if len(groups) == groupsPerPage + .buttons + LoadMore(groupsPerPage) \ No newline at end of file diff --git a/pages/groups/groups.scarlet b/pages/groups/groups.scarlet index ec9a5d90..15f9184d 100644 --- a/pages/groups/groups.scarlet +++ b/pages/groups/groups.scarlet @@ -1,15 +1,32 @@ +group-padding-y = 0.75rem +group-padding-x = 0.75rem + .groups horizontal-wrap justify-content space-around .group - vertical + horizontal ui-element position relative width 100% - max-width 500px - padding 0.5rem 1rem + max-width 520px + padding group-padding-y group-padding-x margin calc(content-padding / 2) + color text-color + + :hover + color white + background-color rgb(60, 60, 60) + +.group-image + width 70px + height 70px + margin-right 1rem + border-radius ui-element-border-radius + +.group-info + vertical .group-name horizontal diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index cbf7f071..b695b439 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -38,7 +38,7 @@ component Track(track *arn.SoundTrack) span Posted span.utc-date(data-date=track.Created) span by - span= track.CreatedByUser().Nick + span= track.Creator().Nick span . diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 0da623d1..3e60fc87 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -17,6 +17,7 @@ ui-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-element-border-radius = 3px // Input input-focus-border-color = rgb(248, 165, 130) diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 1249b597..19d133db 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -26,7 +26,7 @@ mixin noise-strong mixin ui-element border 1px solid ui-border-color background ui-background - border-radius 3px + border-radius ui-element-border-radius default-transition :hover border-color ui-hover-border-color From 3f27562581bf91c9d6ff44942eda2f7988530098 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 18 Oct 2017 21:18:10 +0200 Subject: [PATCH 451/527] Improved groups --- pages/group/group.pixy | 24 ++++++++++++++++++++---- pages/group/group.scarlet | 23 +++++++++++++++++++++++ utils/editform/editform.go | 6 +++++- 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 pages/group/group.scarlet diff --git a/pages/group/group.pixy b/pages/group/group.pixy index d4c29012..48140f4a 100644 --- a/pages/group/group.pixy +++ b/pages/group/group.pixy @@ -2,12 +2,28 @@ component Group(group *arn.Group) GroupTabs(group) if group.Name != "" - h1= group.Name + h1.mountable= group.Name else - h1 untitled + h1.mountable untitled - p= len(group.Members) - p= group.CreatedBy + .group-view + .group-sidebar.mountable + .group-sidebar-section + h3 Description + .group-description!= markdown.Render(group.Description) + + .group-sidebar-section + h3 Rules + .group-rules!= markdown.Render(group.Rules) + + .group-sidebar-section + h3 Members + .user-avatars.group-members + each member in group.Members + Avatar(member.User()) + + .group-feed.mountable + p Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin fermentum tellus congue, placerat augue vel, porta tortor. Nunc in elementum enim. Vestibulum ut arcu sed diam dapibus feugiat. Nam posuere, lectus et pellentesque interdum, mi orci aliquet lacus, a posuere lacus mi ac urna. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec suscipit enim nec dui consectetur, vitae pulvinar urna commodo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. component GroupTabs(group *arn.Group) .tabs diff --git a/pages/group/group.scarlet b/pages/group/group.scarlet new file mode 100644 index 00000000..b273cb2e --- /dev/null +++ b/pages/group/group.scarlet @@ -0,0 +1,23 @@ +.group-view + horizontal-wrap + width 100% + +< 1100px + .group-view + vertical + +.group-feed + flex 0.75 + padding 1rem + +.group-sidebar + flex 0.25 + +.group-sidebar-section + ui-element + padding 0.5rem 1rem + margin-bottom content-padding + +.group-members + margin-bottom 0.5rem + justify-content flex-start \ No newline at end of file diff --git a/utils/editform/editform.go b/utils/editform/editform.go index c3a1c736..37ad9931 100644 --- a/utils/editform/editform.go +++ b/utils/editform/editform.go @@ -66,7 +66,11 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i switch field.Type.String() { case "string": - b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, "")) + if field.Tag.Get("type") == "textarea" { + b.WriteString(components.InputTextArea(idPrefix+field.Name, fieldValue.String(), field.Name, "")) + } else { + b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, "")) + } case "[]string": b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name, field.Tag.Get("tooltip"))) case "bool": From 677dc4518462c69cd8a288d85dd5f319cadbbb76 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 18 Oct 2017 22:09:17 +0200 Subject: [PATCH 452/527] Added video to soundtrack OpenGraph --- pages/group/group.pixy | 14 ++++++++------ pages/soundtrack/soundtrack.go | 12 ++++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pages/group/group.pixy b/pages/group/group.pixy index 48140f4a..434f3913 100644 --- a/pages/group/group.pixy +++ b/pages/group/group.pixy @@ -8,13 +8,15 @@ component Group(group *arn.Group) .group-view .group-sidebar.mountable - .group-sidebar-section - h3 Description - .group-description!= markdown.Render(group.Description) + if group.Description != "" + .group-sidebar-section + h3 Description + .group-description!= markdown.Render(group.Description) - .group-sidebar-section - h3 Rules - .group-rules!= markdown.Render(group.Rules) + if group.Rules != "" + .group-sidebar-section + h3 Rules + .group-rules!= markdown.Render(group.Rules) .group-sidebar-section h3 Members diff --git a/pages/soundtrack/soundtrack.go b/pages/soundtrack/soundtrack.go index 5a5c8e75..416090b2 100644 --- a/pages/soundtrack/soundtrack.go +++ b/pages/soundtrack/soundtrack.go @@ -17,7 +17,7 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Track not found", err) } - ctx.Data = &arn.OpenGraph{ + openGraph := &arn.OpenGraph{ Tags: map[string]string{ "og:title": track.Title, "og:url": "https://" + ctx.App.Config.Domain + track.Link(), @@ -27,8 +27,16 @@ func Get(ctx *aero.Context) string { } if track.MainAnime() != nil { - ctx.Data.(*arn.OpenGraph).Tags["og:image"] = track.MainAnime().Image.Large + openGraph.Tags["og:image"] = track.MainAnime().Image.Large } + // Set video so that it can be played + youtube := track.MediaByName("Youtube") + if len(youtube) > 0 { + openGraph.Tags["og:video"] = "https://www.youtube.com/v/" + youtube[0].ServiceID + } + + ctx.Data = openGraph + return ctx.HTML(components.Track(track)) } From 15267bd102e3ba52876371a564c3cfc49582aad3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 18 Oct 2017 22:28:47 +0200 Subject: [PATCH 453/527] Fixed soundtrack video OpenGraph --- pages/soundtrack/soundtrack.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pages/soundtrack/soundtrack.go b/pages/soundtrack/soundtrack.go index 416090b2..cba2f1d8 100644 --- a/pages/soundtrack/soundtrack.go +++ b/pages/soundtrack/soundtrack.go @@ -31,7 +31,8 @@ func Get(ctx *aero.Context) string { } // Set video so that it can be played - youtube := track.MediaByName("Youtube") + youtube := track.MediaByService("Youtube") + if len(youtube) > 0 { openGraph.Tags["og:video"] = "https://www.youtube.com/v/" + youtube[0].ServiceID } From d72543b31edf277eaafeb52254c5c72992bd9bd0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 18 Oct 2017 23:17:54 +0200 Subject: [PATCH 454/527] Minor changes --- main.go | 1 + pages/group/forum.go | 21 +++++++++++++++++++++ pages/group/forum.pixy | 4 ++++ pages/group/group.pixy | 1 + utils/editform/editform.go | 2 +- 5 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 pages/group/forum.go create mode 100644 pages/group/forum.pixy diff --git a/main.go b/main.go index ad60a955..a2a9dcf8 100644 --- a/main.go +++ b/main.go @@ -107,6 +107,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/groups", groups.Get) app.Ajax("/group/:id", group.Get) app.Ajax("/group/:id/edit", group.Edit) + app.Ajax("/group/:id/forum", group.Forum) // User profiles app.Ajax("/user", user.Get) diff --git a/pages/group/forum.go b/pages/group/forum.go new file mode 100644 index 00000000..4db63468 --- /dev/null +++ b/pages/group/forum.go @@ -0,0 +1,21 @@ +package group + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +// Forum ... +func Forum(ctx *aero.Context) string { + id := ctx.Get("id") + group, err := arn.GetGroup(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Group not found", err) + } + + return ctx.HTML(components.GroupForum(group)) +} diff --git a/pages/group/forum.pixy b/pages/group/forum.pixy new file mode 100644 index 00000000..5253aa94 --- /dev/null +++ b/pages/group/forum.pixy @@ -0,0 +1,4 @@ +component GroupForum(group *arn.Group) + GroupTabs(group) + + h1 Forum \ No newline at end of file diff --git a/pages/group/group.pixy b/pages/group/group.pixy index 434f3913..41c8aecd 100644 --- a/pages/group/group.pixy +++ b/pages/group/group.pixy @@ -30,4 +30,5 @@ component Group(group *arn.Group) component GroupTabs(group *arn.Group) .tabs Tab("Group", "users", group.Link()) + Tab("Forum", "comment", group.Link() + "/forum") Tab("Edit", "pencil", group.Link() + "/edit") \ No newline at end of file diff --git a/utils/editform/editform.go b/utils/editform/editform.go index 37ad9931..96bd36dc 100644 --- a/utils/editform/editform.go +++ b/utils/editform/editform.go @@ -34,7 +34,7 @@ func Render(obj interface{}, title string, user *arn.User) string { if user != nil && (user.Role == "editor" || user.Role == "admin") { b.WriteString(`
    `) b.WriteString(`
    `) - b.WriteString(``) + b.WriteString(``) b.WriteString(`
    `) } From 24914cb6ff908787a94ab77de9c3fc11b1d0e44d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 19 Oct 2017 04:36:05 +0200 Subject: [PATCH 455/527] Improved CSS reloads in service worker --- scripts/AnimeNotifier.ts | 4 + sw/service-worker.ts | 469 +++++++++++++++++++++------------------ 2 files changed, 263 insertions(+), 210 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index bbdce8fe..e60b3b3a 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -250,6 +250,10 @@ export class AnimeNotifier { } break + + case "reload page": + location.reload(true) + break } } diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 2f4bfafb..441ea201 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -1,6 +1,5 @@ // pack:ignore -const CACHE = "v-3" const RELOADS = new Map>() const ETAGS = new Map() const CACHEREFRESH = new Map>() @@ -18,239 +17,257 @@ const EXCLUDECACHE = new Set([ "/from/", // Chrome extension - "chrome-extension" + "chrome-extension", + + // Authorization paths /auth/ and /logout are not listed here because they are handled in a special way. ]) -self.addEventListener("install", (evt: InstallEvent) => { - console.log("service worker install") +class MyCache { + version: string - evt.waitUntil( - (self as any).skipWaiting().then(() => { - return installCache() + constructor(version: string) { + this.version = version + } + + store(request: RequestInfo, response: Response) { + return caches.open(this.version).then(cache => { + return cache.put(request, response) }) - ) -}) + } +} -self.addEventListener("activate", (evt: any) => { - console.log("service worker activate") +class MyServiceWorker { + cache: MyCache + currentCSP: string - // Delete old cache - let cacheWhitelist = [CACHE] + constructor() { + this.cache = new MyCache("v-3") + this.currentCSP = "" + + self.addEventListener("install", (evt: InstallEvent) => evt.waitUntil(this.onInstall(evt))) + self.addEventListener("activate", (evt: any) => evt.waitUntil(this.onActivate(evt))) + self.addEventListener("fetch", (evt: FetchEvent) => evt.waitUntil(this.onRequest(evt))) + self.addEventListener("message", (evt: any) => evt.waitUntil(this.onMessage(evt))) + self.addEventListener("push", (evt: PushEvent) => evt.waitUntil(this.onPush(evt))) + self.addEventListener("pushsubscriptionchange", (evt: any) => evt.waitUntil(this.onPushSubscriptionChange(evt))) + self.addEventListener("notificationclick", (evt: NotificationEvent) => evt.waitUntil(this.onNotificationClick(evt))) + } - let deleteOldCache = caches.keys().then(keyList => { - return Promise.all(keyList.map(key => { - if(cacheWhitelist.indexOf(key) === -1) { - return caches.delete(key) - } - })) - }) + onInstall(evt: InstallEvent) { + console.log("service worker install") + + return (self as any).skipWaiting().then(() => { + return this.installCache() + }) + } - let immediateClaim = (self as any).clients.claim() - - // Immediate claim - evt.waitUntil( - Promise.all([ + onActivate(evt: any) { + console.log("service worker activate") + + // Only keep current version of the cache and delete old caches + let cacheWhitelist = [this.cache.version] + + let deleteOldCache = caches.keys().then(keyList => { + return Promise.all(keyList.map(key => { + if(cacheWhitelist.indexOf(key) === -1) { + return caches.delete(key) + } + })) + }) + + // Immediate claim helps us gain control over a new client immediately + let immediateClaim = (self as any).clients.claim() + + return Promise.all([ deleteOldCache, immediateClaim ]) - ) -}) + } -// controlling service worker -self.addEventListener("message", (evt: any) => { - let message = JSON.parse(evt.data) + onRequest(evt: FetchEvent) { + let request = evt.request as Request - let url = message.url - let refresh = RELOADS.get(url) - let servedETag = ETAGS.get(url) + // console.log("fetch:", request.url) - // If the user requests a sub-page we should prefetch the full page, too. - if(url.includes("/_/")) { - let fullPage = new Request(url.replace("/_/", "/")) + // If it's not a GET request, fetch it normally + if(request.method !== "GET") { + return evt.respondWith(fetch(request)) + } - fetch(fullPage, { - credentials: "same-origin" - }) - .then(response => { + // Clear cache on authentication and fetch it normally + if(request.url.includes("/auth/") || request.url.includes("/logout")) { + return caches.delete(this.cache.version).then(() => evt.respondWith(fetch(request))) + } + + // Exclude certain URLs from being cached + for(let pattern of EXCLUDECACHE.keys()) { + if(request.url.includes(pattern)) { + return evt.respondWith(fetch(request)) + } + } + + // If the request included the header "X-CacheOnly", return a cache-only response. + // This is used in reloads to avoid generating a 2nd request after a cache refresh. + if(request.headers.get("X-CacheOnly") === "true") { + return this.fromCache(request) + } + + // Start fetching the request + let refresh = fetch(request).then(response => { + let clone = response.clone() + // Save the new version of the resource in the cache - let cacheRefresh = caches.open(CACHE).then(cache => { - return cache.put(fullPage, response) + let cacheRefresh = this.cache.store(request, clone) + + CACHEREFRESH.set(request.url, cacheRefresh) + + return response + }) + + // Save in map + RELOADS.set(request.url, refresh) + + // Forced reload + let servedETag = undefined + + let onResponse = response => { + servedETag = response.headers.get("ETag") + ETAGS.set(request.url, servedETag) + return response + } + + if(request.headers.get("X-Reload") === "true") { + return evt.respondWith(refresh.then(onResponse)) + } + + // Try to serve cache first and fall back to network response + let networkOrCache = this.fromCache(request).then(onResponse).catch(error => { + // console.log("Cache MISS:", request.url) + return refresh + }) + + return evt.respondWith(networkOrCache) + } + + onMessage(evt: any) { + let message = JSON.parse(evt.data) + + let url = message.url + let refresh = RELOADS.get(url) + let servedETag = ETAGS.get(url) + + // If the user requests a sub-page we should prefetch the full page, too. + if(url.includes("/_/")) { + let fullPage = new Request(url.replace("/_/", "/")) + + let fullPageRefresh = fetch(fullPage, { + credentials: "same-origin" + }).then(response => { + // Save the new version of the resource in the cache + let cacheRefresh = caches.open(this.cache.version).then(cache => { + return cache.put(fullPage, response) + }) + + CACHEREFRESH.set(fullPage.url, cacheRefresh) + return response }) - CACHEREFRESH.set(fullPage.url, cacheRefresh) - return cacheRefresh - }) - } - - if(!refresh || !servedETag) { - return - } + // Save in map + RELOADS.set(fullPage.url, fullPageRefresh) + } - evt.waitUntil( - refresh.then((response: Response) => { + if(!refresh || !servedETag) { + return Promise.resolve() + } + + return refresh.then((response: Response) => { // If the fresh copy was used to serve the request instead of the cache, // we don"t need to tell the client to do a refresh. if(response.bodyUsed) { return } - + + // Get ETag let eTag = response.headers.get("ETag") - if(eTag === servedETag) { - return - } - + // Update ETag ETAGS.set(url, eTag) - let message = { - type: "new content", - url + // Get CSP + let oldCSP = this.currentCSP + let csp = response.headers.get("Content-Security-Policy") + + if(csp != oldCSP) { + this.currentCSP = csp + + if(oldCSP !== "") { + return this.forceClientReloadPage(url, evt.source) + } } - let cacheRefresh = CACHEREFRESH.get(url) - - if(!cacheRefresh) { - console.log("forcing reload, cache refresh null") - return evt.source.postMessage(JSON.stringify(message)) + // If the ETag changed, we need to do a reload. + // If the CSP and therefore the sha-1 hash of the CSS changed, we need to do a reload. + if(eTag !== servedETag) { + return this.forceClientReloadContent(url, evt.source) } - - return cacheRefresh.then(() => { - console.log("forcing reload after cache refresh") - evt.source.postMessage(JSON.stringify(message)) - }) + + // Do nothing + return Promise.resolve() }) - ) -}) - -self.addEventListener("fetch", async (evt: FetchEvent) => { - let request = evt.request as Request - let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") - let ignoreCache = false - - // console.log("fetch:", request.url) - - // Exclude certain URLs from being cached - for(let pattern of EXCLUDECACHE.keys()) { - if(request.url.includes(pattern)) { - ignoreCache = true - break - } } - // Delete existing cache on authentication - if(isAuth) { - caches.delete(CACHE) - } - - // Do not use cache in some cases - if(request.method !== "GET" || isAuth || ignoreCache) { - return evt.waitUntil(evt.respondWith(fetch(request))) - } - - // Forced cache response? - if(request.headers.get("X-CacheOnly") === "true") { - // console.log("forced cache response") - return evt.waitUntil(fromCache(request)) - } - - let servedETag = undefined + onPush(evt: PushEvent) { + var payload = evt.data ? evt.data.json() : {} - // Start fetching the request - let refresh = fetch(request).then(response => { - // console.log(response) - let clone = response.clone() - - // Save the new version of the resource in the cache - let cacheRefresh = caches.open(CACHE).then(cache => { - return cache.put(request, clone) - }) - - CACHEREFRESH.set(request.url, cacheRefresh) - - return response - }) - - // Save in map - RELOADS.set(request.url, refresh) - - // Forced reload - if(request.headers.get("X-Reload") === "true") { - return evt.waitUntil(evt.respondWith(refresh.then(response => { - servedETag = response.headers.get("ETag") - ETAGS.set(request.url, servedETag) - return response - }))) - } - - // Try to serve cache first and fall back to network response - let networkOrCache = fromCache(request).then(response => { - // console.log("served from cache:", request.url) - servedETag = response.headers.get("ETag") - ETAGS.set(request.url, servedETag) - return response - }).catch(error => { - // console.log("Cache MISS:", request.url) - return refresh - }) - - return evt.waitUntil(evt.respondWith(networkOrCache)) -}) - -self.addEventListener("push", (evt: PushEvent) => { - var payload = evt.data ? evt.data.json() : {} - - evt.waitUntil( - (self as any).registration.showNotification(payload.title, { + return (self as any).registration.showNotification(payload.title, { body: payload.message, icon: payload.icon, image: payload.image, data: payload.link, - badge: "https://notify.moe/brand/64" + badge: "https://notify.moe/brand/64.png" }) - ) -}) + } -self.addEventListener("pushsubscriptionchange", (evt: any) => { - evt.waitUntil((self as any).registration.pushManager.subscribe(evt.oldSubscription.options) - .then(async subscription => { - console.log("send subscription to server...") - - let rawKey = subscription.getKey("p256dh") - let key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : "" - - let rawSecret = subscription.getKey("auth") - let secret = rawSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawSecret))) : "" - - let endpoint = subscription.endpoint - - let pushSubscription = { - endpoint, - p256dh: key, - auth: secret, - platform: navigator.platform, - userAgent: navigator.userAgent, - screen: { - width: window.screen.width, - height: window.screen.height + onPushSubscriptionChange(evt: any) { + return (self as any).registration.pushManager.subscribe(evt.oldSubscription.options) + .then(async subscription => { + console.log("send subscription to server...") + + let rawKey = subscription.getKey("p256dh") + let key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : "" + + let rawSecret = subscription.getKey("auth") + let secret = rawSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawSecret))) : "" + + let endpoint = subscription.endpoint + + let pushSubscription = { + endpoint, + p256dh: key, + auth: secret, + platform: navigator.platform, + userAgent: navigator.userAgent, + screen: { + width: window.screen.width, + height: window.screen.height + } } - } - - let user = await fetch("/api/me").then(response => response.json()) - - return fetch("/api/pushsubscriptions/" + user.id + "/add", { - method: "POST", - credentials: "same-origin", - body: JSON.stringify(pushSubscription) + + let user = await fetch("/api/me").then(response => response.json()) + + return fetch("/api/pushsubscriptions/" + user.id + "/add", { + method: "POST", + credentials: "same-origin", + body: JSON.stringify(pushSubscription) + }) }) - })) -}) + } -self.addEventListener("notificationclick", (evt: NotificationEvent) => { - let notification = evt.notification - notification.close() - - evt.waitUntil( - (self as any).clients.matchAll().then(function(clientList) { + onNotificationClick(evt: NotificationEvent) { + let notification = evt.notification + notification.close() + + return (self as any).clients.matchAll().then(function(clientList) { // If we have a link, use that link to open a new window. let url = notification.data @@ -266,28 +283,60 @@ self.addEventListener("notificationclick", (evt: NotificationEvent) => { // Otherwise open a new window return (self as any).clients.openWindow("https://notify.moe") }) - ) -}) + } -function installCache() { - return caches.open(CACHE).then(cache => { - return cache.addAll([ - "./", - "./scripts", - "https://fonts.gstatic.com/s/ubuntu/v10/2Q-AW1e_taO6pHwMXcXW5w.ttf" - ]) - }) -} + forceClientReloadContent(url: string, eventSource: any) { + let message = { + type: "new content", + url + } -function fromCache(request) { - return caches.open(CACHE).then(cache => { - return cache.match(request).then(matching => { - if(matching) { - // console.log("Cache HIT:", request.url) - return Promise.resolve(matching) - } + this.postMessageAfterPromise(message, CACHEREFRESH.get(url), eventSource) + } - return Promise.reject("no-match") + forceClientReloadPage(url: string, eventSource: any) { + let message = { + type: "reload page", + url + } + + this.postMessageAfterPromise(message, RELOADS.get(url.replace("/_/", "/")), eventSource) + } + + postMessageAfterPromise(message: any, promise: Promise, eventSource: any) { + if(!promise) { + console.log("forcing reload, cache refresh null") + return eventSource.postMessage(JSON.stringify(message)) + } + + return promise.then(() => { + console.log("forcing reload after cache refresh") + eventSource.postMessage(JSON.stringify(message)) }) - }) + } + + installCache() { + return caches.open(this.cache.version).then(cache => { + return cache.addAll([ + "./", + "./scripts", + "https://fonts.gstatic.com/s/ubuntu/v10/2Q-AW1e_taO6pHwMXcXW5w.ttf" + ]) + }) + } + + fromCache(request) { + return caches.open(this.cache.version).then(cache => { + return cache.match(request).then(matching => { + if(matching) { + // console.log("Cache HIT:", request.url) + return Promise.resolve(matching) + } + + return Promise.reject("no-match") + }) + }) + } } + +const serviceWorker = new MyServiceWorker() From da4e026f413125cfe2f0ffb3f184ba5753e29631 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 19 Oct 2017 21:43:42 +0200 Subject: [PATCH 456/527] Added database search --- main.go | 5 ++ pages/database/database.go | 11 ++++ pages/database/database.pixy | 36 ++++++++++ pages/database/database.scarlet | 25 +++++++ pages/database/select.go | 113 ++++++++++++++++++++++++++++++++ pages/editor/editor.pixy | 3 +- scripts/Actions/Search.ts | 78 ++++++++++++++++++++++ 7 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 pages/database/database.go create mode 100644 pages/database/database.pixy create mode 100644 pages/database/database.scarlet create mode 100644 pages/database/select.go diff --git a/main.go b/main.go index a2a9dcf8..b1f9b3bb 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "github.com/animenotifier/notify.moe/pages/character" "github.com/animenotifier/notify.moe/pages/charge" "github.com/animenotifier/notify.moe/pages/dashboard" + "github.com/animenotifier/notify.moe/pages/database" "github.com/animenotifier/notify.moe/pages/editanime" "github.com/animenotifier/notify.moe/pages/editor" "github.com/animenotifier/notify.moe/pages/embed" @@ -153,6 +154,10 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/editor/anilist", editor.AniList) app.Ajax("/editor/shoboi", editor.Shoboi) + // Mixed + app.Ajax("/database", database.Get) + app.Get("/api/select/:data-type/where/:field/is/:field-value", database.Select) + // Import app.Ajax("/import", listimport.Get) app.Ajax("/import/anilist/animelist", listimportanilist.Preview) diff --git a/pages/database/database.go b/pages/database/database.go new file mode 100644 index 00000000..b2cf4b24 --- /dev/null +++ b/pages/database/database.go @@ -0,0 +1,11 @@ +package database + +import ( + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" +) + +// Get the dashboard. +func Get(ctx *aero.Context) string { + return ctx.HTML(components.Database()) +} diff --git a/pages/database/database.pixy b/pages/database/database.pixy new file mode 100644 index 00000000..300d19e2 --- /dev/null +++ b/pages/database/database.pixy @@ -0,0 +1,36 @@ +component Database + EditorTabs + + .widget-form + .widget + h1.mountable Database search + + .widget-section.mountable + label(for="data-type") Search + select#data-type.widget-ui-element(value="Anime") + option(value="Analytics") Analytics + option(value="Anime") Anime + option(value="AnimeList") AnimeList + option(value="Character") Character + option(value="Group") Group + option(value="Post") Post + option(value="Settings") Settings + option(value="SoundTrack") SoundTrack + option(value="Thread") Thread + option(value="User") User + + .widget-section.mountable + label(for="field") where + input#field.widget-ui-element(type="text", placeholder="Field name (e.g. Title or Title.Canonical)") + + .widget-section.mountable + label(for="field-value") is + input#field-value.widget-ui-element(type="text") + + .buttons.mountable + button.action(data-action="searchDB", data-trigger="click") + Icon("search") + span Search + + h3.text-center Results + #records \ No newline at end of file diff --git a/pages/database/database.scarlet b/pages/database/database.scarlet new file mode 100644 index 00000000..9c1638d1 --- /dev/null +++ b/pages/database/database.scarlet @@ -0,0 +1,25 @@ +#records + horizontal-wrap + justify-content space-around + margin-top content-padding + +.record + vertical + ui-element + padding 0.5rem 1rem + margin 0.5rem + +.record-id + :before + content "ID: " + +.record-view + // + +.record-view-api + // + +.record-count + text-align right + font-size 0.8rem + opacity 0.5 \ No newline at end of file diff --git a/pages/database/select.go b/pages/database/select.go new file mode 100644 index 00000000..5b1aa8f2 --- /dev/null +++ b/pages/database/select.go @@ -0,0 +1,113 @@ +package database + +import ( + "net/http" + + "github.com/aerogo/aero" + "github.com/aerogo/mirror" + "github.com/animenotifier/arn" +) + +// QueryResponse .. +type QueryResponse struct { + Results []interface{} `json:"results"` +} + +// Select ... +func Select(ctx *aero.Context) string { + dataTypeName := ctx.Get("data-type") + field := ctx.Get("field") + searchValue := ctx.Get("field-value") + + // Empty values + if dataTypeName == "+" { + dataTypeName = "" + } + + if field == "+" { + field = "" + } + + if searchValue == "+" { + searchValue = "" + } + + // Check empty parameters + if dataTypeName == "" || field == "" { + return ctx.Error(http.StatusBadRequest, "Not enough parameters", nil) + } + + // Check data type parameter + _, found := arn.DB.Types()[dataTypeName] + + if !found { + return ctx.Error(http.StatusBadRequest, "Invalid type", nil) + } + + response := &QueryResponse{ + Results: []interface{}{}, + } + + stream, err := arn.DB.All(dataTypeName) + + if err != nil { + return ctx.Error(http.StatusInternalServerError, "Error fetching data from the database", err) + } + + process := func(obj interface{}) { + _, _, value, _ := mirror.GetField(obj, field) + + if value.String() == searchValue { + response.Results = append(response.Results, obj) + } + } + + switch dataTypeName { + case "Analytics": + for obj := range stream.(chan *arn.Analytics) { + process(obj) + } + case "Anime": + for obj := range stream.(chan *arn.Anime) { + process(obj) + } + case "AnimeList": + for obj := range stream.(chan *arn.AnimeList) { + process(obj) + } + case "Character": + for obj := range stream.(chan *arn.Character) { + process(obj) + } + case "Group": + for obj := range stream.(chan *arn.Group) { + process(obj) + } + case "Post": + for obj := range stream.(chan *arn.Post) { + process(obj) + } + case "Settings": + for obj := range stream.(chan *arn.Settings) { + process(obj) + } + case "SoundTrack": + for obj := range stream.(chan *arn.SoundTrack) { + process(obj) + } + case "Thread": + for obj := range stream.(chan *arn.Thread) { + process(obj) + } + case "User": + for obj := range stream.(chan *arn.User) { + process(obj) + } + } + + for _, obj := range response.Results { + mirror.GetField(obj, field) + } + + return ctx.JSON(response) +} diff --git a/pages/editor/editor.pixy b/pages/editor/editor.pixy index ac9599b9..e62c936d 100644 --- a/pages/editor/editor.pixy +++ b/pages/editor/editor.pixy @@ -3,11 +3,12 @@ component Editor EditorTabs - p Welcome to the Editor Panel! + p.text-center.mountable Welcome to the Editor Panel! component EditorTabs .tabs Tab("Editor", "pencil", "/editor") + Tab("Search", "search", "/database") Tab("Shoboi", "calendar", "/editor/shoboi") Tab("AniList", "list", "/editor/anilist") diff --git a/scripts/Actions/Search.ts b/scripts/Actions/Search.ts index d1615cf3..0be2792f 100644 --- a/scripts/Actions/Search.ts +++ b/scripts/Actions/Search.ts @@ -14,4 +14,82 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard } arn.diff("/search/" + term) +} + +// Search database +export function searchDB(arn: AnimeNotifier, input: HTMLInputElement, e: KeyboardEvent) { + if(e.ctrlKey || e.altKey) { + return + } + + let dataType = (arn.app.find("data-type") as HTMLInputElement).value || "+" + let field = (arn.app.find("field") as HTMLInputElement).value || "+" + let fieldValue = (arn.app.find("field-value") as HTMLInputElement).value || "+" + let records = arn.app.find("records") + + arn.loading(true) + + fetch(`/api/select/${dataType}/where/${field}/is/${fieldValue}`) + .then(response => { + if(response.status !== 200) { + throw response + } + + return response + }) + .then(response => response.json()) + .then(data => { + records.innerHTML = "" + let count = 0 + + if(data.results.length === 0) { + records.innerText = "No results." + return + } + + for(let record of data.results) { + count++ + + let container = document.createElement("div") + container.classList.add("record") + + let id = document.createElement("div") + id.innerText = record.id + id.classList.add("record-id") + container.appendChild(id) + + let link = document.createElement("a") + link.classList.add("record-view") + link.innerText = "Open " + dataType.toLowerCase() + + if(dataType === "User") { + link.href = "/+" + record.nick + } else { + link.href = "/" + dataType.toLowerCase() + "/" + record.id + } + + link.target = "_blank" + container.appendChild(link) + + let apiLink = document.createElement("a") + apiLink.classList.add("record-view-api") + apiLink.innerText = "JSON data" + apiLink.href = "/api/" + dataType.toLowerCase() + "/" + record.id + apiLink.target = "_blank" + container.appendChild(apiLink) + + let recordCount = document.createElement("div") + recordCount.innerText = count + "/" + data.results.length + recordCount.classList.add("record-count") + container.appendChild(recordCount) + + records.appendChild(container) + } + }) + .catch(response => { + response.text().then(text => { + arn.statusMessage.showError(text) + }) + }) + .then(() => arn.loading(false)) } \ No newline at end of file From 4fa15aad583ff22a311002e6c3fdc9c34c56aee7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 01:08:40 +0200 Subject: [PATCH 457/527] Disabled reloads for now --- scripts/AnimeNotifier.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e60b3b3a..81f07c5f 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -252,7 +252,8 @@ export class AnimeNotifier { break case "reload page": - location.reload(true) + console.log("service worker instructed to reload page...disobeying in test mode") + // location.reload(true) break } } From d50ba892e6781fdad9285cd588712d2a653adbc6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 02:43:02 +0200 Subject: [PATCH 458/527] CSS and SW cleanup --- pages/animelist/animelist.scarlet | 2 +- scripts/AnimeNotifier.ts | 94 +++--------------------------- scripts/ServiceWorkerManager.ts | 96 +++++++++++++++++++++++++++++++ styles/include/config.scarlet | 2 +- styles/sidebar.scarlet | 6 +- styles/status-message.scarlet | 2 +- styles/widgets.scarlet | 2 +- sw/service-worker.ts | 59 +++++++++++-------- 8 files changed, 148 insertions(+), 115 deletions(-) create mode 100644 scripts/ServiceWorkerManager.ts diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 59310476..76910ce7 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -38,7 +38,7 @@ :hover .plus-episode opacity 1 - pointer-events all + pointer-events auto .anime-list-item-episodes-watched flex 0.4 diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 81f07c5f..46c2bc33 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -1,6 +1,3 @@ -import * as actions from "./Actions" -import { displayAiringDate, displayDate } from "./DateView" -import { findAll, delay, canUseWebP, swapElements } from "./Utils" import { Application } from "./Application" import { Diff } from "./Diff" import { MutationQueue } from "./MutationQueue" @@ -10,6 +7,10 @@ import { TouchController } from "./TouchController" import { Analytics } from "./Analytics" import { SideBar } from "./SideBar" import { InfiniteScroller } from "./InfiniteScroller" +import { ServiceWorkerManager } from "./ServiceWorkerManager" +import { displayAiringDate, displayDate } from "./DateView" +import { findAll, delay, canUseWebP, swapElements } from "./Utils" +import * as actions from "./Actions" export class AnimeNotifier { app: Application @@ -21,6 +22,7 @@ export class AnimeNotifier { statusMessage: StatusMessage visibilityObserver: IntersectionObserver pushManager: PushManager + serviceWorkerManager: ServiceWorkerManager touchController: TouchController sideBar: SideBar infiniteScroller: InfiniteScroller @@ -122,6 +124,9 @@ export class AnimeNotifier { // Push manager this.pushManager = new PushManager() + // Service worker + this.serviceWorkerManager = new ServiceWorkerManager(this, "/service-worker") + // Analytics this.analytics = new Analytics() @@ -164,7 +169,7 @@ export class AnimeNotifier { onIdle() { // Service worker - this.registerServiceWorker() + this.serviceWorkerManager.register() // Analytics if(this.user) { @@ -177,87 +182,6 @@ export class AnimeNotifier { } } - registerServiceWorker() { - if(!("serviceWorker" in navigator)) { - return - } - - console.log("register service worker") - - navigator.serviceWorker.register("/service-worker").then(registration => { - // registration.update() - }) - - navigator.serviceWorker.addEventListener("message", evt => { - this.onServiceWorkerMessage(evt) - }) - - // This will send a message to the service worker that the DOM has been loaded - let sendContentLoadedEvent = () => { - if(!navigator.serviceWorker.controller) { - return - } - - // A reloadContent call should never trigger another reload - if(this.app.currentPath === this.lastReloadContentPath) { - console.log("reload finished.") - this.lastReloadContentPath = "" - return - } - - let message = { - type: "loaded", - url: "" - } - - // If mainPageLoaded is set, it means every single request is now an AJAX request for the /_/ prefixed page - if(this.mainPageLoaded) { - message.url = window.location.origin + "/_" + window.location.pathname - } else { - this.mainPageLoaded = true - message.url = window.location.href - } - - console.log("checking for updates:", message.url) - - navigator.serviceWorker.controller.postMessage(JSON.stringify(message)) - } - - // For future loaded events - document.addEventListener("DOMContentLoaded", sendContentLoadedEvent) - - // If the page is loaded already, send the loaded event right now. - if(document.readyState !== "loading") { - sendContentLoadedEvent() - } - } - - onServiceWorkerMessage(evt: ServiceWorkerMessageEvent) { - let message = JSON.parse(evt.data) - - switch(message.type) { - case "new content": - if(message.url.includes("/_/")) { - // Content reload - this.contentLoadedActions.then(() => { - this.reloadContent(true) - }) - } else { - // Full page reload - this.contentLoadedActions.then(() => { - this.reloadPage() - }) - } - - break - - case "reload page": - console.log("service worker instructed to reload page...disobeying in test mode") - // location.reload(true) - break - } - } - dragAndDrop() { for(let element of findAll("inventory-slot")) { // Skip elements that have their event listeners attached already diff --git a/scripts/ServiceWorkerManager.ts b/scripts/ServiceWorkerManager.ts new file mode 100644 index 00000000..a7d9d60c --- /dev/null +++ b/scripts/ServiceWorkerManager.ts @@ -0,0 +1,96 @@ +import { AnimeNotifier } from "./AnimeNotifier" + +export class ServiceWorkerManager { + arn: AnimeNotifier + uri: string + + constructor(arn: AnimeNotifier, uri: string) { + this.arn = arn + this.uri = uri + } + + register() { + if(!("serviceWorker" in navigator)) { + return + } + + console.log("register service worker") + + navigator.serviceWorker.register(this.uri).then(registration => { + // registration.update() + }) + + navigator.serviceWorker.addEventListener("message", evt => { + this.onMessage(evt) + }) + + // This will send a message to the service worker that the DOM has been loaded + let sendContentLoadedEvent = () => { + if(!navigator.serviceWorker.controller) { + return + } + + // A reloadContent call should never trigger another reload + if(this.arn.app.currentPath === this.arn.lastReloadContentPath) { + console.log("reload finished.") + this.arn.lastReloadContentPath = "" + return + } + + let message = { + type: "loaded", + url: "" + } + + // If mainPageLoaded is set, it means every single request is now an AJAX request for the /_/ prefixed page + if(this.arn.mainPageLoaded) { + message.url = window.location.origin + "/_" + window.location.pathname + } else { + this.arn.mainPageLoaded = true + message.url = window.location.href + } + + console.log("checking for updates:", message.url) + + this.postMessage(message) + } + + // For future loaded events + document.addEventListener("DOMContentLoaded", sendContentLoadedEvent) + + // If the page is loaded already, send the loaded event right now. + if(document.readyState !== "loading") { + sendContentLoadedEvent() + } + } + + postMessage(message: any) { + navigator.serviceWorker.controller.postMessage(JSON.stringify(message)) + } + + onMessage(evt: ServiceWorkerMessageEvent) { + let message = JSON.parse(evt.data) + + switch(message.type) { + case "new content": + if(message.url.includes("/_/")) { + // Content reload + this.arn.contentLoadedActions.then(() => { + this.arn.reloadContent(true) + }) + } else { + // Full page reload + this.arn.contentLoadedActions.then(() => { + this.arn.reloadPage() + }) + } + + break + + case "reload page": + console.log("service worker instructed to reload page...disobeying in test mode") + // location.reload(true) + break + } + } +} \ No newline at end of file diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 3e60fc87..6750fb15 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -11,7 +11,7 @@ pro-color = hsla(0, 100%, 73%, 0.87) ui-border-color = rgba(0, 0, 0, 0.1) ui-border = 1px solid ui-border-color ui-hover-border-color = rgba(0, 0, 0, 0.15) -ui-hover-border-color = 1px solid ui-hover-border-color +ui-hover-border = 1px solid ui-hover-border-color 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%) diff --git a/styles/sidebar.scarlet b/styles/sidebar.scarlet index 856a493b..3671d382 100644 --- a/styles/sidebar.scarlet +++ b/styles/sidebar.scarlet @@ -16,7 +16,7 @@ sidebar-spacing-y = 0.7rem pointer-events none box-shadow shadow-medium transition opacity transition-speed ease, transform transition-speed ease - will-change opacity transition + will-change opacity, transition .user-image-container horizontal @@ -29,14 +29,14 @@ sidebar-spacing-y = 0.7rem opacity 1 transform none position static - pointer-events all + pointer-events auto box-shadow none border-right ui-border background rgba(0, 0, 0, 0.03) .sidebar-visible transform translateX(0) !important - pointer-events all !important + pointer-events auto !important opacity 1 !important .sidebar-link diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet index f7938bc8..d433119b 100644 --- a/styles/status-message.scarlet +++ b/styles/status-message.scarlet @@ -14,7 +14,7 @@ .status-message-action color white !important - pointer-events all !important + pointer-events auto !important .error-message color white diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index abbbecfe..d3fe8f3f 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -34,7 +34,7 @@ .widget-ui-element vertical-wrap ui-element - transition border transition-speed ease, background transition-speed ease, transform transition-speed ease, transform color ease + transition border transition-speed ease, background transition-speed ease, transform transition-speed ease, color transition-speed ease margin-bottom 1rem padding 0.5rem 1rem width 100% diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 441ea201..701fa408 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -151,29 +151,23 @@ class MyServiceWorker { onMessage(evt: any) { let message = JSON.parse(evt.data) - - let url = message.url + + switch(message.type) { + case "loaded": + this.onDOMContentLoaded(evt, message.url) + break + } + } + + // onDOMContentLoaded is called when the client sent this service worker + // a message that the page has been loaded. + onDOMContentLoaded(evt: any, url: string) { let refresh = RELOADS.get(url) let servedETag = ETAGS.get(url) // If the user requests a sub-page we should prefetch the full page, too. if(url.includes("/_/")) { - let fullPage = new Request(url.replace("/_/", "/")) - - let fullPageRefresh = fetch(fullPage, { - credentials: "same-origin" - }).then(response => { - // Save the new version of the resource in the cache - let cacheRefresh = caches.open(this.cache.version).then(cache => { - return cache.put(fullPage, response) - }) - - CACHEREFRESH.set(fullPage.url, cacheRefresh) - return response - }) - - // Save in map - RELOADS.set(fullPage.url, fullPageRefresh) + this.prefetchFullPage(url) } if(!refresh || !servedETag) { @@ -181,13 +175,13 @@ class MyServiceWorker { } return refresh.then((response: Response) => { - // If the fresh copy was used to serve the request instead of the cache, - // we don"t need to tell the client to do a refresh. + // When the actual network request was used by the client, response.bodyUsed is set. + // In that case the client is already up to date and we don"t need to tell the client to do a refresh. if(response.bodyUsed) { return } - // Get ETag + // Get the ETag of the cached response we sent to the client earlier. let eTag = response.headers.get("ETag") // Update ETag @@ -197,6 +191,7 @@ class MyServiceWorker { let oldCSP = this.currentCSP let csp = response.headers.get("Content-Security-Policy") + // If the CSP and therefore the sha-1 hash of the CSS changed, we need to do a reload. if(csp != oldCSP) { this.currentCSP = csp @@ -206,7 +201,6 @@ class MyServiceWorker { } // If the ETag changed, we need to do a reload. - // If the CSP and therefore the sha-1 hash of the CSS changed, we need to do a reload. if(eTag !== servedETag) { return this.forceClientReloadContent(url, evt.source) } @@ -216,6 +210,25 @@ class MyServiceWorker { }) } + prefetchFullPage(url: string) { + let fullPage = new Request(url.replace("/_/", "/")) + + let fullPageRefresh = fetch(fullPage, { + credentials: "same-origin" + }).then(response => { + // Save the new version of the resource in the cache + let cacheRefresh = caches.open(this.cache.version).then(cache => { + return cache.put(fullPage, response) + }) + + CACHEREFRESH.set(fullPage.url, cacheRefresh) + return response + }) + + // Save in map + RELOADS.set(fullPage.url, fullPageRefresh) + } + onPush(evt: PushEvent) { var payload = evt.data ? evt.data.json() : {} @@ -320,7 +333,7 @@ class MyServiceWorker { return cache.addAll([ "./", "./scripts", - "https://fonts.gstatic.com/s/ubuntu/v10/2Q-AW1e_taO6pHwMXcXW5w.ttf" + "https://fonts.gstatic.com/s/ubuntu/v11/4iCs6KVjbNBYlgoKfw7z.ttf" ]) }) } From 02e89cd5e2c7ac24da563b1a138f762a635cb029 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 16:52:07 +0200 Subject: [PATCH 459/527] Added anime tabs --- main.go | 3 +++ pages/anime/anime.go | 30 +++++++++--------------- pages/anime/anime.pixy | 46 ++++--------------------------------- pages/anime/animetabs.pixy | 6 +++++ pages/anime/characters.go | 22 ++++++++++++++++++ pages/anime/characters.pixy | 8 +++++++ pages/anime/episodes.go | 23 +++++++++++++++++++ pages/anime/episodes.pixy | 27 ++++++++++++++++++++++ pages/anime/tracks.go | 31 +++++++++++++++++++++++++ pages/anime/tracks.pixy | 7 ++++++ 10 files changed, 142 insertions(+), 61 deletions(-) create mode 100644 pages/anime/animetabs.pixy create mode 100644 pages/anime/characters.go create mode 100644 pages/anime/characters.pixy create mode 100644 pages/anime/episodes.go create mode 100644 pages/anime/episodes.pixy create mode 100644 pages/anime/tracks.go create mode 100644 pages/anime/tracks.pixy diff --git a/main.go b/main.go index b1f9b3bb..5d5d9f89 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,9 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/", home.Get) app.Ajax("/dashboard", dashboard.Get) app.Ajax("/anime/:id", anime.Get) + app.Ajax("/anime/:id/episodes", anime.Episodes) + app.Ajax("/anime/:id/characters", anime.Characters) + app.Ajax("/anime/:id/tracks", anime.Tracks) app.Ajax("/anime/:id/edit", editanime.Get) app.Ajax("/api", apiview.Get) app.Ajax("/best/anime", best.Get) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 308a09c0..0277f053 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -10,8 +10,8 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -const maxEpisodes = 26 -const maxEpisodesLongSeries = 5 +// const maxEpisodes = 26 +// const maxEpisodesLongSeries = 5 const maxDescriptionLength = 170 // Get anime page. @@ -24,24 +24,16 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime not found", err) } - tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { - return !track.IsDraft && len(track.Media) > 0 && arn.Contains(track.Tags, "anime:"+anime.ID) - }) + // episodesReversed := false - if err != nil { - return ctx.Error(http.StatusNotFound, "Error fetching soundtracks", err) - } + // if len(anime.Episodes().Items) > maxEpisodes { + // episodesReversed = true + // anime.Episodes().Items = anime.Episodes().Items[len(anime.Episodes().Items)-maxEpisodesLongSeries:] - episodesReversed := false - - if len(anime.Episodes().Items) > maxEpisodes { - episodesReversed = true - anime.Episodes().Items = anime.Episodes().Items[len(anime.Episodes().Items)-maxEpisodesLongSeries:] - - for i, j := 0, len(anime.Episodes().Items)-1; i < j; i, j = i+1, j-1 { - anime.Episodes().Items[i], anime.Episodes().Items[j] = anime.Episodes().Items[j], anime.Episodes().Items[i] - } - } + // for i, j := 0, len(anime.Episodes().Items)-1; i < j; i, j = i+1, j-1 { + // anime.Episodes().Items[i], anime.Episodes().Items[j] = anime.Episodes().Items[j], anime.Episodes().Items[i] + // } + // } // Friends watching var friends []*arn.User @@ -108,5 +100,5 @@ func Get(ctx *aero.Context) string { ctx.Data = openGraph - return ctx.HTML(components.Anime(anime, friends, friendsAnimeListItems, tracks, user, episodesReversed)) + return ctx.HTML(components.Anime(anime, friends, friendsAnimeListItems, user)) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index ec72f8c6..8a4469e7 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,4 +1,6 @@ -component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, tracks []*arn.SoundTrack, user *arn.User, episodesReversed bool) +component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) + AnimeTabs(anime) + .anime-header(data-id=anime.ID) if anime.Image.Small != "" .anime-image-container @@ -191,47 +193,7 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* .anime-rating-category .anime-rating-category-name Dropped .anime-rating= anime.Popularity.Dropped - - if len(tracks) > 0 - h3.anime-section-name Tracks - .sound-tracks - each track in tracks - SoundTrack(track) - - if anime.Characters() != nil && len(anime.Characters().Items) > 0 - h3.anime-section-name Characters - .characters - each character in anime.Characters().Items - if character.Character() != nil - Character(character.Character()) - - if len(anime.Episodes().Items) > 0 - if episodesReversed - h3.anime-section-name Latest episodes - else - h3.anime-section-name Episodes - table.episodes - tbody - each episode in anime.Episodes().Items - tr.episode - td.episode-number - if episode.Number != -1 - span= episode.Number - td.episode-title - if episode.Title.Japanese != "" - Japanese(episode.Title.Japanese) - else - span - - td.episode-actions - for name, link := range episode.Links - a(href=link, target="_blank", rel="noopener", title="Watch episode " + toString(episode.Number) + " on " + name) - RawIcon("eye") - //- a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") - //- RawIcon("google") - if validator.IsValidDate(episode.AiringDate.Start) - td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() - else - td.episode-airing-date-start + //- h3.anime-section-name Reviews //- p Coming soon. diff --git a/pages/anime/animetabs.pixy b/pages/anime/animetabs.pixy new file mode 100644 index 00000000..8fa01e18 --- /dev/null +++ b/pages/anime/animetabs.pixy @@ -0,0 +1,6 @@ +component AnimeTabs(anime *arn.Anime) + .tabs + Tab("Anime", "tv", anime.Link()) + Tab("Episodes", "list-ol", anime.Link() + "/episodes") + Tab("Characters", "male", anime.Link() + "/characters") + Tab("Tracks", "music", anime.Link() + "/tracks") \ No newline at end of file diff --git a/pages/anime/characters.go b/pages/anime/characters.go new file mode 100644 index 00000000..e03b8462 --- /dev/null +++ b/pages/anime/characters.go @@ -0,0 +1,22 @@ +package anime + +import ( + "net/http" + + "github.com/animenotifier/notify.moe/components" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" +) + +// Characters ... +func Characters(ctx *aero.Context) string { + id := ctx.Get("id") + anime, err := arn.GetAnime(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Anime not found", err) + } + + return ctx.HTML(components.AnimeCharacters(anime)) +} diff --git a/pages/anime/characters.pixy b/pages/anime/characters.pixy new file mode 100644 index 00000000..0dcfc24c --- /dev/null +++ b/pages/anime/characters.pixy @@ -0,0 +1,8 @@ +component AnimeCharacters(anime *arn.Anime) + AnimeTabs(anime) + + h3.anime-section-name Characters + .characters + each character in anime.Characters().Items + if character.Character() != nil + Character(character.Character()) \ No newline at end of file diff --git a/pages/anime/episodes.go b/pages/anime/episodes.go new file mode 100644 index 00000000..fe2b097d --- /dev/null +++ b/pages/anime/episodes.go @@ -0,0 +1,23 @@ +package anime + +import ( + "net/http" + + "github.com/animenotifier/notify.moe/components" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" +) + +// Episodes ... +func Episodes(ctx *aero.Context) string { + id := ctx.Get("id") + + anime, err := arn.GetAnime(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Anime not found", err) + } + + return ctx.HTML(components.AnimeEpisodes(anime)) +} diff --git a/pages/anime/episodes.pixy b/pages/anime/episodes.pixy new file mode 100644 index 00000000..9adafd61 --- /dev/null +++ b/pages/anime/episodes.pixy @@ -0,0 +1,27 @@ +component AnimeEpisodes(anime *arn.Anime) + AnimeTabs(anime) + + h3.anime-section-name Episodes + + table.episodes + tbody + each episode in anime.Episodes().Items + tr.episode + td.episode-number + if episode.Number != -1 + span= episode.Number + td.episode-title + if episode.Title.Japanese != "" + Japanese(episode.Title.Japanese) + else + span - + td.episode-actions + for name, link := range episode.Links + a(href=link, target="_blank", rel="noopener", title="Watch episode " + toString(episode.Number) + " on " + name) + RawIcon("eye") + //- a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") + //- RawIcon("google") + if validator.IsValidDate(episode.AiringDate.Start) + td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() + else + td.episode-airing-date-start \ No newline at end of file diff --git a/pages/anime/tracks.go b/pages/anime/tracks.go new file mode 100644 index 00000000..c82a1f95 --- /dev/null +++ b/pages/anime/tracks.go @@ -0,0 +1,31 @@ +package anime + +import ( + "net/http" + + "github.com/animenotifier/notify.moe/components" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" +) + +// Tracks ... +func Tracks(ctx *aero.Context) string { + id := ctx.Get("id") + + anime, err := arn.GetAnime(id) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Anime not found", err) + } + + tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + return !track.IsDraft && len(track.Media) > 0 && arn.Contains(track.Tags, "anime:"+anime.ID) + }) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Error fetching soundtracks", err) + } + + return ctx.HTML(components.AnimeTracks(anime, tracks)) +} diff --git a/pages/anime/tracks.pixy b/pages/anime/tracks.pixy new file mode 100644 index 00000000..f1a7bea2 --- /dev/null +++ b/pages/anime/tracks.pixy @@ -0,0 +1,7 @@ +component AnimeTracks(anime *arn.Anime, tracks []*arn.SoundTrack) + AnimeTabs(anime) + + h3.anime-section-name Tracks + .sound-tracks + each track in tracks + SoundTrack(track) \ No newline at end of file From c3b6a021d6916e17a28537f53acbfa1d4b312081 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 17:01:58 +0200 Subject: [PATCH 460/527] Improved page transitions --- pages/anime/characters.pixy | 3 ++- pages/anime/episodes.pixy | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pages/anime/characters.pixy b/pages/anime/characters.pixy index 0dcfc24c..b1e04ba9 100644 --- a/pages/anime/characters.pixy +++ b/pages/anime/characters.pixy @@ -5,4 +5,5 @@ component AnimeCharacters(anime *arn.Anime) .characters each character in anime.Characters().Items if character.Character() != nil - Character(character.Character()) \ No newline at end of file + .mountable + Character(character.Character()) \ No newline at end of file diff --git a/pages/anime/episodes.pixy b/pages/anime/episodes.pixy index 9adafd61..bc8fe58b 100644 --- a/pages/anime/episodes.pixy +++ b/pages/anime/episodes.pixy @@ -6,7 +6,7 @@ component AnimeEpisodes(anime *arn.Anime) table.episodes tbody each episode in anime.Episodes().Items - tr.episode + tr.episode.mountable td.episode-number if episode.Number != -1 span= episode.Number From 8ec7adb7704e516ca4413e57c988d48ff7e9bbb5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 17:24:16 +0200 Subject: [PATCH 461/527] Added formatting precision for anime ratings --- mixins/Rating.pixy | 4 ++-- pages/anime/anime.pixy | 8 ++++---- pages/settings/settings.pixy | 8 ++++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/mixins/Rating.pixy b/mixins/Rating.pixy index e2a76cd3..d9479160 100644 --- a/mixins/Rating.pixy +++ b/mixins/Rating.pixy @@ -1,2 +1,2 @@ -component Rating(value float64) - .anime-rating= int(value + 0.5) \ No newline at end of file +component Rating(value float64, user *arn.User) + .anime-rating= fmt.Sprintf("%." + strconv.Itoa(user.Settings().Format.RatingsPrecision) + "f", value) \ No newline at end of file diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 8a4469e7..4edd4546 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -51,16 +51,16 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* .anime-rating-category-name Hype else .anime-rating-category-name Overall - Rating(anime.Rating.Overall) + Rating(anime.Rating.Overall, user) .anime-rating-category(title=toString(anime.Rating.Story)) .anime-rating-category-name Story - Rating(anime.Rating.Story) + Rating(anime.Rating.Story, user) .anime-rating-category(title=toString(anime.Rating.Visuals)) .anime-rating-category-name Visuals - Rating(anime.Rating.Visuals) + Rating(anime.Rating.Visuals, user) .anime-rating-category(title=toString(anime.Rating.Soundtrack)) .anime-rating-category-name Soundtrack - Rating(anime.Rating.Soundtrack) + Rating(anime.Rating.Soundtrack, user) if len(friends) > 0 h3.anime-section-name Friends diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 1195d8da..e1d80304 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -157,6 +157,14 @@ component Settings(user *arn.User) a.button.ajax(href="/shop") Icon("star") span Go PRO + + .widget.mountable(data-api="/api/settings/" + user.ID) + h3.widget-title + Icon("font") + span Formatting + + .widget-section + InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings?", "0", "2", "1") //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title From 84db28eb72a46159a84c519907b3057615b05155 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 17:30:19 +0200 Subject: [PATCH 462/527] More precise tooltip --- pages/settings/settings.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index e1d80304..71863be3 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -164,7 +164,7 @@ component Settings(user *arn.User) span Formatting .widget-section - InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings?", "0", "2", "1") + InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title From 5fbdcbfb6a21b94fcf850216292a48ff277e9181 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 17:37:23 +0200 Subject: [PATCH 463/527] Fixed wrong position of anime action buttons --- pages/anime/anime.scarlet | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index a7635f19..3de2745c 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -62,9 +62,8 @@ // Setting z-index requires setting a background as well z-index 10 - background-color bg-color -> 900px +> 1450px .anime-actions position absolute top 0 From 4bbe0ea7d54ce418dc6a07c2e94753d18d038eeb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 18:04:30 +0200 Subject: [PATCH 464/527] Added title language setting --- pages/anime/anime.pixy | 2 +- pages/settings/settings.pixy | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 4edd4546..7c4053ed 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -16,7 +16,7 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* .space .anime-info - h1.anime-title(title=anime.Type)= anime.Title.Canonical + h1.anime-title(title=anime.Type)= anime.Title.ByUser(user) //- if user && user.titleLanguage === "japanese" //- span.second-title(title=anime.Title.English !== anime.Title.Romaji ? anime.Title.English : null)= anime.Title.Romaji diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 71863be3..82a274c1 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -164,7 +164,15 @@ component Settings(user *arn.User) span Formatting .widget-section - InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") + label(for="TitleLanguage")= "Title language:" + select.widget-ui-element.action(id="TitleLanguage", data-field="TitleLanguage", value=user.Settings().TitleLanguage, title="Language of anime titles", data-action="save", data-trigger="change") + option(value="canonical") Canonical + option(value="english") English + option(value="japanese") Japanese + option(value="romaji") Romaji + + InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") + //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title From d8848c7d56538d9c7a341c425aa79abbb7afa4b5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 18:20:45 +0200 Subject: [PATCH 465/527] Applied title language setting to anime lists --- pages/anime/anime.pixy | 2 +- pages/animelist/animelist.pixy | 4 ++-- pages/animelistitem/animelistitem.go | 6 +++--- pages/animelistitem/animelistitem.pixy | 4 ++-- pages/dashboard/dashboard.go | 2 +- pages/dashboard/dashboard.pixy | 4 ++-- pages/profile/profile.pixy | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 7c4053ed..1b68f65a 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -4,7 +4,7 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* .anime-header(data-id=anime.ID) if anime.Image.Small != "" .anime-image-container - img.anime-cover-image(src=anime.Image.Small, alt=anime.Title.Canonical) + img.anime-cover-image(src=anime.Image.Small, alt=anime.Title.ByUser(user)) if anime.StartDate != "" .anime-start-date diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index b0aea99a..96b7e4b1 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -45,10 +45,10 @@ component AnimeList(animeList *arn.AnimeList, viewUser *arn.User, user *arn.User tr.anime-list-item.mountable(title=item.Notes, data-api="/api/animelist/" + animeList.UserID + "/field/Items[AnimeID=\"" + item.AnimeID + "\"]") td.anime-list-item-image-container a.ajax(href=item.Anime().Link()) - img.anime-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + img.anime-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.ByUser(user)) td.anime-list-item-name - a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.Canonical + a.ajax(href=item.Link(animeList.User().Nick))= item.Anime().Title.ByUser(user) td.anime-list-item-actions if user != nil && item.Status == arn.AnimeListStatusWatching && item.Anime().EpisodeByNumber(item.Episodes + 1) != nil diff --git a/pages/animelistitem/animelistitem.go b/pages/animelistitem/animelistitem.go index 5afa5a56..97365146 100644 --- a/pages/animelistitem/animelistitem.go +++ b/pages/animelistitem/animelistitem.go @@ -7,12 +7,12 @@ import ( "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" ) // Get anime page. func Get(ctx *aero.Context) string { - // user := utils.GetUser(ctx) - + user := utils.GetUser(ctx) nick := ctx.Get("nick") viewUser, err := arn.GetUserByNick(nick) @@ -35,7 +35,7 @@ func Get(ctx *aero.Context) string { anime := item.Anime() - return ctx.HTML(components.AnimeListItem(animeList.User(), item, anime)) + return ctx.HTML(components.AnimeListItem(animeList.User(), item, anime, user)) } // t := reflect.TypeOf(item).Elem() diff --git a/pages/animelistitem/animelistitem.pixy b/pages/animelistitem/animelistitem.pixy index 5313971e..54cb7b9e 100644 --- a/pages/animelistitem/animelistitem.pixy +++ b/pages/animelistitem/animelistitem.pixy @@ -1,7 +1,7 @@ -component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.Anime) +component AnimeListItem(viewUser *arn.User, item *arn.AnimeListItem, anime *arn.Anime, user *arn.User) .widget-form.mountable .widget.anime-list-item-view(data-api="/api/animelist/" + viewUser.ID + "/field/Items[AnimeID=\"" + anime.ID + "\"]") - h1= anime.Title.Canonical + h1= anime.Title.ByUser(user) .anime-list-item-progress-edit .anime-list-item-episodes-edit diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 1559ff10..6dac6725 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -82,5 +82,5 @@ func Get(ctx *aero.Context) string { } }) - return ctx.HTML(components.Dashboard(upcomingEpisodes, forumActivity, soundTracks, followingList)) + return ctx.HTML(components.Dashboard(upcomingEpisodes, forumActivity, soundTracks, followingList, user)) } diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index 95bbebeb..de34ab12 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -1,4 +1,4 @@ -component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, soundTracks []*arn.SoundTrack, following []*arn.User) +component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, soundTracks []*arn.SoundTrack, following []*arn.User, user *arn.User) h1.page-title Dashboard .widgets @@ -11,7 +11,7 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, sound .widget-ui-element-text a.schedule-item-link.ajax(href=schedule[i].Anime.Link()) Icon("calendar-o") - .schedule-item-title= schedule[i].Anime.Title.Canonical + .schedule-item-title= schedule[i].Anime.Title.ByUser(user) .spacer .schedule-item-date.utc-airing-date(data-start-date=schedule[i].Episode.AiringDate.Start, data-end-date=schedule[i].Episode.AiringDate.End, data-episode-number=schedule[i].Episode.Number) else diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 9d9eeb61..51e6955a 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -106,8 +106,8 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .profile-watching-list.mountable each item in animeList.Items if item.Status == arn.AnimeListStatusWatching || item.Status == arn.AnimeListStatusCompleted - a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") - img.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.ByUser(user) + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") + img.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.ByUser(user)) if user != nil && (user.Role == "admin" || user.Role == "editor") .footer From 4801a254f7f6a141e2cb039c86488438265cc1f0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 18:26:23 +0200 Subject: [PATCH 466/527] Fixed bug when viewing anime page logged out --- mixins/Rating.pixy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mixins/Rating.pixy b/mixins/Rating.pixy index d9479160..775f6a82 100644 --- a/mixins/Rating.pixy +++ b/mixins/Rating.pixy @@ -1,2 +1,5 @@ component Rating(value float64, user *arn.User) - .anime-rating= fmt.Sprintf("%." + strconv.Itoa(user.Settings().Format.RatingsPrecision) + "f", value) \ No newline at end of file + if user == nil + .anime-rating= fmt.Sprintf("%.1f", value) + else + .anime-rating= fmt.Sprintf("%." + strconv.Itoa(user.Settings().Format.RatingsPrecision) + "f", value) \ No newline at end of file From 9448bc25d5e2965bd18ce004ff194c9a4ecd8581 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 18:46:50 +0200 Subject: [PATCH 467/527] Minor changes --- pages/anime/anime.pixy | 4 ++-- pages/settings/settings.pixy | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 1b68f65a..9efa25f4 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -81,8 +81,8 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* h3.anime-section-name Relations .anime-relations each relation in anime.Relations().Items - a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.Canonical) - img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.Canonical) + a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) + img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) .anime-relation-type= relation.HumanReadableType() .anime-relation-year if relation.Anime().StartDate != "" diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 82a274c1..5521671e 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -1,5 +1,6 @@ component Settings(user *arn.User) h1.page-title Settings + .widgets .widget.mountable(data-api="/api/user/" + user.ID) h3.widget-title @@ -168,8 +169,8 @@ component Settings(user *arn.User) select.widget-ui-element.action(id="TitleLanguage", data-field="TitleLanguage", value=user.Settings().TitleLanguage, title="Language of anime titles", data-action="save", data-trigger="change") option(value="canonical") Canonical option(value="english") English - option(value="japanese") Japanese option(value="romaji") Romaji + option(value="japanese") 日本語 InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") From 6393e91cc2c785ef0a80925c5f4b5203362f4363 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 20 Oct 2017 20:57:41 +0200 Subject: [PATCH 468/527] Started working on group posts --- pages/group/group.pixy | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/group/group.pixy b/pages/group/group.pixy index 41c8aecd..7e2a13bc 100644 --- a/pages/group/group.pixy +++ b/pages/group/group.pixy @@ -25,7 +25,11 @@ component Group(group *arn.Group) Avatar(member.User()) .group-feed.mountable - p Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin fermentum tellus congue, placerat augue vel, porta tortor. Nunc in elementum enim. Vestibulum ut arcu sed diam dapibus feugiat. Nam posuere, lectus et pellentesque interdum, mi orci aliquet lacus, a posuere lacus mi ac urna. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec suscipit enim nec dui consectetur, vitae pulvinar urna commodo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. + if len(group.Posts()) == 0 + p.text-center.mountable No posts in this group yet. + else + each post in group.Posts() + p!= post.HTML() component GroupTabs(group *arn.Group) .tabs From 26422408cef269151fbfbd87490a6a0750fe382a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Oct 2017 05:28:52 +0200 Subject: [PATCH 469/527] Started working on anime images --- images/anime/.gitignore | 2 ++ jobs/anime-images/anime-images.go | 57 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 images/anime/.gitignore create mode 100644 jobs/anime-images/anime-images.go diff --git a/images/anime/.gitignore b/images/anime/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/images/anime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/jobs/anime-images/anime-images.go b/jobs/anime-images/anime-images.go new file mode 100644 index 00000000..f92c4c84 --- /dev/null +++ b/jobs/anime-images/anime-images.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/aerogo/flow/jobqueue" + "github.com/animenotifier/arn" + "github.com/fatih/color" + "github.com/parnurzeal/gorequest" +) + +var ticker = time.NewTicker(50 * time.Millisecond) + +func main() { + color.Yellow("Downloading anime images") + jobs := jobqueue.New(work) + allAnime, _ := arn.AllAnime() + + for _, anime := range allAnime { + jobs.Queue(anime) + } + + results := jobs.Wait() + color.Green("Finished downloading %d anime images.", len(results)) +} + +func work(job interface{}) interface{} { + anime := job.(*arn.Anime) + + if !strings.HasPrefix(anime.Image.Original, "https://media.kitsu.io/anime/") { + return nil + } + + <-ticker.C + resp, body, errs := gorequest.New().Get(anime.Image.Original).End() + + if len(errs) > 0 { + color.Red(errs[0].Error()) + return errs[0] + } + + if resp.StatusCode != http.StatusOK { + color.Red("Status %d", resp.StatusCode) + } + + extension := anime.Image.Original[strings.LastIndex(anime.Image.Original, "."):] + fileName := "anime/" + anime.ID + extension + fmt.Println(fileName) + + ioutil.WriteFile(fileName, []byte(body), 0644) + + return nil +} From 1a693fec05e5b9c723496824e3c33b041f8859ef Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Oct 2017 17:07:25 +0200 Subject: [PATCH 470/527] Minor changes --- middleware/UserInfo.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index f2888793..2a3e825f 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -1,17 +1,16 @@ package middleware import ( - "encoding/json" "net/http" "strconv" "strings" "github.com/aerogo/aero" + "github.com/aerogo/http/client" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/utils" "github.com/fatih/color" "github.com/mssola/user_agent" - "github.com/parnurzeal/gorequest" ) // UserInfo updates user related information after each request. @@ -73,21 +72,20 @@ func updateUserInfo(ctx *aero.Context, user *arn.User) { func updateUserLocation(user *arn.User, newIP string) { user.IP = newIP locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + arn.APIKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json" + response, err := client.Get(locationAPI).End() - response, data, err := gorequest.New().Get(locationAPI).EndBytes() - - if len(err) > 0 && err[0] != nil { - color.Red("Couldn't fetch location data | Error: %s | IP: %s", err[0].Error(), user.IP) + if err != nil { + color.Red("Couldn't fetch location data | Error: %s | IP: %s", err.Error(), user.IP) return } - if response.StatusCode != http.StatusOK { + if response.StatusCode() != http.StatusOK { color.Red("Couldn't fetch location data | Status: %d | IP: %s", response.StatusCode, user.IP) return } newLocation := arn.IPInfoDBLocation{} - json.Unmarshal(data, &newLocation) + response.Unmarshal(&newLocation) if newLocation.CountryName != "-" { user.Location.CountryName = newLocation.CountryName From 6c0f8a6318aa163e621a35e9ae90db43d4c58093 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Oct 2017 19:16:11 +0200 Subject: [PATCH 471/527] Added anime list comparison --- main.go | 4 ++ pages/animelist/animelist.scarlet | 1 + pages/compare/animelist.go | 74 +++++++++++++++++++++++++++++++ pages/compare/animelist.pixy | 65 +++++++++++++++++++++++++++ pages/compare/animelist.scarlet | 14 ++++++ pages/profile/profile.pixy | 4 ++ utils/Comparison.go | 10 +++++ 7 files changed, 172 insertions(+) create mode 100644 pages/compare/animelist.go create mode 100644 pages/compare/animelist.pixy create mode 100644 pages/compare/animelist.scarlet create mode 100644 utils/Comparison.go diff --git a/main.go b/main.go index 5d5d9f89..0e62f80d 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/animenotifier/notify.moe/pages/best" "github.com/animenotifier/notify.moe/pages/character" "github.com/animenotifier/notify.moe/pages/charge" + "github.com/animenotifier/notify.moe/pages/compare" "github.com/animenotifier/notify.moe/pages/dashboard" "github.com/animenotifier/notify.moe/pages/database" "github.com/animenotifier/notify.moe/pages/editanime" @@ -136,6 +137,9 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/animelist/hold", home.FilterByStatus(arn.AnimeListStatusHold)) app.Ajax("/animelist/dropped", home.FilterByStatus(arn.AnimeListStatusDropped)) + // Compare + app.Ajax("/compare/animelist/:nick-1/:nick-2", compare.AnimeList) + // Search app.Ajax("/search", search.Get) app.Ajax("/search/:term", search.Get) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 76910ce7..7c7bc4ca 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -17,6 +17,7 @@ .anime-list-item-image-container padding 0 + width 39px .anime-list-item-image width 39px diff --git a/pages/compare/animelist.go b/pages/compare/animelist.go new file mode 100644 index 00000000..2c825c6e --- /dev/null +++ b/pages/compare/animelist.go @@ -0,0 +1,74 @@ +package compare + +import ( + "net/http" + "sort" + + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" +) + +// AnimeList ... +func AnimeList(ctx *aero.Context) string { + user := utils.GetUser(ctx) + nickA := ctx.Get("nick-1") + nickB := ctx.Get("nick-2") + + a, err := arn.GetUserByNick(nickA) + + if err != nil || a == nil { + return ctx.Error(http.StatusNotFound, "User not found: "+nickA, err) + } + + b, err := arn.GetUserByNick(nickB) + + if err != nil || b == nil { + return ctx.Error(http.StatusNotFound, "User not found: "+nickB, err) + } + + comparisons := []*utils.Comparison{} + + for _, item := range a.AnimeList().Items { + if item.Status == arn.AnimeListStatusPlanned { + continue + } + + comparisons = append(comparisons, &utils.Comparison{ + Anime: item.Anime(), + ItemA: item, + ItemB: b.AnimeList().Find(item.AnimeID), + }) + } + + for _, item := range b.AnimeList().Items { + if Contains(comparisons, item.AnimeID) || item.Status == arn.AnimeListStatusPlanned { + continue + } + + comparisons = append(comparisons, &utils.Comparison{ + Anime: item.Anime(), + ItemA: a.AnimeList().Find(item.AnimeID), + ItemB: item, + }) + } + + sort.Slice(comparisons, func(i, j int) bool { + return comparisons[i].Anime.Popularity.Total() > comparisons[j].Anime.Popularity.Total() + }) + + return ctx.HTML(components.CompareAnimeList(a, b, comparisons, user)) +} + +// Contains ... +func Contains(comparisons []*utils.Comparison, animeID string) bool { + for _, comparison := range comparisons { + if comparison.Anime.ID == animeID { + return true + } + } + + return false +} diff --git a/pages/compare/animelist.pixy b/pages/compare/animelist.pixy new file mode 100644 index 00000000..5afbe1b8 --- /dev/null +++ b/pages/compare/animelist.pixy @@ -0,0 +1,65 @@ +component CompareAnimeList(a *arn.User, b *arn.User, comparisons []*utils.Comparison, user *arn.User) + h1= "Anime list comparison between " + a.Nick + " and " + b.Nick + + table.anime-list + thead + tr.anime-list-item.mountable + th.anime-list-item-image-container + th.anime-list-item-name + th.comparison + Avatar(a) + th.comparison + th.comparison + Avatar(b) + th.comparison + + tbody + each comparison in comparisons + tr.anime-list-item.mountable + td.anime-list-item-image-container + a.ajax(href=comparison.Anime.Link()) + img.anime-list-item-image.lazy(data-src=comparison.Anime.Image.Tiny, alt=comparison.Anime.Title.ByUser(user)) + + td.anime-list-item-name + a.ajax(href=comparison.Anime.Link())= comparison.Anime.Title.ByUser(user) + + td.comparison + if comparison.ItemA != nil + span= comparison.ItemA.Status + else + span - + + td.comparison + if comparison.ItemA != nil + if comparison.ItemA.Rating.Overall != 0 + if comparison.ItemB != nil && comparison.ItemB.Rating.Overall != 0 && comparison.ItemA.Rating.Overall == comparison.ItemB.Rating.Overall + span.comparison-rating-equal= utils.FormatRating(comparison.ItemA.Rating.Overall) + else + span= utils.FormatRating(comparison.ItemA.Rating.Overall) + else + span - + else + span - + + td.comparison + if comparison.ItemB != nil + span= comparison.ItemB.Status + else + span - + + td.comparison + if comparison.ItemB != nil + if comparison.ItemB.Rating.Overall != 0 + if comparison.ItemA != nil && comparison.ItemA.Rating.Overall != 0 + if comparison.ItemA.Rating.Overall == comparison.ItemB.Rating.Overall + span.comparison-rating-equal= utils.FormatRating(comparison.ItemB.Rating.Overall) + else if comparison.ItemB.Rating.Overall > comparison.ItemA.Rating.Overall + span.comparison-rating-higher(title=utils.FormatRating(comparison.ItemB.Rating.Overall))= "+" + utils.FormatRating(comparison.ItemB.Rating.Overall - comparison.ItemA.Rating.Overall) + else + span.comparison-rating-lower(title=utils.FormatRating(comparison.ItemB.Rating.Overall))= "-" + utils.FormatRating(comparison.ItemA.Rating.Overall - comparison.ItemB.Rating.Overall) + else + span= utils.FormatRating(comparison.ItemB.Rating.Overall) + else + span - + else + span - \ No newline at end of file diff --git a/pages/compare/animelist.scarlet b/pages/compare/animelist.scarlet new file mode 100644 index 00000000..741684a8 --- /dev/null +++ b/pages/compare/animelist.scarlet @@ -0,0 +1,14 @@ +.comparison + width 100px + text-align center + horizontal + justify-content center + +.comparison-rating-equal + // ... + +.comparison-rating-lower + color red + +.comparison-rating-higher + color green \ No newline at end of file diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 51e6955a..94b0f040 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -61,6 +61,10 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) button.profile-action.action(data-action="unfollowUser", data-trigger="click", data-api="/api/userfollows/" + user.ID + "/remove/" + viewUser.ID) Icon("user-times") span Unfollow + + a.button.profile-action.ajax(href="/compare/animelist/" + user.Nick + "/" + viewUser.Nick) + Icon("exchange") + span Compare ProfileNavigation(viewUser, uri) diff --git a/utils/Comparison.go b/utils/Comparison.go new file mode 100644 index 00000000..4e3bd4a5 --- /dev/null +++ b/utils/Comparison.go @@ -0,0 +1,10 @@ +package utils + +import "github.com/animenotifier/arn" + +// Comparison of an anime between 2 users. +type Comparison struct { + Anime *arn.Anime + ItemA *arn.AnimeListItem + ItemB *arn.AnimeListItem +} From fd03d311874b44030684ae6c07d3ddcf6b104158 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Oct 2017 19:23:40 +0200 Subject: [PATCH 472/527] Improved anime list comparison --- pages/compare/animelist.go | 14 ++++++++++++-- pages/compare/animelist.pixy | 6 ++++-- pages/compare/animelist.scarlet | 7 +++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pages/compare/animelist.go b/pages/compare/animelist.go index 2c825c6e..784c661d 100644 --- a/pages/compare/animelist.go +++ b/pages/compare/animelist.go @@ -30,12 +30,16 @@ func AnimeList(ctx *aero.Context) string { } comparisons := []*utils.Comparison{} + countA := 0 + countB := 0 for _, item := range a.AnimeList().Items { if item.Status == arn.AnimeListStatusPlanned { continue } + countA++ + comparisons = append(comparisons, &utils.Comparison{ Anime: item.Anime(), ItemA: item, @@ -44,7 +48,13 @@ func AnimeList(ctx *aero.Context) string { } for _, item := range b.AnimeList().Items { - if Contains(comparisons, item.AnimeID) || item.Status == arn.AnimeListStatusPlanned { + if item.Status == arn.AnimeListStatusPlanned { + continue + } + + countB++ + + if Contains(comparisons, item.AnimeID) { continue } @@ -59,7 +69,7 @@ func AnimeList(ctx *aero.Context) string { return comparisons[i].Anime.Popularity.Total() > comparisons[j].Anime.Popularity.Total() }) - return ctx.HTML(components.CompareAnimeList(a, b, comparisons, user)) + return ctx.HTML(components.CompareAnimeList(a, b, countA, countB, comparisons, user)) } // Contains ... diff --git a/pages/compare/animelist.pixy b/pages/compare/animelist.pixy index 5afbe1b8..d1aeadee 100644 --- a/pages/compare/animelist.pixy +++ b/pages/compare/animelist.pixy @@ -1,5 +1,7 @@ -component CompareAnimeList(a *arn.User, b *arn.User, comparisons []*utils.Comparison, user *arn.User) - h1= "Anime list comparison between " + a.Nick + " and " + b.Nick +component CompareAnimeList(a *arn.User, b *arn.User, countA int, countB int, comparisons []*utils.Comparison, user *arn.User) + h1 Anime list comparison + + p.comparison-info= a.Nick + "'s list contains " + strconv.Itoa(countA) + " anime and " + b.Nick + "'s list contains " + strconv.Itoa(countB) + " anime." table.anime-list thead diff --git a/pages/compare/animelist.scarlet b/pages/compare/animelist.scarlet index 741684a8..13ca95ae 100644 --- a/pages/compare/animelist.scarlet +++ b/pages/compare/animelist.scarlet @@ -1,3 +1,10 @@ +.comparison-info + text-align center + font-size 0.9rem + opacity 0.5 + margin-top 0 + margin-bottom content-padding + .comparison width 100px text-align center From 066ce01c12122ec2920464f64334f7910d45d3a5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Oct 2017 20:06:37 +0200 Subject: [PATCH 473/527] Fixed image size in embedded view --- styles/embedded.scarlet | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/styles/embedded.scarlet b/styles/embedded.scarlet index 7a0fd9f8..c0b8aef4 100644 --- a/styles/embedded.scarlet +++ b/styles/embedded.scarlet @@ -19,7 +19,8 @@ remove-margin = 1.1rem .anime-list-owner display none - .anime-list-item-image + .anime-list-item-image, + .anime-list-item-image-container width 27px #navigation From 79ab2daee78129edd84f8f3233f8d482005159b0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Oct 2017 20:49:14 +0200 Subject: [PATCH 474/527] Updated discord bot --- bots/discord/discord.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bots/discord/discord.go b/bots/discord/discord.go index f656a56f..4c9c974a 100644 --- a/bots/discord/discord.go +++ b/bots/discord/discord.go @@ -95,6 +95,11 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) { return } + if strings.HasPrefix(m.Content, "!play ") { + s.UpdateStatus(0, strings.Split(m.Content, " ")[1]) + return + } + if strings.HasPrefix(m.Content, "!s ") { term := m.Content[len("!s "):] users, animes, posts, threads := arn.Search(term, 3, 3, 3, 3) From 664debf024bfd5cdfc956bbd2afedd9c08c2ccdc Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Oct 2017 22:47:04 +0200 Subject: [PATCH 475/527] Minor change --- jobs/sync-media-relations/sync-media-relations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/sync-media-relations/sync-media-relations.go b/jobs/sync-media-relations/sync-media-relations.go index 71206674..019e5c4f 100644 --- a/jobs/sync-media-relations/sync-media-relations.go +++ b/jobs/sync-media-relations/sync-media-relations.go @@ -14,7 +14,7 @@ func main() { color.Yellow("Syncing media relations with Kitsu DB") kitsuMediaRelations := kitsu.StreamMediaRelations() - relations := map[arn.AnimeID]*arn.AnimeRelations{} + relations := map[string]*arn.AnimeRelations{} for mediaRelation := range kitsuMediaRelations { // We only care about anime for now From ae32a1a6cbf5fd155179627c5df48fdc134ddef2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 00:17:35 +0200 Subject: [PATCH 476/527] Added datalists --- mixins/Input.pixy | 4 +++- utils/editform/editform.go | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index a85ac9c2..c6984dfa 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -13,10 +13,12 @@ component InputNumber(id string, value float64, label string, placeholder string label(for=id)= label + ":" input.widget-ui-element.action(id=id, data-field=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, values []string) +component InputSelection(id string, value string, label string, placeholder string, options []*arn.Option) .widget-section label(for=id)= label + ":" select.widget-ui-element.action(id=id, data-field=id, value=value, title=placeholder, data-action="save", data-trigger="change") + each option in options + option(value=option.Value)= option.Label component InputTags(id string, value []string, label string, tooltip string) .widget-section diff --git a/utils/editform/editform.go b/utils/editform/editform.go index 96bd36dc..33756e24 100644 --- a/utils/editform/editform.go +++ b/utils/editform/editform.go @@ -66,10 +66,14 @@ func RenderField(b *bytes.Buffer, v *reflect.Value, field reflect.StructField, i switch field.Type.String() { case "string": - if field.Tag.Get("type") == "textarea" { - b.WriteString(components.InputTextArea(idPrefix+field.Name, fieldValue.String(), field.Name, "")) + if field.Tag.Get("datalist") != "" { + dataList := field.Tag.Get("datalist") + values := arn.DataLists[dataList] + b.WriteString(components.InputSelection(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip"), values)) + } else if field.Tag.Get("type") == "textarea" { + b.WriteString(components.InputTextArea(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip"))) } else { - b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, "")) + b.WriteString(components.InputText(idPrefix+field.Name, fieldValue.String(), field.Name, field.Tag.Get("tooltip"))) } case "[]string": b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name, field.Tag.Get("tooltip"))) From b86b41fd8de8c172513e9547b730472516b275c0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 13:02:29 +0200 Subject: [PATCH 477/527] Minor change --- Installation.md => INSTALLATION.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Installation.md => INSTALLATION.md (100%) diff --git a/Installation.md b/INSTALLATION.md similarity index 100% rename from Installation.md rename to INSTALLATION.md From f4d1801e676c00e3b3bf5f8e1f635ed6a849c4f2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 13:12:22 +0200 Subject: [PATCH 478/527] Removed go test script --- go-test-color.sh | 114 ----------------------------------------------- 1 file changed, 114 deletions(-) delete mode 100755 go-test-color.sh diff --git a/go-test-color.sh b/go-test-color.sh deleted file mode 100755 index e184ba1c..00000000 --- a/go-test-color.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash - -# Colorizing Go test output: -# This is meant to be used in place of `go test`. Provided this script is in -# your PATH, calling `color-go-test` will call through to `go test` and then -# colorize and reformat the output. - -RED=$(tput setaf 1) -GREEN=$(tput setaf 2) -YELLOW=$(tput setaf 3) -COLOR_RESET=$(tput sgr0) -BOLD=$(tput bold) - -previous_line_fail=false -verbose_output=false -verbose_flag_prefix="-v " -pass_count=0 -fail_count=0 -errors=() - -echo_last_line() { - local time_string=$1 - local color=$GREEN - if [ $verbose_output = false ]; then - echo -e "\n" - if [ ${#errors[@]} -gt 0 ]; then - for error in "${errors[@]}"; do - echo -e "$error" - done - fi - fi - if [ $fail_count -gt 0 ]; then - color=$RED - fi - local num_tests=$((pass_count + fail_count)) - echo -e "\n${color}${BOLD}$num_tests tests, $fail_count failure, run time ($time_string)${COLOR_RESET}" -} - -colorize_output() { - while read line; do - if echo $line | grep --quiet '^FAIL$'; then - continue - - elif echo $line | grep --quiet '^PASS$'; then - continue - - elif echo $line | grep --quiet '^=== RUN'; then - continue - - elif echo $line | grep --quiet '^exit status 1$'; then - continue - - elif echo $line | grep --quiet 'FAIL'; then - if echo $line | grep --quiet "\-\-\- FAIL:"; then - fail_count=$((fail_count + 1)) - error_message="${RED}$(echo $line | sed 's/--- FAIL:/✗/')${COLOR_RESET}" - - if [ $verbose_output = true ]; then - echo $error_message - else - errors+=("$error_message") - printf "${RED}.${COLOR_RESET}" - fi - previous_line_fail=true - else - local test_run_time=$(echo $line | grep -o '[0-9]*\.[0-9]*s$') - echo_last_line $test_run_time - previous_line_fail=false - fi - - elif [ $previous_line_fail = true ]; then - error_message=" ${YELLOW}➝ $line${COLOR_RESET}" - if [ $verbose_output = true ]; then - echo -e "$error_message" - else - errors+=("$error_message") - fi - previous_line_fail=false - - elif echo $line | grep --quiet 'PASS'; then - if echo $line | grep --quiet "\-\-\- PASS:"; then - if [ $verbose_output = true ]; then - echo "${GREEN}$(echo $line | sed 's/--- PASS:/✔/')${COLOR_RESET}" - else - printf "${GREEN}.${COLOR_RESET}" - fi - pass_count=$((pass_count + 1)) - else - local test_run_time=$(echo $line | grep -o '[0-9]*\.[0-9]*s$') - echo_last_line $test_run_time - fi - - previous_line_fail=false - - elif echo $line | grep --quiet '^ok '; then - local test_run_time=$(echo $line | grep -o '[0-9]*\.[0-9]*s$') - echo_last_line $test_run_time - previous_line_fail=false - - else - echo $line - previous_line_fail=false - fi - done -} - -for flag in $@; do - if [ "$flag" = "-v" ]; then - verbose_output=true - verbose_flag_prefix="" - fi -done - -go test ${verbose_flag_prefix}$@ | colorize_output From 19ebaf87252740862121df7d51218f2c01736683 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 14:36:55 +0200 Subject: [PATCH 479/527] Added beatmap support --- pages/soundtrack/soundtrack.pixy | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pages/soundtrack/soundtrack.pixy b/pages/soundtrack/soundtrack.pixy index b695b439..3beb4d91 100644 --- a/pages/soundtrack/soundtrack.pixy +++ b/pages/soundtrack/soundtrack.pixy @@ -22,6 +22,14 @@ component Track(track *arn.SoundTrack) a.sound-track-anime-list-item.ajax(href=anime.Link(), title=anime.Title.Canonical) img.sound-track-anime-list-item-image.lazy(data-src=anime.Image.Tiny, alt=anime.Title.Canonical) + if len(track.Beatmaps()) > 0 + .widget.mountable + h3.widget-title Beatmaps + ul.beatmaps + for index, beatmap := range track.Beatmaps() + li + a.beatmap(href="https://osu.ppy.sh/s/" + beatmap, target="_blank")= "Beatmap #" + strconv.Itoa(index + 1) + .widget.mountable h3.widget-title Tags .tags From a051e5e2abf0997905b92fa952ddb72efc257915 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 15:57:27 +0200 Subject: [PATCH 480/527] Updated installation --- INSTALLATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 0e825372..4998f96a 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -28,7 +28,7 @@ namespace arn { storage-engine device { file /home/YOUR_NAME/YOUR_PATH/notify.moe/db/arn-dev.dat - filesize 64M + filesize 300M data-in-memory true # Maximum object size. 128K is ideal for SSDs but we need 1M for search indices. From 3e4278f1cd42423bb65597cdb040d0b012166c5b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 16:50:22 +0200 Subject: [PATCH 481/527] Updated database link --- INSTALLATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 4998f96a..2f689bbd 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -40,7 +40,7 @@ namespace arn { } ``` -* Download the [database for developers](https://mega.nz/#!iN4WTRxb!R_cRjBbnUUvGeXdtRGiqbZRrnvy0CHc2MjlyiGBxdP4) to notify.moe/db/arn-dev.dat +* Download the [database for developers](https://mega.nz/#!yFAiSIzI!rlbM4_3WK9hH2OfAt44xGWnWMWs-kVHhvC_DTKyRMBQ) to notify.moe/db/arn-dev.dat * Start the database using `sudo service aerospike start` * Confirm that the status is "green": `sudo service aerospike status` From 43fb54ac499cf9af434d394d37dd9eee8e88a5f9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 16:57:27 +0200 Subject: [PATCH 482/527] Added typescript to installation instructions --- INSTALLATION.md | 1 + 1 file changed, 1 insertion(+) diff --git a/INSTALLATION.md b/INSTALLATION.md index 2f689bbd..e1bbfcbb 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -6,6 +6,7 @@ * Install a Debian based operating system * Install [Go](https://golang.org/dl/) (1.9 or higher) +* Install [TypeScript](https://www.typescriptlang.org/) (2.5 or higher) * Install [Aerospike](http://www.aerospike.com/download) (3.14.0 or higher) ### Download the repository and its dependencies From 0d09c488fc9dac1cc112bf6b48b91cb39423108b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 22 Oct 2017 17:02:36 +0200 Subject: [PATCH 483/527] Added more route tests --- tests.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests.go b/tests.go index 51d3579c..0e8fe3e7 100644 --- a/tests.go +++ b/tests.go @@ -59,6 +59,18 @@ var routeTests = map[string][]string{ "/anime/1", }, + "/anime/:id/characters": []string{ + "/anime/1/characters", + }, + + "/anime/:id/episodes": []string{ + "/anime/1/episodes", + }, + + "/anime/:id/tracks": []string{ + "/anime/1/tracks", + }, + "/thread/:id": []string{ "/thread/HJgS7c2K", }, @@ -79,10 +91,22 @@ var routeTests = map[string][]string{ "/soundtrack/h0ac8sKkg", }, + "/soundtrack/:id/edit": []string{ + "/soundtrack/h0ac8sKkg/edit", + }, + + "/soundtracks/from/:index": []string{ + "/soundtracks/from/12", + }, + "/character/:id": []string{ "/character/6556", }, + "/compare/animelist/:nick-1/:nick-2": []string{ + "/compare/animelist/Akyoto/Scott", + }, + // API "/api/anime/:id": []string{ "/api/anime/1", From b6062328eba5e7c947ceb5b2994558dd7222ef16 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 24 Oct 2017 01:32:12 +0200 Subject: [PATCH 484/527] Minor change --- jobs/anime-images/anime-images.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobs/anime-images/anime-images.go b/jobs/anime-images/anime-images.go index f92c4c84..08a68f46 100644 --- a/jobs/anime-images/anime-images.go +++ b/jobs/anime-images/anime-images.go @@ -31,7 +31,7 @@ func main() { func work(job interface{}) interface{} { anime := job.(*arn.Anime) - if !strings.HasPrefix(anime.Image.Original, "https://media.kitsu.io/anime/") { + if !strings.HasPrefix(anime.Image.Original, "//media.kitsu.io/anime/") { return nil } From 68c8f3b2fe0ba1c6a151a996cd5d0c07d97374ba Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 24 Oct 2017 13:11:36 +0200 Subject: [PATCH 485/527] Removed fetch data step --- INSTALLATION.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index e1bbfcbb..06e3e973 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -69,10 +69,6 @@ namespace arn { } ``` -### Fetch data - -* Run `jobs/sync-anime/sync-anime` from this repository to fetch anime data - ### Run * Start the web server in notify.moe directory: `run` From a3609d2897e74956f1537159784e923c975fc348 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 25 Oct 2017 23:34:39 +0200 Subject: [PATCH 486/527] Simplified installation --- INSTALLATION.md | 39 ++++++++------------------------------- images/anime/.gitignore | 2 -- 2 files changed, 8 insertions(+), 33 deletions(-) delete mode 100644 images/anime/.gitignore diff --git a/INSTALLATION.md b/INSTALLATION.md index 06e3e973..b5d983ff 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -7,7 +7,6 @@ * Install a Debian based operating system * Install [Go](https://golang.org/dl/) (1.9 or higher) * Install [TypeScript](https://www.typescriptlang.org/) (2.5 or higher) -* Install [Aerospike](http://www.aerospike.com/download) (3.14.0 or higher) ### Download the repository and its dependencies @@ -18,42 +17,20 @@ * Run `make tools` to install [pack](https://github.com/aerogo/pack) & [run](https://github.com/aerogo/run) * Run `make all` * Run `make ports` to set up local port forwarding *(80 to 4000, 443 to 4001)* -* You should be able to start the server by executing `run` now - -### Database - -* Remove all namespaces in `/etc/aerospike/aerospike.conf` -* Add a namespace called `arn`: - -``` -namespace arn { - storage-engine device { - file /home/YOUR_NAME/YOUR_PATH/notify.moe/db/arn-dev.dat - filesize 300M - data-in-memory true - - # Maximum object size. 128K is ideal for SSDs but we need 1M for search indices. - write-block-size 1M - - # Write block size x Post write queue = Cache memory usage (for write block buffers) - post-write-queue 1 - } -} -``` - -* Download the [database for developers](https://mega.nz/#!yFAiSIzI!rlbM4_3WK9hH2OfAt44xGWnWMWs-kVHhvC_DTKyRMBQ) to notify.moe/db/arn-dev.dat -* Start the database using `sudo service aerospike start` -* Confirm that the status is "green": `sudo service aerospike status` ### Hosts -* Add `127.0.0.1 arn-db` to `/etc/hosts` -* Add `127.0.0.1 beta.notify.moe` to `/etc/hosts` +* Add the following lines to `/etc/hosts`: + +``` +127.0.0.1 arn-db +45.32.159.101 beta.notify.moe +``` ### HTTPS -* Create the certificate `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) -* Create the private key `notify.moe/security/privkey.pem` +* [Create the certificate](https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl) `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) +* Create the private key `notify.moe/security/privkey.pem` (make sure it's decoded) ### API keys diff --git a/images/anime/.gitignore b/images/anime/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/images/anime/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file From 47e845f8924e5443bd77fb7db9a5443e1958ddcb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 25 Oct 2017 23:53:57 +0200 Subject: [PATCH 487/527] Fixed installation --- INSTALLATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index b5d983ff..0e5b8ef0 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -23,8 +23,8 @@ * Add the following lines to `/etc/hosts`: ``` -127.0.0.1 arn-db -45.32.159.101 beta.notify.moe +45.32.159.101 arn-db +127.0.0.1 beta.notify.moe ``` ### HTTPS From c87bc71f896cef9190fb1a399d1b2a4271155669 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 26 Oct 2017 00:04:32 +0200 Subject: [PATCH 488/527] Reverted installation changes --- INSTALLATION.md | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 0e5b8ef0..ef45b52b 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -7,6 +7,7 @@ * Install a Debian based operating system * Install [Go](https://golang.org/dl/) (1.9 or higher) * Install [TypeScript](https://www.typescriptlang.org/) (2.5 or higher) +* Install [Aerospike](http://www.aerospike.com/download) (3.14.0 or higher) ### Download the repository and its dependencies @@ -18,19 +19,40 @@ * Run `make all` * Run `make ports` to set up local port forwarding *(80 to 4000, 443 to 4001)* +### Database + +* Remove all namespaces in `/etc/aerospike/aerospike.conf` +* Add a namespace called `arn`: + +``` +namespace arn { + storage-engine device { + file /home/YOUR_NAME/YOUR_PATH/notify.moe/db/arn-dev.dat + filesize 300M + data-in-memory true + + # Maximum object size. 128K is ideal for SSDs but we need 1M for search indices. + write-block-size 1M + + # Write block size x Post write queue = Cache memory usage (for write block buffers) + post-write-queue 1 + } +} +``` + +* Download the database for developers (get in contact with me to receive a link) +* Start the database using `sudo service aerospike start` +* Confirm that the status is "green": `sudo service aerospike status` + ### Hosts -* Add the following lines to `/etc/hosts`: - -``` -45.32.159.101 arn-db -127.0.0.1 beta.notify.moe -``` +* Add `127.0.0.1 arn-db` to `/etc/hosts` +* Add `127.0.0.1 beta.notify.moe` to `/etc/hosts` ### HTTPS -* [Create the certificate](https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl) `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) -* Create the private key `notify.moe/security/privkey.pem` (make sure it's decoded) +* Create the certificate `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) +* Create the private key `notify.moe/security/privkey.pem` ### API keys From 63757a9eddd73283619c4ed71158151a276013e1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 26 Oct 2017 04:01:26 +0200 Subject: [PATCH 489/527] Removed current installCache --- sw/service-worker.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 701fa408..901a49ca 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -329,13 +329,16 @@ class MyServiceWorker { } installCache() { - return caches.open(this.cache.version).then(cache => { - return cache.addAll([ - "./", - "./scripts", - "https://fonts.gstatic.com/s/ubuntu/v11/4iCs6KVjbNBYlgoKfw7z.ttf" - ]) - }) + // TODO: Implement a solution that caches resources with credentials: "same-origin" + return Promise.resolve() + + // return caches.open(this.cache.version).then(cache => { + // return cache.addAll([ + // "./", + // "./scripts", + // "https://fonts.gstatic.com/s/ubuntu/v11/4iCs6KVjbNBYlgoKfw7z.ttf" + // ]) + // }) } fromCache(request) { From c0d7b0d2df7c2574ae17556914061c1eb034f852 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 27 Oct 2017 02:00:54 +0200 Subject: [PATCH 490/527] Added export script for Aero DB --- patches/export-aero-db/export-aero-db.go | 273 +++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 patches/export-aero-db/export-aero-db.go diff --git a/patches/export-aero-db/export-aero-db.go b/patches/export-aero-db/export-aero-db.go new file mode 100644 index 00000000..d66c872b --- /dev/null +++ b/patches/export-aero-db/export-aero-db.go @@ -0,0 +1,273 @@ +package main + +import ( + "time" + + "github.com/aerogo/database" + "github.com/animenotifier/arn" + "github.com/fatih/color" +) + +func main() { + arn.DB.SetScanPriority("high") + + aeroDB := database.New("db", arn.DBTypes) + defer aeroDB.Close() + + for typeName := range arn.DB.Types() { + count := 0 + + switch typeName { + case "Anime": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Anime) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "AnimeEpisodes": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.AnimeEpisodes) { + aeroDB.Set(typeName, obj.AnimeID, obj) + count++ + } + + case "AnimeList": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.AnimeList) { + aeroDB.Set(typeName, obj.UserID, obj) + count++ + } + + case "AnimeCharacters": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.AnimeCharacters) { + aeroDB.Set(typeName, obj.AnimeID, obj) + count++ + } + + case "AnimeRelations": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.AnimeRelations) { + aeroDB.Set(typeName, obj.AnimeID, obj) + count++ + } + + case "Character": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Character) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "Purchase": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Purchase) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "PushSubscriptions": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.PushSubscriptions) { + aeroDB.Set(typeName, obj.UserID, obj) + count++ + } + + case "User": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.User) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "Post": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Post) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "Thread": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Thread) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "Analytics": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Analytics) { + aeroDB.Set(typeName, obj.UserID, obj) + count++ + } + + case "SoundTrack": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.SoundTrack) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "Item": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Item) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "Inventory": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Inventory) { + aeroDB.Set(typeName, obj.UserID, obj) + count++ + } + + case "Settings": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.Settings) { + aeroDB.Set(typeName, obj.UserID, obj) + count++ + } + + case "UserFollows": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.UserFollows) { + aeroDB.Set(typeName, obj.UserID, obj) + count++ + } + + case "PayPalPayment": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.PayPalPayment) { + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "AniListToAnime": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.AniListToAnime) { + aeroDB.Set(typeName, obj.ServiceID, obj) + count++ + } + + case "MyAnimeListToAnime": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.MyAnimeListToAnime) { + aeroDB.Set(typeName, obj.ServiceID, obj) + count++ + } + + case "SearchIndex": + anime, _ := arn.DB.Get(typeName, "Anime") + aeroDB.Set(typeName, "Anime", anime) + + users, _ := arn.DB.Get(typeName, "User") + aeroDB.Set(typeName, "User", users) + + posts, _ := arn.DB.Get(typeName, "Post") + aeroDB.Set(typeName, "Post", posts) + + threads, _ := arn.DB.Get(typeName, "Thread") + aeroDB.Set(typeName, "Thread", threads) + + count += 4 + + case "DraftIndex": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.DraftIndex) { + aeroDB.Set(typeName, obj.UserID, obj) + count++ + } + + case "EmailToUser": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.EmailToUser) { + if obj.Email == "" { + continue + } + + aeroDB.Set(typeName, obj.Email, obj) + count++ + } + + case "FacebookToUser": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.FacebookToUser) { + if obj.ID == "" { + continue + } + + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "GoogleToUser": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.GoogleToUser) { + if obj.ID == "" { + continue + } + + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "TwitterToUser": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.TwitterToUser) { + if obj.ID == "" { + continue + } + + aeroDB.Set(typeName, obj.ID, obj) + count++ + } + + case "NickToUser": + channel, _ := arn.DB.All(typeName) + + for obj := range channel.(chan *arn.NickToUser) { + if obj.Nick == "" { + continue + } + + aeroDB.Set(typeName, obj.Nick, obj) + count++ + } + + default: + color.Yellow("Skipping %s", typeName) + continue + } + + color.Green("Export %d %s", count, typeName) + } + + time.Sleep(1 * time.Second) +} From b07a98ed32d0991e9ba86aaa6c293d2fef052567 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 27 Oct 2017 09:11:56 +0200 Subject: [PATCH 491/527] Refactor to use aerogo/database --- auth/facebook.go | 20 ++------ auth/google.go | 20 ++------ main.go | 8 +-- pages/database/select.go | 49 ++----------------- pages/editor/anilist.go | 7 +-- pages/editor/shoboi.go | 7 +-- pages/explore/explore.go | 17 +++---- pages/forum/forum.go | 2 +- pages/listimport/listimportanilist/anilist.go | 6 +-- pages/listimport/listimportkitsu/kitsu.go | 6 +-- .../listimportmyanimelist/myanimelist.go | 6 +-- pages/paypal/success.go | 12 +---- pages/shop/buyitem.go | 19 ++----- pages/statistics/anime.go | 9 ++-- pages/statistics/statistics.go | 9 ++-- pages/threads/threads.go | 9 ++-- pages/users/users.go | 8 +-- patches/export-aero-db/export-aero-db.go | 2 +- 18 files changed, 52 insertions(+), 164 deletions(-) diff --git a/auth/facebook.go b/auth/facebook.go index fe80d2f9..75fa4b5a 100644 --- a/auth/facebook.go +++ b/auth/facebook.go @@ -92,11 +92,7 @@ func InstallFacebookAuth(app *aero.Application) { if user != nil { // Add FacebookToUser reference - err = user.ConnectFacebook(fbUser.ID) - - if err != nil { - ctx.Error(http.StatusInternalServerError, "Could not connect account to Facebook account", err) - } + user.ConnectFacebook(fbUser.ID) // Save in DB user.Save() @@ -110,7 +106,7 @@ func InstallFacebookAuth(app *aero.Application) { var getErr error // Try to find an existing user via the Facebook user ID - user, getErr = arn.GetUserFromTable("FacebookToUser", fbUser.ID) + user, getErr = arn.GetUserByFacebookID(fbUser.ID) if getErr == nil && user != nil { authLog.Info("User logged in via Facebook ID", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) @@ -148,18 +144,10 @@ func InstallFacebookAuth(app *aero.Application) { user.Save() // Register user - err = arn.RegisterUser(user) - - if err != nil { - ctx.Error(http.StatusInternalServerError, "Could not register a new user", err) - } + arn.RegisterUser(user) // Connect account to a Facebook account - err = user.ConnectFacebook(fbUser.ID) - - if err != nil { - ctx.Error(http.StatusInternalServerError, "Could not connect account to Facebook account", err) - } + user.ConnectFacebook(fbUser.ID) // Save user object again with updated data user.Save() diff --git a/auth/google.go b/auth/google.go index ff49d05e..77321831 100644 --- a/auth/google.go +++ b/auth/google.go @@ -102,11 +102,7 @@ func InstallGoogleAuth(app *aero.Application) { if user != nil { // Add GoogleToUser reference - err = user.ConnectGoogle(googleUser.Sub) - - if err != nil { - ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err) - } + user.ConnectGoogle(googleUser.Sub) // Save in DB user.Save() @@ -120,7 +116,7 @@ func InstallGoogleAuth(app *aero.Application) { var getErr error // Try to find an existing user via the Google user ID - user, getErr = arn.GetUserFromTable("GoogleToUser", googleUser.Sub) + user, getErr = arn.GetUserByGoogleID(googleUser.Sub) if getErr == nil && user != nil { authLog.Info("User logged in via Google ID", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) @@ -158,18 +154,10 @@ func InstallGoogleAuth(app *aero.Application) { user.Save() // Register user - err = arn.RegisterUser(user) - - if err != nil { - ctx.Error(http.StatusInternalServerError, "Could not register a new user", err) - } + arn.RegisterUser(user) // Connect account to a Google account - err = user.ConnectGoogle(googleUser.Sub) - - if err != nil { - ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err) - } + user.ConnectGoogle(googleUser.Sub) // Save user object again with updated data user.Save() diff --git a/main.go b/main.go index 0e62f80d..4c7277e2 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "github.com/aerogo/aero" - "github.com/aerogo/session-store-aerospike" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/auth" "github.com/animenotifier/notify.moe/components/css" @@ -69,7 +68,10 @@ func configure(app *aero.Application) *aero.Application { // Sessions app.Sessions.Duration = 3600 * 24 * 30 * 6 - app.Sessions.Store = aerospikestore.New(arn.DB, "Session", app.Sessions.Duration) + + // TODO: ... + println("Using memory session store") + // app.Sessions.Store = aerospikestore.New(arn.DB, "Session", app.Sessions.Duration) // Layout app.Layout = layout.Render @@ -210,8 +212,6 @@ func configure(app *aero.Application) *aero.Application { // Domain if arn.IsDevelopment() { app.Config.Domain = "beta.notify.moe" - } else { - arn.DB.SetScanPriority("high") } // Authentication diff --git a/pages/database/select.go b/pages/database/select.go index 5b1aa8f2..a9666d1c 100644 --- a/pages/database/select.go +++ b/pages/database/select.go @@ -48,11 +48,7 @@ func Select(ctx *aero.Context) string { Results: []interface{}{}, } - stream, err := arn.DB.All(dataTypeName) - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error fetching data from the database", err) - } + stream := arn.DB.All(dataTypeName) process := func(obj interface{}) { _, _, value, _ := mirror.GetField(obj, field) @@ -62,47 +58,8 @@ func Select(ctx *aero.Context) string { } } - switch dataTypeName { - case "Analytics": - for obj := range stream.(chan *arn.Analytics) { - process(obj) - } - case "Anime": - for obj := range stream.(chan *arn.Anime) { - process(obj) - } - case "AnimeList": - for obj := range stream.(chan *arn.AnimeList) { - process(obj) - } - case "Character": - for obj := range stream.(chan *arn.Character) { - process(obj) - } - case "Group": - for obj := range stream.(chan *arn.Group) { - process(obj) - } - case "Post": - for obj := range stream.(chan *arn.Post) { - process(obj) - } - case "Settings": - for obj := range stream.(chan *arn.Settings) { - process(obj) - } - case "SoundTrack": - for obj := range stream.(chan *arn.SoundTrack) { - process(obj) - } - case "Thread": - for obj := range stream.(chan *arn.Thread) { - process(obj) - } - case "User": - for obj := range stream.(chan *arn.User) { - process(obj) - } + for obj := range stream { + process(obj) } for _, obj := range response.Results { diff --git a/pages/editor/anilist.go b/pages/editor/anilist.go index ffb8678b..f3c1415f 100644 --- a/pages/editor/anilist.go +++ b/pages/editor/anilist.go @@ -1,7 +1,6 @@ package editor import ( - "net/http" "sort" "github.com/aerogo/aero" @@ -13,14 +12,10 @@ const maxAniListEntries = 70 // AniList ... func AniList(ctx *aero.Context) string { - missing, err := arn.FilterAnime(func(anime *arn.Anime) bool { + missing := arn.FilterAnime(func(anime *arn.Anime) bool { return anime.GetMapping("anilist/anime") == "" }) - if err != nil { - ctx.Error(http.StatusInternalServerError, "Couldn't filter anime", err) - } - sort.Slice(missing, func(i, j int) bool { a := missing[i] b := missing[j] diff --git a/pages/editor/shoboi.go b/pages/editor/shoboi.go index dd33e329..96a1df94 100644 --- a/pages/editor/shoboi.go +++ b/pages/editor/shoboi.go @@ -1,7 +1,6 @@ package editor import ( - "net/http" "sort" "github.com/aerogo/aero" @@ -13,14 +12,10 @@ const maxShoboiEntries = 70 // Shoboi ... func Shoboi(ctx *aero.Context) string { - missing, err := arn.FilterAnime(func(anime *arn.Anime) bool { + missing := arn.FilterAnime(func(anime *arn.Anime) bool { return anime.GetMapping("shoboi/anime") == "" }) - if err != nil { - ctx.Error(http.StatusInternalServerError, "Couldn't filter anime", err) - } - sort.Slice(missing, func(i, j int) bool { a := missing[i] b := missing[j] diff --git a/pages/explore/explore.go b/pages/explore/explore.go index 529bbb15..fe1225f5 100644 --- a/pages/explore/explore.go +++ b/pages/explore/explore.go @@ -2,20 +2,19 @@ package explore import ( "github.com/aerogo/aero" - "github.com/animenotifier/arn" - "github.com/animenotifier/notify.moe/components" ) // Get ... func Get(ctx *aero.Context) string { - var cache arn.ListOfIDs - err := arn.DB.GetObject("Cache", "airing anime", &cache) + // var cache arn.ListOfIDs + // err := arn.DB.GetObject("Cache", "airing anime", &cache) - airing, err := arn.GetAiringAnimeCached() + // airing, err := arn.GetAiringAnimeCached() - if err != nil { - return ctx.Error(500, "Couldn't fetch airing anime", err) - } + // if err != nil { + // return ctx.Error(500, "Couldn't fetch airing anime", err) + // } - return ctx.HTML(components.Airing(airing)) + // return ctx.HTML(components.Airing(airing)) + return ctx.HTML("Not implemented") } diff --git a/pages/forum/forum.go b/pages/forum/forum.go index 1d648a98..49eaf27b 100644 --- a/pages/forum/forum.go +++ b/pages/forum/forum.go @@ -12,7 +12,7 @@ const ThreadsPerPage = 20 // Get forum category. func Get(ctx *aero.Context) string { tag := ctx.Get("tag") - threads, _ := arn.GetThreadsByTag(tag) + threads := arn.GetThreadsByTag(tag) arn.SortThreads(threads) if len(threads) > ThreadsPerPage { diff --git a/pages/listimport/listimportanilist/anilist.go b/pages/listimport/listimportanilist/anilist.go index 6f1b3c95..c3e6e0f0 100644 --- a/pages/listimport/listimportanilist/anilist.go +++ b/pages/listimport/listimportanilist/anilist.go @@ -64,11 +64,7 @@ func Finish(ctx *aero.Context) string { animeList.Import(item) } - err := animeList.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving your anime list", err) - } + animeList.Save() return ctx.Redirect("/+" + user.Nick + "/animelist") } diff --git a/pages/listimport/listimportkitsu/kitsu.go b/pages/listimport/listimportkitsu/kitsu.go index ebdcd7a3..52254045 100644 --- a/pages/listimport/listimportkitsu/kitsu.go +++ b/pages/listimport/listimportkitsu/kitsu.go @@ -77,11 +77,7 @@ func Finish(ctx *aero.Context) string { animeList.Import(item) } - err := animeList.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving your anime list", err) - } + animeList.Save() return ctx.Redirect("/+" + user.Nick + "/animelist") } diff --git a/pages/listimport/listimportmyanimelist/myanimelist.go b/pages/listimport/listimportmyanimelist/myanimelist.go index 7b816e37..455a9f8b 100644 --- a/pages/listimport/listimportmyanimelist/myanimelist.go +++ b/pages/listimport/listimportmyanimelist/myanimelist.go @@ -73,11 +73,7 @@ func Finish(ctx *aero.Context) string { animeList.Import(item) } - err := animeList.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving your anime list", err) - } + animeList.Save() return ctx.Redirect("/+" + user.Nick + "/animelist") } diff --git a/pages/paypal/success.go b/pages/paypal/success.go index ee7f609f..6bd509e3 100644 --- a/pages/paypal/success.go +++ b/pages/paypal/success.go @@ -66,21 +66,13 @@ func Success(ctx *aero.Context) string { Created: arn.DateTimeUTC(), } - err = payment.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Could not save payment in the database", err) - } + payment.Save() // Increase user's balance user.Balance += payment.Gems() // Save in DB - err = user.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Could not save new balance", err) - } + user.Save() // Notify admin go func() { diff --git a/pages/shop/buyitem.go b/pages/shop/buyitem.go index 669c4ebf..df2b1d78 100644 --- a/pages/shop/buyitem.go +++ b/pages/shop/buyitem.go @@ -46,27 +46,16 @@ func BuyItem(ctx *aero.Context) string { } user.Balance -= totalPrice - err = user.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving user data", err) - } + user.Save() // Add item to user inventory inventory := user.Inventory() inventory.AddItem(itemID, uint(quantity)) - err = inventory.Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving inventory", err) - } + inventory.Save() // Save purchase - err = arn.NewPurchase(user.ID, itemID, quantity, int(item.Price), "gem").Save() - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error saving purchase", err) - } + purchase := arn.NewPurchase(user.ID, itemID, quantity, int(item.Price), "gem") + purchase.Save() return "ok" } diff --git a/pages/statistics/anime.go b/pages/statistics/anime.go index 87e20c28..19e4ed4e 100644 --- a/pages/statistics/anime.go +++ b/pages/statistics/anime.go @@ -2,13 +2,12 @@ package statistics import ( "github.com/aerogo/aero" - "github.com/animenotifier/arn" - "github.com/animenotifier/notify.moe/components" ) // Anime ... func Anime(ctx *aero.Context) string { - statistics := arn.StatisticsCategory{} - arn.DB.GetObject("Cache", "anime statistics", &statistics) - return ctx.HTML(components.Statistics(statistics.PieCharts...)) + // statistics := arn.StatisticsCategory{} + // arn.DB.GetObject("Cache", "anime statistics", &statistics) + // return ctx.HTML(components.Statistics(statistics.PieCharts...)) + return ctx.HTML("Not implemented") } diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index 04d1855d..a9219f76 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -2,13 +2,12 @@ package statistics import ( "github.com/aerogo/aero" - "github.com/animenotifier/arn" - "github.com/animenotifier/notify.moe/components" ) // Get ... func Get(ctx *aero.Context) string { - statistics := arn.StatisticsCategory{} - arn.DB.GetObject("Cache", "user statistics", &statistics) - return ctx.HTML(components.Statistics(statistics.PieCharts...)) + // statistics := arn.StatisticsCategory{} + // arn.DB.GetObject("Cache", "user statistics", &statistics) + // return ctx.HTML(components.Statistics(statistics.PieCharts...)) + return ctx.HTML("Not implemented") } diff --git a/pages/threads/threads.go b/pages/threads/threads.go index b9b7977e..435e1056 100644 --- a/pages/threads/threads.go +++ b/pages/threads/threads.go @@ -22,14 +22,13 @@ func Get(ctx *aero.Context) string { } // Fetch posts - postObjects, getErr := arn.DB.GetMany("Post", thread.Posts) + postObjects := arn.DB.GetMany("Post", thread.Posts) + posts := make([]*arn.Post, len(postObjects), len(postObjects)) - if getErr != nil { - return ctx.Error(http.StatusInternalServerError, "Could not retrieve posts", getErr) + for i, obj := range postObjects { + posts[i] = obj.(*arn.Post) } - posts := postObjects.([]*arn.Post) - // Sort posts arn.SortPostsLatestLast(posts) diff --git a/pages/users/users.go b/pages/users/users.go index 919af4af..b12b5fe2 100644 --- a/pages/users/users.go +++ b/pages/users/users.go @@ -10,11 +10,11 @@ import ( // Active ... func Active(ctx *aero.Context) string { - users, err := arn.GetListOfUsersCached("active users") + users := arn.FilterUsers(func(user *arn.User) bool { + return user.IsActive() && user.HasAvatar() + }) - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) - } + arn.SortUsersLastSeen(users) return ctx.HTML(components.Users(users)) } diff --git a/patches/export-aero-db/export-aero-db.go b/patches/export-aero-db/export-aero-db.go index d66c872b..a03ddc11 100644 --- a/patches/export-aero-db/export-aero-db.go +++ b/patches/export-aero-db/export-aero-db.go @@ -11,7 +11,7 @@ import ( func main() { arn.DB.SetScanPriority("high") - aeroDB := database.New("db", arn.DBTypes) + aeroDB := database.New("arn", arn.DBTypes) defer aeroDB.Close() for typeName := range arn.DB.Types() { From 76e5f37eca4413d347ce62bca5195dc51fca2623 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 28 Oct 2017 03:53:12 +0200 Subject: [PATCH 492/527] Nano integration improvements --- jobs/active-users/active-users.go | 114 -------------- jobs/airing-anime/airing-anime.go | 78 ---------- jobs/jobs.go | 3 - jobs/statistics/statistics.go | 251 ------------------------------ main.go | 3 +- pages/best/best.go | 35 +---- pages/explore/explore.go | 51 +++++- pages/explore/explore.pixy | 2 +- pages/statistics/anime.go | 125 ++++++++++++++- pages/statistics/statistics.go | 103 +++++++++++- pages/statistics/statistics.pixy | 2 +- pages/users/users.go | 41 +++-- sw/service-worker.ts | 6 +- tests.go | 1 - 14 files changed, 298 insertions(+), 517 deletions(-) delete mode 100644 jobs/active-users/active-users.go delete mode 100644 jobs/airing-anime/airing-anime.go delete mode 100644 jobs/statistics/statistics.go diff --git a/jobs/active-users/active-users.go b/jobs/active-users/active-users.go deleted file mode 100644 index 4719f262..00000000 --- a/jobs/active-users/active-users.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "fmt" - "sort" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - color.Yellow("Caching list of active users") - - // Filter out active users with an avatar - users, err := arn.FilterUsers(func(user *arn.User) bool { - return user.IsActive() && user.Avatar.Extension != "" - }) - fmt.Println(len(users)) - - arn.PanicOnError(err) - - // Sort - sort.Slice(users, func(i, j int) bool { - if users[i].LastSeen < users[j].LastSeen { - return false - } - - if users[i].LastSeen > users[j].LastSeen { - return true - } - - return users[i].Registered > users[j].Registered - }) - - // Add users to list - SaveInCache("active users", users) - - // Sort by osu rank - osuUsers := users[:] - - sort.Slice(osuUsers, func(i, j int) bool { - return osuUsers[i].Accounts.Osu.PP > osuUsers[j].Accounts.Osu.PP - }) - - // Cut off users with 0 pp - for index, user := range osuUsers { - if user.Accounts.Osu.PP == 0 { - osuUsers = osuUsers[:index] - break - } - } - - // Save osu users - SaveInCache("active osu users", osuUsers) - - // Sort by role - staff := users[:] - - sort.Slice(staff, func(i, j int) bool { - if staff[i].Role == "" { - return false - } - - if staff[j].Role == "" { - return true - } - - return staff[i].Role == "admin" - }) - - // Cut off non-staff - for index, user := range staff { - if user.Role == "" { - staff = staff[:index] - break - } - } - - // Save staff users - SaveInCache("active staff users", staff) - - // Sort by anime watching list length - watching := users[:] - - sort.Slice(watching, func(i, j int) bool { - return len(watching[i].AnimeList().FilterStatus(arn.AnimeListStatusWatching).Items) > len(watching[j].AnimeList().FilterStatus(arn.AnimeListStatusWatching).Items) - }) - - // Save watching users - SaveInCache("active anime watching users", watching) - - color.Green("Finished.") -} - -// SaveInCache ... -func SaveInCache(key string, users []*arn.User) { - cache := arn.ListOfIDs{ - IDList: GenerateIDList(users), - } - - fmt.Println(len(cache.IDList), key) - arn.PanicOnError(arn.DB.Set("Cache", key, cache)) -} - -// GenerateIDList generates an ID list from a slice of users. -func GenerateIDList(users []*arn.User) []string { - list := []string{} - - for _, user := range users { - list = append(list, user.ID) - } - - return list -} diff --git a/jobs/airing-anime/airing-anime.go b/jobs/airing-anime/airing-anime.go deleted file mode 100644 index 96d41214..00000000 --- a/jobs/airing-anime/airing-anime.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "sort" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -const ( - currentlyAiringBonus = 5.0 - popularityThreshold = 5 - popularityPenalty = 8.0 - watchingPopularityWeight = 0.3 - plannedPopularityWeight = 0.2 -) - -func main() { - color.Yellow("Caching airing anime") - - animeList, err := arn.GetAiringAnime() - - if err != nil { - color.Red("Failed fetching airing anime") - color.Red(err.Error()) - return - } - - sort.Slice(animeList, func(i, j int) bool { - a := animeList[i] - b := animeList[j] - scoreA := a.Rating.Overall - scoreB := b.Rating.Overall - - if a.Status == "current" { - scoreA += currentlyAiringBonus - } - - if b.Status == "current" { - scoreB += currentlyAiringBonus - } - - if a.Popularity.Total() < popularityThreshold { - scoreA -= popularityPenalty - } - - if b.Popularity.Total() < popularityThreshold { - scoreB -= popularityPenalty - } - - scoreA += float64(a.Popularity.Watching) * watchingPopularityWeight - scoreB += float64(b.Popularity.Watching) * watchingPopularityWeight - - scoreA += float64(a.Popularity.Planned) * plannedPopularityWeight - scoreB += float64(b.Popularity.Planned) * plannedPopularityWeight - - return scoreA > scoreB - }) - - // Convert to small anime list - cache := &arn.ListOfIDs{} - - for _, anime := range animeList { - cache.IDList = append(cache.IDList, anime.ID) - } - - println(len(cache.IDList)) - - saveErr := arn.DB.Set("Cache", "airing anime", cache) - - if saveErr != nil { - color.Red("Error saving airing anime") - color.Red(saveErr.Error()) - return - } - - color.Green("Finished.") -} diff --git a/jobs/jobs.go b/jobs/jobs.go index d05f663e..280cb7b5 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -24,10 +24,7 @@ var colorPool = []*color.Color{ var jobs = map[string]time.Duration{ "forum-activity": 1 * time.Minute, - "active-users": 5 * time.Minute, "anime-ratings": 10 * time.Minute, - "airing-anime": 10 * time.Minute, - "statistics": 15 * time.Minute, "popular-anime": 20 * time.Minute, "avatars": 1 * time.Hour, "test": 1 * time.Hour, diff --git a/jobs/statistics/statistics.go b/jobs/statistics/statistics.go deleted file mode 100644 index deb9d5e1..00000000 --- a/jobs/statistics/statistics.go +++ /dev/null @@ -1,251 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -type stats map[string]float64 - -func main() { - color.Yellow("Generating statistics") - - userStats := getUserStats() - animeStats := getAnimeStats() - - arn.PanicOnError(arn.DB.Set("Cache", "user statistics", &arn.StatisticsCategory{ - Name: "Users", - PieCharts: userStats, - })) - - arn.PanicOnError(arn.DB.Set("Cache", "anime statistics", &arn.StatisticsCategory{ - Name: "Anime", - PieCharts: animeStats, - })) - - color.Green("Finished.") -} - -func getUserStats() []*arn.PieChart { - println("Generating user statistics") - - analytics, err := arn.AllAnalytics() - arn.PanicOnError(err) - - screenSize := stats{} - pixelRatio := stats{} - browser := stats{} - country := stats{} - gender := stats{} - os := stats{} - notifications := stats{} - avatar := stats{} - ip := stats{} - pro := stats{} - - for _, info := range analytics { - user, err := arn.GetUser(info.UserID) - arn.PanicOnError(err) - - if !user.IsActive() { - continue - } - - pixelRatio[fmt.Sprintf("%.0f", info.Screen.PixelRatio)]++ - - size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) - screenSize[size]++ - } - - for user := range arn.MustStreamUsers() { - if !user.IsActive() { - continue - } - - if user.Gender != "" && user.Gender != "other" { - gender[user.Gender]++ - } - - if user.Browser.Name != "" { - browser[user.Browser.Name]++ - } - - if user.Location.CountryName != "" { - country[user.Location.CountryName]++ - } - - if user.OS.Name != "" { - if strings.HasPrefix(user.OS.Name, "CrOS") { - user.OS.Name = "Chrome OS" - } - - os[user.OS.Name]++ - } - - if len(user.PushSubscriptions().Items) > 0 { - notifications["Enabled"]++ - } else { - notifications["Disabled"]++ - } - - if user.Avatar.Source == "" { - avatar["none"]++ - } else { - avatar[user.Avatar.Source]++ - } - - if arn.IsIPv6(user.IP) { - ip["IPv6"]++ - } else { - ip["IPv4"]++ - } - - if user.IsPro() { - pro["PRO accounts"]++ - } else { - pro["Free accounts"]++ - } - } - - println("Finished user statistics") - - return []*arn.PieChart{ - arn.NewPieChart("OS", os), - arn.NewPieChart("Screen size", screenSize), - arn.NewPieChart("Browser", browser), - arn.NewPieChart("Country", country), - arn.NewPieChart("Avatar", avatar), - arn.NewPieChart("Notifications", notifications), - arn.NewPieChart("Gender", gender), - arn.NewPieChart("Pixel ratio", pixelRatio), - arn.NewPieChart("IP version", ip), - arn.NewPieChart("PRO accounts", pro), - } -} - -func getAnimeStats() []*arn.PieChart { - println("Generating anime statistics") - - allAnime, err := arn.AllAnime() - arn.PanicOnError(err) - - shoboi := stats{} - anilist := stats{} - mal := stats{} - anidb := stats{} - status := stats{} - types := stats{} - shoboiEdits := stats{} - anilistEdits := stats{} - malEdits := stats{} - anidbEdits := stats{} - rating := stats{} - twist := stats{} - - for _, anime := range allAnime { - for _, external := range anime.Mappings { - if external.Service == "shoboi/anime" { - if external.CreatedBy == "" { - shoboiEdits["(auto-generated)"]++ - } else { - user, err := arn.GetUser(external.CreatedBy) - arn.PanicOnError(err) - shoboiEdits[user.Nick]++ - } - } - - if external.Service == "anilist/anime" { - if external.CreatedBy == "" { - anilistEdits["(auto-generated)"]++ - } else { - user, err := arn.GetUser(external.CreatedBy) - arn.PanicOnError(err) - anilistEdits[user.Nick]++ - } - } - - if external.Service == "myanimelist/anime" { - if external.CreatedBy == "" { - malEdits["(auto-generated)"]++ - } else { - user, err := arn.GetUser(external.CreatedBy) - arn.PanicOnError(err) - malEdits[user.Nick]++ - } - } - - if external.Service == "anidb/anime" { - if external.CreatedBy == "" { - anidbEdits["(auto-generated)"]++ - } else { - user, err := arn.GetUser(external.CreatedBy) - arn.PanicOnError(err) - anidbEdits[user.Nick]++ - } - } - } - - if anime.GetMapping("shoboi/anime") != "" { - shoboi["Connected with Shoboi"]++ - } else { - shoboi["Not connected with Shoboi"]++ - } - - if anime.GetMapping("anilist/anime") != "" { - anilist["Connected with AniList"]++ - } else { - anilist["Not connected with AniList"]++ - } - - if anime.GetMapping("myanimelist/anime") != "" { - mal["Connected with MyAnimeList"]++ - } else { - mal["Not connected with MyAnimeList"]++ - } - - if anime.GetMapping("anidb/anime") != "" { - anidb["Connected with AniDB"]++ - } else { - anidb["Not connected with AniDB"]++ - } - - rating[arn.ToString(int(anime.Rating.Overall+0.5))]++ - - found := false - for _, episode := range anime.Episodes().Items { - if episode.Links != nil && episode.Links["twist.moe"] != "" { - found = true - break - } - } - - if found { - twist["Connected with AnimeTwist"]++ - } else { - twist["Not connected with AnimeTwist"]++ - } - - status[anime.Status]++ - types[anime.Type]++ - } - - println("Finished anime statistics") - - return []*arn.PieChart{ - arn.NewPieChart("Type", types), - arn.NewPieChart("Status", status), - arn.NewPieChart("Rating", rating), - arn.NewPieChart("MyAnimeList", mal), - arn.NewPieChart("AniList", anilist), - arn.NewPieChart("AniDB", anidb), - arn.NewPieChart("Shoboi", shoboi), - arn.NewPieChart("AnimeTwist", twist), - // arn.NewPieChart("MyAnimeList Editors", malEdits), - arn.NewPieChart("AniList Editors", anilistEdits), - // arn.NewPieChart("AniDB Editors", anidbEdits), - arn.NewPieChart("Shoboi Editors", shoboiEdits), - } -} diff --git a/main.go b/main.go index 4c7277e2..8debce18 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/aerogo/aero" + "github.com/aerogo/session-store-nano" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/auth" "github.com/animenotifier/notify.moe/components/css" @@ -70,8 +71,8 @@ func configure(app *aero.Application) *aero.Application { app.Sessions.Duration = 3600 * 24 * 30 * 6 // TODO: ... - println("Using memory session store") // app.Sessions.Store = aerospikestore.New(arn.DB, "Session", app.Sessions.Duration) + app.Sessions.Store = nanostore.New(arn.DB, "Session") // Layout app.Layout = layout.Render diff --git a/pages/best/best.go b/pages/best/best.go index e5ce245d..5257a910 100644 --- a/pages/best/best.go +++ b/pages/best/best.go @@ -1,10 +1,7 @@ package best import ( - "net/http" - "github.com/aerogo/aero" - "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" ) @@ -12,35 +9,5 @@ const maxEntries = 7 // Get search page. func Get(ctx *aero.Context) string { - overall, err := arn.GetListOfAnimeCached("best anime overall") - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) - } - - story, err := arn.GetListOfAnimeCached("best anime story") - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) - } - - visuals, err := arn.GetListOfAnimeCached("best anime visuals") - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) - } - - soundtrack, err := arn.GetListOfAnimeCached("best anime soundtrack") - - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) - } - - airing, err := arn.GetAiringAnimeCached() - - if err != nil { - return ctx.Error(500, "Couldn't fetch airing anime", err) - } - - return ctx.HTML(components.BestAnime(overall[:maxEntries], story[:maxEntries], visuals[:maxEntries], soundtrack[:maxEntries], airing[:maxEntries])) + return ctx.HTML(components.BestAnime(nil, nil, nil, nil, nil)) } diff --git a/pages/explore/explore.go b/pages/explore/explore.go index fe1225f5..be55016b 100644 --- a/pages/explore/explore.go +++ b/pages/explore/explore.go @@ -1,20 +1,55 @@ package explore import ( + "sort" + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" +) + +const ( + currentlyAiringBonus = 5.0 + popularityThreshold = 5 + popularityPenalty = 8.0 + watchingPopularityWeight = 0.3 + plannedPopularityWeight = 0.2 ) // Get ... func Get(ctx *aero.Context) string { - // var cache arn.ListOfIDs - // err := arn.DB.GetObject("Cache", "airing anime", &cache) + animeList := arn.GetAiringAnime() - // airing, err := arn.GetAiringAnimeCached() + sort.Slice(animeList, func(i, j int) bool { + a := animeList[i] + b := animeList[j] + scoreA := a.Rating.Overall + scoreB := b.Rating.Overall - // if err != nil { - // return ctx.Error(500, "Couldn't fetch airing anime", err) - // } + if a.Status == "current" { + scoreA += currentlyAiringBonus + } - // return ctx.HTML(components.Airing(airing)) - return ctx.HTML("Not implemented") + if b.Status == "current" { + scoreB += currentlyAiringBonus + } + + if a.Popularity.Total() < popularityThreshold { + scoreA -= popularityPenalty + } + + if b.Popularity.Total() < popularityThreshold { + scoreB -= popularityPenalty + } + + scoreA += float64(a.Popularity.Watching) * watchingPopularityWeight + scoreB += float64(b.Popularity.Watching) * watchingPopularityWeight + + scoreA += float64(a.Popularity.Planned) * plannedPopularityWeight + scoreB += float64(b.Popularity.Planned) * plannedPopularityWeight + + return scoreA > scoreB + }) + + return ctx.HTML(components.Explore(animeList)) } diff --git a/pages/explore/explore.pixy b/pages/explore/explore.pixy index 8a0149a5..7c7c2cc0 100644 --- a/pages/explore/explore.pixy +++ b/pages/explore/explore.pixy @@ -1,3 +1,3 @@ -component Airing(animeList []*arn.Anime) +component Explore(animeList []*arn.Anime) h1.page-title(title=toString(len(animeList)) + " anime") Explore AnimeGrid(animeList) \ No newline at end of file diff --git a/pages/statistics/anime.go b/pages/statistics/anime.go index 19e4ed4e..06b04db6 100644 --- a/pages/statistics/anime.go +++ b/pages/statistics/anime.go @@ -2,12 +2,129 @@ package statistics import ( "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" ) // Anime ... func Anime(ctx *aero.Context) string { - // statistics := arn.StatisticsCategory{} - // arn.DB.GetObject("Cache", "anime statistics", &statistics) - // return ctx.HTML(components.Statistics(statistics.PieCharts...)) - return ctx.HTML("Not implemented") + pieCharts := getAnimeStats() + return ctx.HTML(components.Statistics(pieCharts)) +} + +func getAnimeStats() []*arn.PieChart { + shoboi := stats{} + anilist := stats{} + mal := stats{} + anidb := stats{} + status := stats{} + types := stats{} + shoboiEdits := stats{} + anilistEdits := stats{} + malEdits := stats{} + anidbEdits := stats{} + rating := stats{} + twist := stats{} + + for anime := range arn.StreamAnime() { + for _, external := range anime.Mappings { + if external.Service == "shoboi/anime" { + if external.CreatedBy == "" { + shoboiEdits["(auto-generated)"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + shoboiEdits[user.Nick]++ + } + } + + if external.Service == "anilist/anime" { + if external.CreatedBy == "" { + anilistEdits["(auto-generated)"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + anilistEdits[user.Nick]++ + } + } + + if external.Service == "myanimelist/anime" { + if external.CreatedBy == "" { + malEdits["(auto-generated)"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + malEdits[user.Nick]++ + } + } + + if external.Service == "anidb/anime" { + if external.CreatedBy == "" { + anidbEdits["(auto-generated)"]++ + } else { + user, err := arn.GetUser(external.CreatedBy) + arn.PanicOnError(err) + anidbEdits[user.Nick]++ + } + } + } + + if anime.GetMapping("shoboi/anime") != "" { + shoboi["Connected with Shoboi"]++ + } else { + shoboi["Not connected with Shoboi"]++ + } + + if anime.GetMapping("anilist/anime") != "" { + anilist["Connected with AniList"]++ + } else { + anilist["Not connected with AniList"]++ + } + + if anime.GetMapping("myanimelist/anime") != "" { + mal["Connected with MyAnimeList"]++ + } else { + mal["Not connected with MyAnimeList"]++ + } + + if anime.GetMapping("anidb/anime") != "" { + anidb["Connected with AniDB"]++ + } else { + anidb["Not connected with AniDB"]++ + } + + rating[arn.ToString(int(anime.Rating.Overall+0.5))]++ + + found := false + for _, episode := range anime.Episodes().Items { + if episode.Links != nil && episode.Links["twist.moe"] != "" { + found = true + break + } + } + + if found { + twist["Connected with AnimeTwist"]++ + } else { + twist["Not connected with AnimeTwist"]++ + } + + status[anime.Status]++ + types[anime.Type]++ + } + + return []*arn.PieChart{ + arn.NewPieChart("Type", types), + arn.NewPieChart("Status", status), + arn.NewPieChart("Rating", rating), + arn.NewPieChart("MyAnimeList", mal), + arn.NewPieChart("AniList", anilist), + arn.NewPieChart("AniDB", anidb), + arn.NewPieChart("Shoboi", shoboi), + arn.NewPieChart("AnimeTwist", twist), + // arn.NewPieChart("MyAnimeList Editors", malEdits), + arn.NewPieChart("AniList Editors", anilistEdits), + // arn.NewPieChart("AniDB Editors", anidbEdits), + arn.NewPieChart("Shoboi Editors", shoboiEdits), + } } diff --git a/pages/statistics/statistics.go b/pages/statistics/statistics.go index a9219f76..649deb63 100644 --- a/pages/statistics/statistics.go +++ b/pages/statistics/statistics.go @@ -1,13 +1,108 @@ package statistics import ( + "fmt" + "strings" + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/components" ) +type stats map[string]float64 + // Get ... func Get(ctx *aero.Context) string { - // statistics := arn.StatisticsCategory{} - // arn.DB.GetObject("Cache", "user statistics", &statistics) - // return ctx.HTML(components.Statistics(statistics.PieCharts...)) - return ctx.HTML("Not implemented") + pieCharts := getUserStats() + return ctx.HTML(components.Statistics(pieCharts)) +} + +func getUserStats() []*arn.PieChart { + screenSize := stats{} + pixelRatio := stats{} + browser := stats{} + country := stats{} + gender := stats{} + os := stats{} + notifications := stats{} + avatar := stats{} + ip := stats{} + pro := stats{} + + for info := range arn.StreamAnalytics() { + user, err := arn.GetUser(info.UserID) + arn.PanicOnError(err) + + if !user.IsActive() { + continue + } + + pixelRatio[fmt.Sprintf("%.0f", info.Screen.PixelRatio)]++ + + size := arn.ToString(info.Screen.Width) + " x " + arn.ToString(info.Screen.Height) + screenSize[size]++ + } + + for user := range arn.StreamUsers() { + if !user.IsActive() { + continue + } + + if user.Gender != "" && user.Gender != "other" { + gender[user.Gender]++ + } + + if user.Browser.Name != "" { + browser[user.Browser.Name]++ + } + + if user.Location.CountryName != "" { + country[user.Location.CountryName]++ + } + + if user.OS.Name != "" { + if strings.HasPrefix(user.OS.Name, "CrOS") { + user.OS.Name = "Chrome OS" + } + + os[user.OS.Name]++ + } + + if len(user.PushSubscriptions().Items) > 0 { + notifications["Enabled"]++ + } else { + notifications["Disabled"]++ + } + + if user.Avatar.Source == "" { + avatar["none"]++ + } else { + avatar[user.Avatar.Source]++ + } + + if arn.IsIPv6(user.IP) { + ip["IPv6"]++ + } else { + ip["IPv4"]++ + } + + if user.IsPro() { + pro["PRO accounts"]++ + } else { + pro["Free accounts"]++ + } + } + + return []*arn.PieChart{ + arn.NewPieChart("OS", os), + arn.NewPieChart("Screen size", screenSize), + arn.NewPieChart("Browser", browser), + arn.NewPieChart("Country", country), + arn.NewPieChart("Avatar", avatar), + arn.NewPieChart("Notifications", notifications), + arn.NewPieChart("Gender", gender), + arn.NewPieChart("Pixel ratio", pixelRatio), + arn.NewPieChart("IP version", ip), + arn.NewPieChart("PRO accounts", pro), + } } diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index c9fad809..7abdc72f 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -1,4 +1,4 @@ -component Statistics(pieCharts ...*arn.PieChart) +component Statistics(pieCharts []*arn.PieChart) h1.page-title Statistics StatisticsHeader diff --git a/pages/users/users.go b/pages/users/users.go index b12b5fe2..7004e18e 100644 --- a/pages/users/users.go +++ b/pages/users/users.go @@ -1,7 +1,7 @@ package users import ( - "net/http" + "sort" "github.com/aerogo/aero" "github.com/animenotifier/arn" @@ -21,11 +21,14 @@ func Active(ctx *aero.Context) string { // Osu ... func Osu(ctx *aero.Context) string { - users, err := arn.GetListOfUsersCached("active osu users") + users := arn.FilterUsers(func(user *arn.User) bool { + return user.IsActive() && user.HasAvatar() && user.Accounts.Osu.PP > 0 + }) - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) - } + // Sort by pp + sort.Slice(users, func(i, j int) bool { + return users[i].Accounts.Osu.PP > users[j].Accounts.Osu.PP + }) if len(users) > 50 { users = users[:50] @@ -36,22 +39,34 @@ func Osu(ctx *aero.Context) string { // Staff ... func Staff(ctx *aero.Context) string { - users, err := arn.GetListOfUsersCached("active staff users") + users := arn.FilterUsers(func(user *arn.User) bool { + return user.IsActive() && user.HasAvatar() && user.Role != "" + }) - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) - } + sort.Slice(users, func(i, j int) bool { + if users[i].Role == "" { + return false + } + + if users[j].Role == "" { + return true + } + + return users[i].Role == "admin" + }) return ctx.HTML(components.Users(users)) } // AnimeWatching ... func AnimeWatching(ctx *aero.Context) string { - users, err := arn.GetListOfUsersCached("active anime watching users") + users := arn.FilterUsers(func(user *arn.User) bool { + return user.IsActive() && user.HasAvatar() + }) - if err != nil { - return ctx.Error(http.StatusInternalServerError, "Could not fetch user data", err) - } + sort.Slice(users, func(i, j int) bool { + return len(users[i].AnimeList().Watching().Items) > len(users[j].AnimeList().Watching().Items) + }) return ctx.HTML(components.Users(users)) } diff --git a/sw/service-worker.ts b/sw/service-worker.ts index 901a49ca..e87756cc 100644 --- a/sw/service-worker.ts +++ b/sw/service-worker.ts @@ -87,8 +87,6 @@ class MyServiceWorker { onRequest(evt: FetchEvent) { let request = evt.request as Request - // console.log("fetch:", request.url) - // If it's not a GET request, fetch it normally if(request.method !== "GET") { return evt.respondWith(fetch(request)) @@ -96,7 +94,7 @@ class MyServiceWorker { // Clear cache on authentication and fetch it normally if(request.url.includes("/auth/") || request.url.includes("/logout")) { - return caches.delete(this.cache.version).then(() => evt.respondWith(fetch(request))) + return evt.respondWith(caches.delete(this.cache.version).then(() => fetch(request))) } // Exclude certain URLs from being cached @@ -109,7 +107,7 @@ class MyServiceWorker { // If the request included the header "X-CacheOnly", return a cache-only response. // This is used in reloads to avoid generating a 2nd request after a cache refresh. if(request.headers.get("X-CacheOnly") === "true") { - return this.fromCache(request) + return evt.respondWith(this.fromCache(request)) } // Start fetching the request diff --git a/tests.go b/tests.go index 0e8fe3e7..00c691be 100644 --- a/tests.go +++ b/tests.go @@ -245,7 +245,6 @@ var routeTests = map[string][]string{ "/paypal/cancel": nil, "/anime/:id/edit": nil, "/new/thread": nil, - "/new/soundtrack": nil, "/admin/purchases": nil, "/editor/anilist": nil, "/editor/shoboi": nil, From 0bcab02262f4d4afc21cfc1f2d3cb9082f439be3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 28 Oct 2017 04:54:24 +0200 Subject: [PATCH 493/527] Added prefetching --- main.go | 3 +++ patches/show-season/show-season.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 8debce18..faaf7de9 100644 --- a/main.go +++ b/main.go @@ -207,6 +207,9 @@ func configure(app *aero.Application) *aero.Application { middleware.UserInfo(), ) + // Database + arn.DB.LoadCollections() + // API arn.API.Install(app) diff --git a/patches/show-season/show-season.go b/patches/show-season/show-season.go index f0ee77b2..0f079c68 100644 --- a/patches/show-season/show-season.go +++ b/patches/show-season/show-season.go @@ -7,7 +7,7 @@ import ( ) func main() { - for anime := range arn.MustStreamAnime() { + for anime := range arn.StreamAnime() { if anime.NSFW == 1 || anime.Status != "current" || anime.StartDate == "" || anime.StartDate < "2017-09" || anime.StartDate > "2017-10-17" { continue } From 843e340b4521539658bb661fe80a54fb074db592 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 28 Oct 2017 17:18:27 +0200 Subject: [PATCH 494/527] Fixed publish button not displaying --- utils/editform/editform.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utils/editform/editform.go b/utils/editform/editform.go index 33756e24..754292e4 100644 --- a/utils/editform/editform.go +++ b/utils/editform/editform.go @@ -31,10 +31,14 @@ func Render(obj interface{}, title string, user *arn.User) string { RenderObject(&b, obj, "") - if user != nil && (user.Role == "editor" || user.Role == "admin") { + if user != nil { b.WriteString(`
    `) b.WriteString(`
    `) - b.WriteString(``) + + if user.Role == "editor" || user.Role == "admin" { + b.WriteString(``) + } + b.WriteString(`
    `) } From 2b03ba6cc62030f142addcbebcaeb1595570c37f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 29 Oct 2017 10:27:47 +0100 Subject: [PATCH 495/527] Updated API --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index faaf7de9..4b5fdca5 100644 --- a/main.go +++ b/main.go @@ -208,7 +208,7 @@ func configure(app *aero.Application) *aero.Application { ) // Database - arn.DB.LoadCollections() + arn.DB.PrefetchData() // API arn.API.Install(app) From 32d7378e87bc72b03f9cd40380d22fdb91f1c004 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 31 Oct 2017 12:12:21 +0100 Subject: [PATCH 496/527] Minor changes --- main.go | 2 +- patches/clear-sessions/clear-sessions.go | 2 +- patches/nano-test/main.go | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 patches/nano-test/main.go diff --git a/main.go b/main.go index 4b5fdca5..48f4f1cf 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func configure(app *aero.Application) *aero.Application { // TODO: ... // app.Sessions.Store = aerospikestore.New(arn.DB, "Session", app.Sessions.Duration) - app.Sessions.Store = nanostore.New(arn.DB, "Session") + app.Sessions.Store = nanostore.New(arn.DB.Collection("Session")) // Layout app.Layout = layout.Render diff --git a/patches/clear-sessions/clear-sessions.go b/patches/clear-sessions/clear-sessions.go index 2b68107d..66324afc 100644 --- a/patches/clear-sessions/clear-sessions.go +++ b/patches/clear-sessions/clear-sessions.go @@ -7,6 +7,6 @@ import ( func main() { color.Yellow("Deleting all sessions...") - arn.DB.DeleteTable("Session") + arn.DB.Clear("Session") color.Green("Finished.") } diff --git a/patches/nano-test/main.go b/patches/nano-test/main.go new file mode 100644 index 00000000..af91a252 --- /dev/null +++ b/patches/nano-test/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + defer arn.Node.Close() + + user, _ := arn.GetUserByNick("Akyoto") + + if user.Language == ":)" { + user.Language = ":(" + } else { + user.Language = ":)" + } + + user.Save() +} From c60f53ed76533febca9ebbc8717abad4dd860179 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 09:45:14 +0100 Subject: [PATCH 497/527] Migration to nano --- jobs/anime-ratings/anime-ratings.go | 5 +- jobs/avatars/avatars.go | 1 + jobs/jobs.go | 2 - jobs/test/test.go | 2 +- jobs/twist/twist.go | 6 +- pages/dashboard/dashboard.go | 18 +- patches/export-aero-db/export-aero-db.go | 406 +++++++++++------------ 7 files changed, 228 insertions(+), 212 deletions(-) diff --git a/jobs/anime-ratings/anime-ratings.go b/jobs/anime-ratings/anime-ratings.go index 99c020e9..875ed50a 100644 --- a/jobs/anime-ratings/anime-ratings.go +++ b/jobs/anime-ratings/anime-ratings.go @@ -13,6 +13,7 @@ var popularity = map[string]*arn.AnimePopularity{} // made to it. func main() { color.Yellow("Updating anime ratings") + defer arn.Node.Close() allAnimeLists, err := arn.AllAnimeLists() arn.PanicOnError(err) @@ -58,7 +59,7 @@ func main() { anime, err := arn.GetAnime(animeID) arn.PanicOnError(err) anime.Rating = finalRating[animeID] - arn.PanicOnError(anime.Save()) + anime.Save() } // Save popularity @@ -66,7 +67,7 @@ func main() { anime, err := arn.GetAnime(animeID) arn.PanicOnError(err) anime.Popularity = popularity[animeID] - arn.PanicOnError(anime.Save()) + anime.Save() } color.Green("Finished.") diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index 97d73ac0..60c82d64 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -21,6 +21,7 @@ var wg sync.WaitGroup // Main func main() { color.Yellow("Generating user avatars") + defer arn.Node.Close() // Switch to main directory exe, err := os.Executable() diff --git a/jobs/jobs.go b/jobs/jobs.go index 280cb7b5..2137c493 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -23,9 +23,7 @@ var colorPool = []*color.Color{ } var jobs = map[string]time.Duration{ - "forum-activity": 1 * time.Minute, "anime-ratings": 10 * time.Minute, - "popular-anime": 20 * time.Minute, "avatars": 1 * time.Hour, "test": 1 * time.Hour, "twist": 2 * time.Hour, diff --git a/jobs/test/test.go b/jobs/test/test.go index 0fc58c8d..f506acd9 100644 --- a/jobs/test/test.go +++ b/jobs/test/test.go @@ -18,7 +18,7 @@ var packages = []string{ "github.com/animenotifier/shoboi", "github.com/animenotifier/twist", "github.com/animenotifier/avatar", - "github.com/animenotifier/japanese", + // "github.com/animenotifier/japanese", // "github.com/animenotifier/osu", } diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index 04f0219a..f4e557d9 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -13,15 +13,17 @@ import ( var rateLimiter = time.NewTicker(500 * time.Millisecond) func main() { + defer arn.Node.Close() + // Replace this with ID list from twist.moe later twistAnime, err := twist.GetAnimeIndex() arn.PanicOnError(err) idList := twistAnime.KitsuIDs() // Save index in cache - arn.PanicOnError(arn.DB.Set("Cache", "animetwist index", &arn.ListOfIDs{ + arn.DB.Set("Cache", "animetwist index", &arn.ListOfIDs{ IDList: idList, - })) + }) color.Yellow("Refreshing twist.moe links for %d anime", len(idList)) diff --git a/pages/dashboard/dashboard.go b/pages/dashboard/dashboard.go index 6dac6725..8733f999 100644 --- a/pages/dashboard/dashboard.go +++ b/pages/dashboard/dashboard.go @@ -11,7 +11,7 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -const maxPosts = 5 +const maxForumActivity = 5 const maxFollowing = 5 const maxSoundTracks = 5 const maxScheduleItems = 5 @@ -30,7 +30,21 @@ func Get(ctx *aero.Context) string { } flow.Parallel(func() { - forumActivity, _ = arn.GetForumActivityCached() + posts := arn.AllPosts() + threads := arn.AllThreads() + + arn.SortPostsLatestFirst(posts) + arn.SortThreadsLatestFirst(threads) + + posts = arn.FilterPostsWithUniqueThreads(posts, maxForumActivity) + + postPostables := arn.ToPostables(posts) + threadPostables := arn.ToPostables(threads) + + allPostables := append(postPostables, threadPostables...) + + arn.SortPostablesLatestFirst(allPostables) + forumActivity = arn.FilterPostablesWithUniqueThreads(allPostables, maxForumActivity) }, func() { animeList, err := arn.GetAnimeList(user.ID) diff --git a/patches/export-aero-db/export-aero-db.go b/patches/export-aero-db/export-aero-db.go index a03ddc11..553e3c23 100644 --- a/patches/export-aero-db/export-aero-db.go +++ b/patches/export-aero-db/export-aero-db.go @@ -1,273 +1,273 @@ -package main +// package main -import ( - "time" +// import ( +// "time" - "github.com/aerogo/database" - "github.com/animenotifier/arn" - "github.com/fatih/color" -) +// "github.com/aerogo/nano" +// "github.com/animenotifier/arn" +// "github.com/fatih/color" +// ) -func main() { - arn.DB.SetScanPriority("high") +// func main() { +// arn.DB.SetScanPriority("high") - aeroDB := database.New("arn", arn.DBTypes) - defer aeroDB.Close() +// aeroDB := nano.New(5000).Namespace("arn", arn.DBTypes...) +// defer aeroDB.Close() - for typeName := range arn.DB.Types() { - count := 0 +// for typeName := range arn.DB.Types() { +// count := 0 - switch typeName { - case "Anime": - channel, _ := arn.DB.All(typeName) +// switch typeName { +// case "Anime": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Anime) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.Anime) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "AnimeEpisodes": - channel, _ := arn.DB.All(typeName) +// case "AnimeEpisodes": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.AnimeEpisodes) { - aeroDB.Set(typeName, obj.AnimeID, obj) - count++ - } +// for obj := range channel.(chan *arn.AnimeEpisodes) { +// aeroDB.Set(typeName, obj.AnimeID, obj) +// count++ +// } - case "AnimeList": - channel, _ := arn.DB.All(typeName) +// case "AnimeList": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.AnimeList) { - aeroDB.Set(typeName, obj.UserID, obj) - count++ - } +// for obj := range channel.(chan *arn.AnimeList) { +// aeroDB.Set(typeName, obj.UserID, obj) +// count++ +// } - case "AnimeCharacters": - channel, _ := arn.DB.All(typeName) +// case "AnimeCharacters": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.AnimeCharacters) { - aeroDB.Set(typeName, obj.AnimeID, obj) - count++ - } +// for obj := range channel.(chan *arn.AnimeCharacters) { +// aeroDB.Set(typeName, obj.AnimeID, obj) +// count++ +// } - case "AnimeRelations": - channel, _ := arn.DB.All(typeName) +// case "AnimeRelations": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.AnimeRelations) { - aeroDB.Set(typeName, obj.AnimeID, obj) - count++ - } +// for obj := range channel.(chan *arn.AnimeRelations) { +// aeroDB.Set(typeName, obj.AnimeID, obj) +// count++ +// } - case "Character": - channel, _ := arn.DB.All(typeName) +// case "Character": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Character) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.Character) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "Purchase": - channel, _ := arn.DB.All(typeName) +// case "Purchase": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Purchase) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.Purchase) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "PushSubscriptions": - channel, _ := arn.DB.All(typeName) +// case "PushSubscriptions": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.PushSubscriptions) { - aeroDB.Set(typeName, obj.UserID, obj) - count++ - } +// for obj := range channel.(chan *arn.PushSubscriptions) { +// aeroDB.Set(typeName, obj.UserID, obj) +// count++ +// } - case "User": - channel, _ := arn.DB.All(typeName) +// case "User": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.User) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.User) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "Post": - channel, _ := arn.DB.All(typeName) +// case "Post": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Post) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.Post) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "Thread": - channel, _ := arn.DB.All(typeName) +// case "Thread": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Thread) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.Thread) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "Analytics": - channel, _ := arn.DB.All(typeName) +// case "Analytics": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Analytics) { - aeroDB.Set(typeName, obj.UserID, obj) - count++ - } +// for obj := range channel.(chan *arn.Analytics) { +// aeroDB.Set(typeName, obj.UserID, obj) +// count++ +// } - case "SoundTrack": - channel, _ := arn.DB.All(typeName) +// case "SoundTrack": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.SoundTrack) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.SoundTrack) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "Item": - channel, _ := arn.DB.All(typeName) +// case "Item": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Item) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.Item) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "Inventory": - channel, _ := arn.DB.All(typeName) +// case "Inventory": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Inventory) { - aeroDB.Set(typeName, obj.UserID, obj) - count++ - } +// for obj := range channel.(chan *arn.Inventory) { +// aeroDB.Set(typeName, obj.UserID, obj) +// count++ +// } - case "Settings": - channel, _ := arn.DB.All(typeName) +// case "Settings": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.Settings) { - aeroDB.Set(typeName, obj.UserID, obj) - count++ - } +// for obj := range channel.(chan *arn.Settings) { +// aeroDB.Set(typeName, obj.UserID, obj) +// count++ +// } - case "UserFollows": - channel, _ := arn.DB.All(typeName) +// case "UserFollows": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.UserFollows) { - aeroDB.Set(typeName, obj.UserID, obj) - count++ - } +// for obj := range channel.(chan *arn.UserFollows) { +// aeroDB.Set(typeName, obj.UserID, obj) +// count++ +// } - case "PayPalPayment": - channel, _ := arn.DB.All(typeName) +// case "PayPalPayment": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.PayPalPayment) { - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// for obj := range channel.(chan *arn.PayPalPayment) { +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "AniListToAnime": - channel, _ := arn.DB.All(typeName) +// case "AniListToAnime": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.AniListToAnime) { - aeroDB.Set(typeName, obj.ServiceID, obj) - count++ - } +// for obj := range channel.(chan *arn.AniListToAnime) { +// aeroDB.Set(typeName, obj.ServiceID, obj) +// count++ +// } - case "MyAnimeListToAnime": - channel, _ := arn.DB.All(typeName) +// case "MyAnimeListToAnime": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.MyAnimeListToAnime) { - aeroDB.Set(typeName, obj.ServiceID, obj) - count++ - } +// for obj := range channel.(chan *arn.MyAnimeListToAnime) { +// aeroDB.Set(typeName, obj.ServiceID, obj) +// count++ +// } - case "SearchIndex": - anime, _ := arn.DB.Get(typeName, "Anime") - aeroDB.Set(typeName, "Anime", anime) +// case "SearchIndex": +// anime, _ := arn.DB.Get(typeName, "Anime") +// aeroDB.Set(typeName, "Anime", anime) - users, _ := arn.DB.Get(typeName, "User") - aeroDB.Set(typeName, "User", users) +// users, _ := arn.DB.Get(typeName, "User") +// aeroDB.Set(typeName, "User", users) - posts, _ := arn.DB.Get(typeName, "Post") - aeroDB.Set(typeName, "Post", posts) +// posts, _ := arn.DB.Get(typeName, "Post") +// aeroDB.Set(typeName, "Post", posts) - threads, _ := arn.DB.Get(typeName, "Thread") - aeroDB.Set(typeName, "Thread", threads) +// threads, _ := arn.DB.Get(typeName, "Thread") +// aeroDB.Set(typeName, "Thread", threads) - count += 4 +// count += 4 - case "DraftIndex": - channel, _ := arn.DB.All(typeName) +// case "DraftIndex": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.DraftIndex) { - aeroDB.Set(typeName, obj.UserID, obj) - count++ - } +// for obj := range channel.(chan *arn.DraftIndex) { +// aeroDB.Set(typeName, obj.UserID, obj) +// count++ +// } - case "EmailToUser": - channel, _ := arn.DB.All(typeName) +// case "EmailToUser": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.EmailToUser) { - if obj.Email == "" { - continue - } +// for obj := range channel.(chan *arn.EmailToUser) { +// if obj.Email == "" { +// continue +// } - aeroDB.Set(typeName, obj.Email, obj) - count++ - } +// aeroDB.Set(typeName, obj.Email, obj) +// count++ +// } - case "FacebookToUser": - channel, _ := arn.DB.All(typeName) +// case "FacebookToUser": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.FacebookToUser) { - if obj.ID == "" { - continue - } +// for obj := range channel.(chan *arn.FacebookToUser) { +// if obj.ID == "" { +// continue +// } - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "GoogleToUser": - channel, _ := arn.DB.All(typeName) +// case "GoogleToUser": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.GoogleToUser) { - if obj.ID == "" { - continue - } +// for obj := range channel.(chan *arn.GoogleToUser) { +// if obj.ID == "" { +// continue +// } - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "TwitterToUser": - channel, _ := arn.DB.All(typeName) +// case "TwitterToUser": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.TwitterToUser) { - if obj.ID == "" { - continue - } +// for obj := range channel.(chan *arn.TwitterToUser) { +// if obj.ID == "" { +// continue +// } - aeroDB.Set(typeName, obj.ID, obj) - count++ - } +// aeroDB.Set(typeName, obj.ID, obj) +// count++ +// } - case "NickToUser": - channel, _ := arn.DB.All(typeName) +// case "NickToUser": +// channel, _ := arn.DB.All(typeName) - for obj := range channel.(chan *arn.NickToUser) { - if obj.Nick == "" { - continue - } +// for obj := range channel.(chan *arn.NickToUser) { +// if obj.Nick == "" { +// continue +// } - aeroDB.Set(typeName, obj.Nick, obj) - count++ - } +// aeroDB.Set(typeName, obj.Nick, obj) +// count++ +// } - default: - color.Yellow("Skipping %s", typeName) - continue - } +// default: +// color.Yellow("Skipping %s", typeName) +// continue +// } - color.Green("Export %d %s", count, typeName) - } +// color.Green("Export %d %s", count, typeName) +// } - time.Sleep(1 * time.Second) -} +// time.Sleep(1 * time.Second) +// } From b0d03fe8ef5113af9d6aa638db8616cef5aece6f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 10:28:40 +0100 Subject: [PATCH 498/527] Migration to nano --- jobs/refresh-episodes/refresh-episodes.go | 3 +- jobs/refresh-osu/refresh-osu.go | 1 + jobs/search-index/search-index.go | 39 ++++++----------------- jobs/sync-anime/sync-anime.go | 18 ++--------- jobs/sync-shoboi/sync-shoboi.go | 5 +-- jobs/twist/twist.go | 4 +-- 6 files changed, 19 insertions(+), 51 deletions(-) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 7f500693..5f7e9f00 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -10,6 +10,7 @@ import ( func main() { color.Yellow("Refreshing episode information for each anime.") + defer arn.Node.Close() if InvokeShellArgs() { return @@ -19,7 +20,7 @@ func main() { mediumPriority := []*arn.Anime{} lowPriority := []*arn.Anime{} - for anime := range arn.MustStreamAnime() { + for anime := range arn.StreamAnime() { if anime.GetMapping("shoboi/anime") == "" { continue } diff --git a/jobs/refresh-osu/refresh-osu.go b/jobs/refresh-osu/refresh-osu.go index 10568a66..8794c2d6 100644 --- a/jobs/refresh-osu/refresh-osu.go +++ b/jobs/refresh-osu/refresh-osu.go @@ -9,6 +9,7 @@ import ( func main() { color.Yellow("Refreshing osu information") + defer arn.Node.Close() ticker := time.NewTicker(500 * time.Millisecond) diff --git a/jobs/search-index/search-index.go b/jobs/search-index/search-index.go index 01cb9ada..e4b597d2 100644 --- a/jobs/search-index/search-index.go +++ b/jobs/search-index/search-index.go @@ -11,6 +11,7 @@ import ( func main() { color.Yellow("Updating search index") + defer arn.Node.Close() flow.Parallel( updateAnimeIndex, @@ -26,13 +27,7 @@ func updateAnimeIndex() { animeSearchIndex := arn.NewSearchIndex() // Anime - animeStream, err := arn.StreamAnime() - - if err != nil { - panic(err) - } - - for anime := range animeStream { + for anime := range arn.StreamAnime() { if anime.Title.Canonical != "" { animeSearchIndex.TextToID[strings.ToLower(anime.Title.Canonical)] = anime.ID } @@ -64,21 +59,14 @@ func updateAnimeIndex() { fmt.Println(len(animeSearchIndex.TextToID), "anime titles") // Save in database - err = arn.DB.Set("SearchIndex", "Anime", animeSearchIndex) - - if err != nil { - panic(err) - } + arn.DB.Set("SearchIndex", "Anime", animeSearchIndex) } func updateUserIndex() { userSearchIndex := arn.NewSearchIndex() // Users - userStream, err := arn.StreamUsers() - arn.PanicOnError(err) - - for user := range userStream { + for user := range arn.StreamUsers() { if user.HasNick() { userSearchIndex.TextToID[strings.ToLower(user.Nick)] = user.ID } @@ -87,36 +75,28 @@ func updateUserIndex() { fmt.Println(len(userSearchIndex.TextToID), "user names") // Save in database - err = arn.DB.Set("SearchIndex", "User", userSearchIndex) - arn.PanicOnError(err) + arn.DB.Set("SearchIndex", "User", userSearchIndex) } func updatePostIndex() { postSearchIndex := arn.NewSearchIndex() // Users - postStream, err := arn.StreamPosts() - arn.PanicOnError(err) - - for post := range postStream { + for post := range arn.StreamPosts() { postSearchIndex.TextToID[strings.ToLower(post.Text)] = post.ID } fmt.Println(len(postSearchIndex.TextToID), "posts") // Save in database - err = arn.DB.Set("SearchIndex", "Post", postSearchIndex) - arn.PanicOnError(err) + arn.DB.Set("SearchIndex", "Post", postSearchIndex) } func updateThreadIndex() { threadSearchIndex := arn.NewSearchIndex() // Users - threadStream, err := arn.StreamThreads() - arn.PanicOnError(err) - - for thread := range threadStream { + for thread := range arn.StreamThreads() { threadSearchIndex.TextToID[strings.ToLower(thread.Title)] = thread.ID threadSearchIndex.TextToID[strings.ToLower(thread.Text)] = thread.ID } @@ -124,6 +104,5 @@ func updateThreadIndex() { fmt.Println(len(threadSearchIndex.TextToID)/2, "threads") // Save in database - err = arn.DB.Set("SearchIndex", "Thread", threadSearchIndex) - arn.PanicOnError(err) + arn.DB.Set("SearchIndex", "Thread", threadSearchIndex) } diff --git a/jobs/sync-anime/sync-anime.go b/jobs/sync-anime/sync-anime.go index 2b97a418..7db928f0 100644 --- a/jobs/sync-anime/sync-anime.go +++ b/jobs/sync-anime/sync-anime.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "strings" @@ -12,6 +11,7 @@ import ( func main() { color.Yellow("Syncing Anime") + defer arn.Node.Close() // In case we refresh only one anime if InvokeShellArgs() { @@ -127,19 +127,7 @@ func sync(data *kitsu.Anime) *arn.Anime { } // Save in database - err = anime.Save() - status := "" - - if err == nil { - status = color.GreenString("✔") - } else { - color.Red(err.Error()) - - data, _ := json.MarshalIndent(anime, "", "\t") - fmt.Println(string(data)) - - status = color.RedString("✘") - } + anime.Save() // Episodes episodes, err := arn.GetAnimeEpisodes(anime.ID) @@ -149,7 +137,7 @@ func sync(data *kitsu.Anime) *arn.Anime { } // Log - fmt.Println(status, anime.ID, anime.Title.Canonical) + fmt.Println(color.GreenString("✔"), anime.ID, anime.Title.Canonical) return anime } diff --git a/jobs/sync-shoboi/sync-shoboi.go b/jobs/sync-shoboi/sync-shoboi.go index 4624adda..ab3eb549 100644 --- a/jobs/sync-shoboi/sync-shoboi.go +++ b/jobs/sync-shoboi/sync-shoboi.go @@ -10,13 +10,14 @@ import ( func main() { color.Yellow("Syncing Shoboi Anime") + defer arn.Node.Close() // Priority queues highPriority := []*arn.Anime{} mediumPriority := []*arn.Anime{} lowPriority := []*arn.Anime{} - for anime := range arn.MustStreamAnime() { + for anime := range arn.StreamAnime() { if anime.GetMapping("shoboi/anime") != "" { continue } @@ -51,7 +52,7 @@ func refreshQueue(queue []*arn.Anime) { for _, anime := range queue { if sync(anime) { - arn.PanicOnError(anime.Save()) + anime.Save() count++ } } diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index f4e557d9..b86631cf 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -21,9 +21,7 @@ func main() { idList := twistAnime.KitsuIDs() // Save index in cache - arn.DB.Set("Cache", "animetwist index", &arn.ListOfIDs{ - IDList: idList, - }) + arn.DB.Set("IDList", "animetwist index", idList) color.Yellow("Refreshing twist.moe links for %d anime", len(idList)) From 46dd9f53a87a1246cdd3a5bb407d1fcd3b50e67e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 11:16:31 +0100 Subject: [PATCH 499/527] Removed old jobs --- jobs/forum-activity/forum-activity.go | 49 --------------- jobs/popular-anime/popular-anime.go | 62 ------------------- .../refresh-track-titles.go | 37 ----------- jobs/sync-characters/sync-characters.go | 3 +- .../sync-media-relations.go | 15 ++--- patches/add-balance/add-balance.go | 8 +-- 6 files changed, 8 insertions(+), 166 deletions(-) delete mode 100644 jobs/forum-activity/forum-activity.go delete mode 100644 jobs/popular-anime/popular-anime.go delete mode 100644 jobs/refresh-track-titles/refresh-track-titles.go diff --git a/jobs/forum-activity/forum-activity.go b/jobs/forum-activity/forum-activity.go deleted file mode 100644 index 3c2bb808..00000000 --- a/jobs/forum-activity/forum-activity.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -const maxEntries = 5 - -func main() { - color.Yellow("Caching list of forum activities") - - posts, err := arn.AllPosts() - arn.PanicOnError(err) - - threads, err := arn.AllThreads() - arn.PanicOnError(err) - - arn.SortPostsLatestFirst(posts) - arn.SortThreadsLatestFirst(threads) - - posts = arn.FilterPostsWithUniqueThreads(posts, maxEntries) - - postPostables := arn.ToPostables(posts) - threadPostables := arn.ToPostables(threads) - - allPostables := append(postPostables, threadPostables...) - - arn.SortPostablesLatestFirst(allPostables) - cachedPostables := arn.FilterPostablesWithUniqueThreads(allPostables, maxEntries) - - cache := &arn.ListOfMappedIDs{} - - for _, postable := range cachedPostables { - cache.Append(postable.Type(), postable.ID()) - } - - // // Debug log - // arn.PrettyPrint(cache) - - // // Try to resolve - // for _, r := range arn.ToPostables(cache.Resolve()) { - // color.Green(r.Title()) - // } - - arn.PanicOnError(arn.DB.Set("Cache", "forum activity", cache)) - - color.Green("Finished.") -} diff --git a/jobs/popular-anime/popular-anime.go b/jobs/popular-anime/popular-anime.go deleted file mode 100644 index 10a5483a..00000000 --- a/jobs/popular-anime/popular-anime.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "sort" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -const maxPopularAnime = 10 - -// Note this is using the airing-anime as a template with modfications -// made to it. -func main() { - color.Yellow("Caching popular anime") - - // Fetch all anime - animeList, err := arn.AllAnime() - arn.PanicOnError(err) - - // Overall - sort.Slice(animeList, func(i, j int) bool { - return animeList[i].Rating.Overall > animeList[j].Rating.Overall - }) - - saveAs(animeList[:maxPopularAnime], "best anime overall") - - // Story - sort.Slice(animeList, func(i, j int) bool { - return animeList[i].Rating.Story > animeList[j].Rating.Story - }) - - saveAs(animeList[:maxPopularAnime], "best anime story") - - // Visuals - sort.Slice(animeList, func(i, j int) bool { - return animeList[i].Rating.Visuals > animeList[j].Rating.Visuals - }) - - saveAs(animeList[:maxPopularAnime], "best anime visuals") - - // Soundtrack - sort.Slice(animeList, func(i, j int) bool { - return animeList[i].Rating.Soundtrack > animeList[j].Rating.Soundtrack - }) - - saveAs(animeList[:maxPopularAnime], "best anime soundtrack") - - // Done. - color.Green("Finished.") -} - -// Convert to ListOfIDs and save in cache. -func saveAs(list []*arn.Anime, cacheKey string) { - cache := &arn.ListOfIDs{} - - for _, anime := range list { - cache.IDList = append(cache.IDList, anime.ID) - } - - arn.PanicOnError(arn.DB.Set("Cache", cacheKey, cache)) -} diff --git a/jobs/refresh-track-titles/refresh-track-titles.go b/jobs/refresh-track-titles/refresh-track-titles.go deleted file mode 100644 index e6ed3d7f..00000000 --- a/jobs/refresh-track-titles/refresh-track-titles.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - color.Yellow("Refreshing track titles") - - // Get a stream of all soundtracks - soundtracks, err := arn.StreamSoundTracks() - - if err != nil { - panic(err) - } - - // Iterate over the stream - for track := range soundtracks { - sync(track) - } - - color.Green("Finished.") -} - -func sync(track *arn.SoundTrack) { - // for _, media := range track.Media { - // media.RefreshMetaData() - // println(media.Service, media.Title) - // } - - // err := track.Save() - - // if err != nil { - // panic(err) - // } -} diff --git a/jobs/sync-characters/sync-characters.go b/jobs/sync-characters/sync-characters.go index 3668d3fa..76ad09a8 100644 --- a/jobs/sync-characters/sync-characters.go +++ b/jobs/sync-characters/sync-characters.go @@ -10,6 +10,7 @@ import ( func main() { color.Yellow("Syncing characters with Kitsu DB") + defer arn.Node.Close() kitsuCharacters := kitsu.StreamCharacters() @@ -23,7 +24,7 @@ func main() { fmt.Printf("%s %s\n", character.ID, character.Name) - arn.PanicOnError(character.Save()) + character.Save() } color.Green("Finished.") diff --git a/jobs/sync-media-relations/sync-media-relations.go b/jobs/sync-media-relations/sync-media-relations.go index 019e5c4f..2a9fca9d 100644 --- a/jobs/sync-media-relations/sync-media-relations.go +++ b/jobs/sync-media-relations/sync-media-relations.go @@ -12,6 +12,7 @@ import ( func main() { color.Yellow("Syncing media relations with Kitsu DB") + defer arn.Node.Close() kitsuMediaRelations := kitsu.StreamMediaRelations() relations := map[string]*arn.AnimeRelations{} @@ -27,15 +28,11 @@ func main() { destinationAnimeID := mediaRelation.Relationships.Destination.Data.ID // Confirm that the anime IDs are valid - exists, _ := arn.DB.Exists("Anime", animeID) - - if !exists { + if !arn.DB.Exists("Anime", animeID) { continue } - exists, _ = arn.DB.Exists("Anime", destinationAnimeID) - - if !exists { + if !arn.DB.Exists("Anime", destinationAnimeID) { continue } @@ -71,11 +68,7 @@ func main() { // Save relations map for _, animeRelations := range relations { - err := animeRelations.Save() - - if err != nil { - color.Red(err.Error()) - } + animeRelations.Save() } color.Green("Finished.") diff --git a/patches/add-balance/add-balance.go b/patches/add-balance/add-balance.go index 8289aab3..dd2326da 100644 --- a/patches/add-balance/add-balance.go +++ b/patches/add-balance/add-balance.go @@ -8,14 +8,10 @@ import ( func main() { color.Yellow("Adding balance to all users") - // Get a stream of all users - allUsers, err := arn.StreamUsers() - arn.PanicOnError(err) - // Iterate over the stream - for user := range allUsers { + for user := range arn.StreamUsers() { user.Balance += 100000 - arn.PanicOnError(user.Save()) + user.Save() } color.Green("Finished.") From 9b0f01f96ebd80595138eec7e22661fc44663924 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 11:27:30 +0100 Subject: [PATCH 500/527] Improved installation guide --- CONTRIBUTING.md | 2 ++ INSTALLATION.md | 54 ++++++++++++++----------------------------------- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 98454386..52eaf6bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,3 +3,5 @@ Please get in contact with the team on the [Anime Notifier Discord](https://discord.gg/0kimAmMCeXGXuzNF). We're willing to help with installations and how to get started with contributions. There are no stupid questions so feel free to ask anything if you encounter any troubles. + +If you'd like to install this project locally, take a look at the [Installation](INSTALLATION.md) guide. \ No newline at end of file diff --git a/INSTALLATION.md b/INSTALLATION.md index ef45b52b..2e83b402 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -1,60 +1,36 @@ -# Anime Notifier +# Installation -## Installation +## Prerequisites -### Prerequisites - -* Install a Debian based operating system +* Install [Ubuntu](https://www.ubuntu.com/) or any of its derivates * Install [Go](https://golang.org/dl/) (1.9 or higher) * Install [TypeScript](https://www.typescriptlang.org/) (2.5 or higher) -* Install [Aerospike](http://www.aerospike.com/download) (3.14.0 or higher) -### Download the repository and its dependencies +## Download the repository and its dependencies * `go get github.com/animenotifier/notify.moe` -### Build all +## Build all +* Navigate to the project directory `notify.moe` * Run `make tools` to install [pack](https://github.com/aerogo/pack) & [run](https://github.com/aerogo/run) -* Run `make all` * Run `make ports` to set up local port forwarding *(80 to 4000, 443 to 4001)* +* Run `make all` -### Database +## Hosts -* Remove all namespaces in `/etc/aerospike/aerospike.conf` -* Add a namespace called `arn`: - -``` -namespace arn { - storage-engine device { - file /home/YOUR_NAME/YOUR_PATH/notify.moe/db/arn-dev.dat - filesize 300M - data-in-memory true - - # Maximum object size. 128K is ideal for SSDs but we need 1M for search indices. - write-block-size 1M - - # Write block size x Post write queue = Cache memory usage (for write block buffers) - post-write-queue 1 - } -} -``` - -* Download the database for developers (get in contact with me to receive a link) -* Start the database using `sudo service aerospike start` -* Confirm that the status is "green": `sudo service aerospike status` - -### Hosts - -* Add `127.0.0.1 arn-db` to `/etc/hosts` * Add `127.0.0.1 beta.notify.moe` to `/etc/hosts` -### HTTPS +## HTTPS * Create the certificate `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) * Create the private key `notify.moe/security/privkey.pem` -### API keys +## Browser + +* Start Chrome via `google-chrome --ignore-certificate-errors` + +## API keys * Get a Google OAuth 2.0 client key & secret from [console.developers.google.com](https://console.developers.google.com) * Create the file `notify.moe/security/api-keys.json`: @@ -68,7 +44,7 @@ namespace arn { } ``` -### Run +## Run * Start the web server in notify.moe directory: `run` * Open `https://beta.notify.moe` which should now resolve to localhost \ No newline at end of file From 5adc2def74eac15577e26ecfb31579b3212e1b7e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 11:30:01 +0100 Subject: [PATCH 501/527] Minor change --- INSTALLATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 2e83b402..07d33897 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -23,7 +23,7 @@ ## HTTPS -* Create the certificate `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) +* [Create the certificate](https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl) `notify.moe/security/fullchain.pem` (domain: `beta.notify.moe`) * Create the private key `notify.moe/security/privkey.pem` ## Browser From e70dc0daed564fa63022fa886fa8c3277f7beb5e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 19:27:04 +0100 Subject: [PATCH 502/527] Minor changes --- pages/editor/anilist.pixy | 2 +- pages/editor/shoboi.pixy | 2 +- patches/add-anime-lists/add-anime-lists.go | 39 ---------------------- 3 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 patches/add-anime-lists/add-anime-lists.go diff --git a/pages/editor/anilist.pixy b/pages/editor/anilist.pixy index d27b6086..be296eca 100644 --- a/pages/editor/anilist.pixy +++ b/pages/editor/anilist.pixy @@ -13,7 +13,7 @@ component AniListMissingMapping(missing []*arn.Anime) th Tools tbody each anime in missing - tr + tr.mountable td= anime.Popularity.Total() td a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical diff --git a/pages/editor/shoboi.pixy b/pages/editor/shoboi.pixy index 8dd2aca0..9783b645 100644 --- a/pages/editor/shoboi.pixy +++ b/pages/editor/shoboi.pixy @@ -13,7 +13,7 @@ component ShoboiMissingMapping(missing []*arn.Anime) th Tools tbody each anime in missing - tr + tr.mountable td= anime.Popularity.Total() td a(href=anime.Link(), target="_blank", rel="noopener")= anime.Title.Canonical diff --git a/patches/add-anime-lists/add-anime-lists.go b/patches/add-anime-lists/add-anime-lists.go deleted file mode 100644 index 88776a6f..00000000 --- a/patches/add-anime-lists/add-anime-lists.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - color.Yellow("Adding empty anime lists to users who don't have one") - - // Get a stream of all users - allUsers, err := arn.StreamUsers() - - if err != nil { - panic(err) - } - - // Iterate over the stream - for user := range allUsers { - exists, err := arn.DB.Exists("AnimeList", user.ID) - - if err == nil && !exists { - fmt.Println(user.Nick) - - err := arn.DB.Set("AnimeList", user.ID, &arn.AnimeList{ - UserID: user.ID, - Items: make([]*arn.AnimeListItem, 0), - }) - - if err != nil { - color.Red(err.Error()) - } - } - } - - color.Green("Finished.") -} From cf00461febfa0da4e3368b5523321c3c8183eef9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 20:11:05 +0100 Subject: [PATCH 503/527] Removed old patches --- patches/add-balance/add-balance.go | 18 --------- patches/add-draft-index/add-draft-index.go | 22 ----------- patches/add-episodes/add-episodes.go | 28 ------------- patches/add-follows/add-follows.go | 39 ------------------- patches/add-item/add-item.go | 5 ++- patches/add-last-seen/add-last-seen.go | 29 -------------- .../add-mal-connections.go | 8 ++-- patches/add-mappings/add-mappings.go | 28 ------------- patches/add-popularity/add-popularity.go | 16 -------- patches/add-push-subs/add-push-subs.go | 39 ------------------- .../clear-anime-ratings.go | 6 ++- patches/clear-sessions/clear-sessions.go | 2 + .../delete-custom-anime.go | 9 +++-- patches/delete-balance/delete-balance.go | 9 ++--- .../delete-invalid-avatars.go | 17 -------- .../delete-private-data.go | 15 +++---- patches/delete-pro/delete-pro.go | 5 ++- patches/fix-airing-dates/fix-airing-dates.go | 6 ++- .../fix-anime-list-item-status.go} | 13 ++----- patches/import-anilist/import-anilist.go | 5 ++- patches/nano-test/main.go | 19 --------- patches/post-texts/post-texts.go | 9 ++--- .../reset-inventories/reset-inventories.go | 13 ++----- patches/thread-posts/thread-posts.go | 10 ++--- .../update-soundtracks/update-soundtracks.go | 11 ------ .../update-user-struct/update-user-struct.go | 22 ----------- patches/user-references/user-references.go | 19 ++++----- .../video-id-to-service-id.go | 38 ------------------ 28 files changed, 57 insertions(+), 403 deletions(-) delete mode 100644 patches/add-balance/add-balance.go delete mode 100644 patches/add-draft-index/add-draft-index.go delete mode 100644 patches/add-episodes/add-episodes.go delete mode 100644 patches/add-follows/add-follows.go delete mode 100644 patches/add-last-seen/add-last-seen.go delete mode 100644 patches/add-mappings/add-mappings.go delete mode 100644 patches/add-popularity/add-popularity.go delete mode 100644 patches/add-push-subs/add-push-subs.go delete mode 100644 patches/delete-invalid-avatars/delete-invalid-avatars.go rename patches/{anime-list-item-status/anime-list-item-status.go => fix-anime-list-item-status/fix-anime-list-item-status.go} (75%) delete mode 100644 patches/nano-test/main.go delete mode 100644 patches/update-soundtracks/update-soundtracks.go delete mode 100644 patches/update-user-struct/update-user-struct.go delete mode 100644 patches/video-id-to-service-id/video-id-to-service-id.go diff --git a/patches/add-balance/add-balance.go b/patches/add-balance/add-balance.go deleted file mode 100644 index dd2326da..00000000 --- a/patches/add-balance/add-balance.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - color.Yellow("Adding balance to all users") - - // Iterate over the stream - for user := range arn.StreamUsers() { - user.Balance += 100000 - user.Save() - } - - color.Green("Finished.") -} diff --git a/patches/add-draft-index/add-draft-index.go b/patches/add-draft-index/add-draft-index.go deleted file mode 100644 index 260f4810..00000000 --- a/patches/add-draft-index/add-draft-index.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - color.Yellow("Addind draft indices") - - // Iterate over the stream - for user := range arn.MustStreamUsers() { - fmt.Println(user.Nick) - - draftIndex := arn.NewDraftIndex(user.ID) - arn.PanicOnError(draftIndex.Save()) - } - - color.Green("Finished.") -} diff --git a/patches/add-episodes/add-episodes.go b/patches/add-episodes/add-episodes.go deleted file mode 100644 index 111d0644..00000000 --- a/patches/add-episodes/add-episodes.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/animenotifier/arn" -) - -func main() { - count := 0 - - for anime := range arn.MustStreamAnime() { - episodes := anime.Episodes() - - if episodes == nil { - episodes = &arn.AnimeEpisodes{ - AnimeID: anime.ID, - Items: []*arn.AnimeEpisode{}, - } - - if episodes.Save() == nil { - count++ - } - } - } - - fmt.Println("Added empty anime episodes to", count, "anime.") -} diff --git a/patches/add-follows/add-follows.go b/patches/add-follows/add-follows.go deleted file mode 100644 index a30c9a40..00000000 --- a/patches/add-follows/add-follows.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - color.Yellow("Adding user follows to users who don't have one") - - // Get a stream of all users - allUsers, err := arn.StreamUsers() - arn.PanicOnError(err) - - // Iterate over the stream - for user := range allUsers { - exists, err := arn.DB.Exists("UserFollows", user.ID) - - if err != nil || exists { - continue - } - - fmt.Println(user.Nick) - - follows := &arn.UserFollows{} - follows.UserID = user.ID - follows.Items = user.Following - - err = arn.DB.Set("UserFollows", follows.UserID, follows) - - if err != nil { - color.Red(err.Error()) - } - } - - color.Green("Finished.") -} diff --git a/patches/add-item/add-item.go b/patches/add-item/add-item.go index d624745b..85ad81d5 100644 --- a/patches/add-item/add-item.go +++ b/patches/add-item/add-item.go @@ -19,6 +19,8 @@ func init() { } func main() { + defer arn.Node.Close() + if nick == "" || itemID == "" { color.Red("Missing parameters") return @@ -38,6 +40,5 @@ func main() { // Add to user inventory inventory := user.Inventory() inventory.AddItem(itemID, uint(quantity)) - err = inventory.Save() - arn.PanicOnError(err) + inventory.Save() } diff --git a/patches/add-last-seen/add-last-seen.go b/patches/add-last-seen/add-last-seen.go deleted file mode 100644 index d2c3f228..00000000 --- a/patches/add-last-seen/add-last-seen.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" -) - -func main() { - // Get a stream of all users - allUsers, err := arn.StreamUsers() - - if err != nil { - panic(err) - } - - // Iterate over the stream - for user := range allUsers { - if user.LastSeen != "" { - continue - } - - user.LastSeen = user.LastLogin - - if user.LastSeen == "" { - user.LastSeen = user.Registered - } - - user.Save() - } -} diff --git a/patches/add-mal-connections/add-mal-connections.go b/patches/add-mal-connections/add-mal-connections.go index b9bf2596..2f3a8057 100644 --- a/patches/add-mal-connections/add-mal-connections.go +++ b/patches/add-mal-connections/add-mal-connections.go @@ -8,7 +8,9 @@ import ( ) func main() { - for anime := range arn.MustStreamAnime() { + defer arn.Node.Close() + + for anime := range arn.StreamAnime() { malID := anime.GetMapping("myanimelist/anime") if malID == "" { @@ -25,11 +27,11 @@ func main() { } // Save - arn.PanicOnError(arn.DB.Set("MyAnimeListToAnime", malID, &arn.MyAnimeListToAnime{ + arn.DB.Set("MyAnimeListToAnime", malID, &arn.MyAnimeListToAnime{ AnimeID: anime.ID, ServiceID: malID, Edited: arn.DateTimeUTC(), EditedBy: "", - })) + }) } } diff --git a/patches/add-mappings/add-mappings.go b/patches/add-mappings/add-mappings.go deleted file mode 100644 index 12c2f541..00000000 --- a/patches/add-mappings/add-mappings.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/animenotifier/arn" -) - -var mappings = map[string]arn.Mapping{ - "13055": arn.Mapping{ - Service: "shoboi/anime", - ServiceID: "4528", - }, -} - -func main() { - for animeID, mapping := range mappings { - anime, err := arn.GetAnime(animeID) - - if err != nil { - panic(err) - } - - fmt.Println(anime.ID, "=", mapping.Service, mapping.ServiceID) - anime.AddMapping(mapping.Service, mapping.ServiceID, "4J6qpK1ve") - anime.Save() - } -} diff --git a/patches/add-popularity/add-popularity.go b/patches/add-popularity/add-popularity.go deleted file mode 100644 index c799eaca..00000000 --- a/patches/add-popularity/add-popularity.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" -) - -func main() { - for anime := range arn.MustStreamAnime() { - if anime.Popularity != nil { - continue - } - - anime.Popularity = &arn.AnimePopularity{} - arn.PanicOnError(anime.Save()) - } -} diff --git a/patches/add-push-subs/add-push-subs.go b/patches/add-push-subs/add-push-subs.go deleted file mode 100644 index 932487a2..00000000 --- a/patches/add-push-subs/add-push-subs.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/animenotifier/arn" - "github.com/fatih/color" -) - -func main() { - color.Yellow("Adding push subscriptions to users who don't have one") - - // Get a stream of all users - allUsers, err := arn.StreamUsers() - - if err != nil { - panic(err) - } - - // Iterate over the stream - for user := range allUsers { - exists, err := arn.DB.Exists("PushSubscriptions", user.ID) - - if err == nil && !exists { - fmt.Println(user.Nick) - - err := arn.DB.Set("PushSubscriptions", user.ID, &arn.PushSubscriptions{ - UserID: user.ID, - Items: make([]*arn.PushSubscription, 0), - }) - - if err != nil { - color.Red(err.Error()) - } - } - } - - color.Green("Finished.") -} diff --git a/patches/clear-anime-ratings/clear-anime-ratings.go b/patches/clear-anime-ratings/clear-anime-ratings.go index 359dea2c..c847c56f 100644 --- a/patches/clear-anime-ratings/clear-anime-ratings.go +++ b/patches/clear-anime-ratings/clear-anime-ratings.go @@ -3,8 +3,10 @@ package main import "github.com/animenotifier/arn" func main() { - for anime := range arn.MustStreamAnime() { + defer arn.Node.Close() + + for anime := range arn.StreamAnime() { anime.Rating.Reset() - anime.MustSave() + anime.Save() } } diff --git a/patches/clear-sessions/clear-sessions.go b/patches/clear-sessions/clear-sessions.go index 66324afc..e811932f 100644 --- a/patches/clear-sessions/clear-sessions.go +++ b/patches/clear-sessions/clear-sessions.go @@ -6,6 +6,8 @@ import ( ) func main() { + defer arn.Node.Close() + color.Yellow("Deleting all sessions...") arn.DB.Clear("Session") color.Green("Finished.") diff --git a/patches/delete-anilist-mappings/delete-custom-anime.go b/patches/delete-anilist-mappings/delete-custom-anime.go index 1160d3f8..b9fa6232 100644 --- a/patches/delete-anilist-mappings/delete-custom-anime.go +++ b/patches/delete-anilist-mappings/delete-custom-anime.go @@ -5,11 +5,12 @@ import ( ) func main() { - for anime := range arn.MustStreamAnime() { + defer arn.Node.Close() + + for anime := range arn.StreamAnime() { providerID := anime.GetMapping("anilist/anime") - _, err := arn.DB.Delete("AniListToAnime", providerID) - arn.PanicOnError(err) + arn.DB.Delete("AniListToAnime", providerID) anime.RemoveMapping("anilist/anime", providerID) - arn.PanicOnError(anime.Save()) + anime.Save() } } diff --git a/patches/delete-balance/delete-balance.go b/patches/delete-balance/delete-balance.go index bc329500..0dfa6e0b 100644 --- a/patches/delete-balance/delete-balance.go +++ b/patches/delete-balance/delete-balance.go @@ -23,15 +23,12 @@ func main() { } color.Yellow("Resetting balance of all users to 0") - - // Get a stream of all users - allUsers, err := arn.StreamUsers() - arn.PanicOnError(err) + defer arn.Node.Close() // Iterate over the stream - for user := range allUsers { + for user := range arn.StreamUsers() { user.Balance = 0 - arn.PanicOnError(user.Save()) + user.Save() } color.Green("Finished.") diff --git a/patches/delete-invalid-avatars/delete-invalid-avatars.go b/patches/delete-invalid-avatars/delete-invalid-avatars.go deleted file mode 100644 index 18e7016c..00000000 --- a/patches/delete-invalid-avatars/delete-invalid-avatars.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "strings" - - "github.com/animenotifier/arn" -) - -func main() { - for user := range arn.MustStreamUsers() { - if !strings.HasPrefix(user.Avatar.Extension, ".") { - user.Avatar.Extension = "" - } - - user.Save() - } -} diff --git a/patches/delete-private-data/delete-private-data.go b/patches/delete-private-data/delete-private-data.go index 6b3d4ad2..7520cdf1 100644 --- a/patches/delete-private-data/delete-private-data.go +++ b/patches/delete-private-data/delete-private-data.go @@ -7,20 +7,15 @@ import ( func main() { color.Yellow("Deleting private user data") + defer arn.Node.Close() - // Get a stream of all users - allUsers, err := arn.StreamUsers() - - if err != nil { - panic(err) - } - - arn.DB.DeleteTable("EmailToUser") - arn.DB.DeleteTable("GoogleToUser") + arn.DB.Clear("EmailToUser") + arn.DB.Clear("GoogleToUser") // Iterate over the stream count := 0 - for user := range allUsers { + + for user := range arn.StreamUsers() { count++ println(count, user.Nick) diff --git a/patches/delete-pro/delete-pro.go b/patches/delete-pro/delete-pro.go index 148c7421..1072d682 100644 --- a/patches/delete-pro/delete-pro.go +++ b/patches/delete-pro/delete-pro.go @@ -23,10 +23,11 @@ func main() { } color.Yellow("Deleting all pro subscriptions") + defer arn.Node.Close() - for user := range arn.MustStreamUsers() { + for user := range arn.StreamUsers() { user.ProExpires = "" - arn.PanicOnError(user.Save()) + user.Save() } color.Green("Finished.") diff --git a/patches/fix-airing-dates/fix-airing-dates.go b/patches/fix-airing-dates/fix-airing-dates.go index 9d90af62..87b12547 100644 --- a/patches/fix-airing-dates/fix-airing-dates.go +++ b/patches/fix-airing-dates/fix-airing-dates.go @@ -9,10 +9,12 @@ import ( ) func main() { + defer arn.Node.Close() + now := time.Now() futureThreshold := 8 * 7 * 24 * time.Hour - for anime := range arn.MustStreamAnime() { + for anime := range arn.StreamAnime() { modified := false // Try to find incorrect airing dates @@ -38,7 +40,7 @@ func main() { } if modified == true { - arn.PanicOnError(anime.Episodes().Save()) + anime.Episodes().Save() } } } diff --git a/patches/anime-list-item-status/anime-list-item-status.go b/patches/fix-anime-list-item-status/fix-anime-list-item-status.go similarity index 75% rename from patches/anime-list-item-status/anime-list-item-status.go rename to patches/fix-anime-list-item-status/fix-anime-list-item-status.go index 97233c47..7f9323d8 100644 --- a/patches/anime-list-item-status/anime-list-item-status.go +++ b/patches/fix-anime-list-item-status/fix-anime-list-item-status.go @@ -9,16 +9,10 @@ import ( func main() { color.Yellow("Setting list item status to correct value") - - // Get a stream of all anime lists - allAnimeLists, err := arn.StreamAnimeLists() - - if err != nil { - panic(err) - } + defer arn.Node.Close() // Iterate over the stream - for animeList := range allAnimeLists { + for animeList := range arn.StreamAnimeLists() { fmt.Println(animeList.User().Nick) for _, item := range animeList.Items { @@ -32,8 +26,7 @@ func main() { } } - err := animeList.Save() - arn.PanicOnError(err) + animeList.Save() } color.Green("Finished.") diff --git a/patches/import-anilist/import-anilist.go b/patches/import-anilist/import-anilist.go index 17335d01..1bd427ce 100644 --- a/patches/import-anilist/import-anilist.go +++ b/patches/import-anilist/import-anilist.go @@ -7,6 +7,8 @@ import ( ) func main() { + defer arn.Node.Close() + arn.PanicOnError(anilist.Authorize()) color.Green(anilist.AccessToken) @@ -14,9 +16,8 @@ func main() { arn.PanicOnError(err) count := 0 - stream := anilist.StreamAnime() - for aniListAnime := range stream { + for aniListAnime := range anilist.StreamAnime() { println(aniListAnime.TitleRomaji) anime := arn.FindAniListAnime(aniListAnime, allAnime) diff --git a/patches/nano-test/main.go b/patches/nano-test/main.go deleted file mode 100644 index af91a252..00000000 --- a/patches/nano-test/main.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" -) - -func main() { - defer arn.Node.Close() - - user, _ := arn.GetUserByNick("Akyoto") - - if user.Language == ":)" { - user.Language = ":(" - } else { - user.Language = ":)" - } - - user.Save() -} diff --git a/patches/post-texts/post-texts.go b/patches/post-texts/post-texts.go index d448616d..4c5fb314 100644 --- a/patches/post-texts/post-texts.go +++ b/patches/post-texts/post-texts.go @@ -7,12 +7,10 @@ import ( ) func main() { - // Get a stream of all posts - allPosts, err := arn.StreamPosts() - arn.PanicOnError(err) + defer arn.Node.Close() // Iterate over the stream - for post := range allPosts { + for post := range arn.StreamPosts() { // Fix text color.Yellow(post.Text) post.Text = autocorrect.FixPostText(post.Text) @@ -24,7 +22,6 @@ func main() { } // Save - err = post.Save() - arn.PanicOnError(err) + post.Save() } } diff --git a/patches/reset-inventories/reset-inventories.go b/patches/reset-inventories/reset-inventories.go index 5e4e8149..413bbb66 100644 --- a/patches/reset-inventories/reset-inventories.go +++ b/patches/reset-inventories/reset-inventories.go @@ -24,21 +24,14 @@ func main() { } color.Yellow("Resetting all inventories") - - // Get a stream of all users - allUsers, err := arn.StreamUsers() - arn.PanicOnError(err) + defer arn.Node.Close() // Iterate over the stream - for user := range allUsers { + for user := range arn.StreamUsers() { fmt.Println(user.Nick) inventory := arn.NewInventory(user.ID) - err = inventory.Save() - - if err != nil { - color.Red(err.Error()) - } + inventory.Save() } color.Green("Finished.") diff --git a/patches/thread-posts/thread-posts.go b/patches/thread-posts/thread-posts.go index c668ab73..166d414b 100644 --- a/patches/thread-posts/thread-posts.go +++ b/patches/thread-posts/thread-posts.go @@ -5,14 +5,13 @@ import ( ) func main() { - // Get a stream of all posts - allPosts, err := arn.StreamPosts() - arn.PanicOnError(err) + defer arn.Node.Close() + // Get a stream of all posts threadToPosts := make(map[string][]string) // Iterate over the stream - for post := range allPosts { + for post := range arn.StreamPosts() { _, found := threadToPosts[post.ThreadID] if !found { @@ -28,7 +27,6 @@ func main() { arn.PanicOnError(err) thread.Posts = posts - err = thread.Save() - arn.PanicOnError(err) + thread.Save() } } diff --git a/patches/update-soundtracks/update-soundtracks.go b/patches/update-soundtracks/update-soundtracks.go deleted file mode 100644 index 833156c2..00000000 --- a/patches/update-soundtracks/update-soundtracks.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "github.com/animenotifier/arn" -) - -func main() { - for track := range arn.MustStreamSoundTracks() { - arn.PanicOnError(track.Save()) - } -} diff --git a/patches/update-user-struct/update-user-struct.go b/patches/update-user-struct/update-user-struct.go deleted file mode 100644 index 512cd1ee..00000000 --- a/patches/update-user-struct/update-user-struct.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "github.com/fatih/color" -) - -func main() { - color.Yellow("Updating user struct") - - // // Iterate over the stream - // for user := range arn.MustStreamUsers() { - // newUser := &arn.UserNew{} - - // copier.Copy(newUser, user) - // newUser.Avatar.Extension = user.Avatar - - // // Save in DB - // arn.PanicOnError(arn.DB.Set("User", user.ID, newUser)) - // } - - color.Green("Finished.") -} diff --git a/patches/user-references/user-references.go b/patches/user-references/user-references.go index c25e37a8..f5aab5f7 100644 --- a/patches/user-references/user-references.go +++ b/patches/user-references/user-references.go @@ -7,22 +7,17 @@ import ( func main() { color.Yellow("Updating user references") + defer arn.Node.Close() - arn.DB.DeleteTable("NickToUser") - arn.DB.DeleteTable("EmailToUser") - arn.DB.DeleteTable("GoogleToUser") - arn.DB.DeleteTable("FacebookToUser") - - // Get a stream of all users - allUsers, err := arn.StreamUsers() - - if err != nil { - panic(err) - } + arn.DB.Clear("NickToUser") + arn.DB.Clear("EmailToUser") + arn.DB.Clear("GoogleToUser") + arn.DB.Clear("FacebookToUser") // Iterate over the stream count := 0 - for user := range allUsers { + + for user := range arn.StreamUsers() { count++ println(count, user.Nick) diff --git a/patches/video-id-to-service-id/video-id-to-service-id.go b/patches/video-id-to-service-id/video-id-to-service-id.go deleted file mode 100644 index d2d7136a..00000000 --- a/patches/video-id-to-service-id/video-id-to-service-id.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -func main() { - -} - -// import ( -// "github.com/animenotifier/arn" -// "github.com/fatih/color" -// ) - -// func main() { -// // Get a stream of all anime -// allAnime, err := arn.AllAnime() - -// if err != nil { -// panic(err) -// } - -// // Iterate over the stream -// for _, anime := range allAnime { -// for _, trailer := range anime.Trailers { -// // trailer.ServiceID = trailer.DeprecatedVideoID -// println(trailer.DeprecatedVideoID) -// trailer.ServiceID = trailer.DeprecatedVideoID -// } - -// if anime.Trailers == nil { -// anime.Trailers = []*arn.ExternalMedia{} -// } - -// err := anime.Save() - -// if err != nil { -// color.Red("Error saving anime: %v", err) -// } -// } -// } From 2de358c843d65b7d640830efc9dec1b1ecb62778 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 1 Nov 2017 20:12:38 +0100 Subject: [PATCH 504/527] Minor change --- jobs/anime-characters/anime-characters.go | 1 + jobs/anime-images/anime-images.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/jobs/anime-characters/anime-characters.go b/jobs/anime-characters/anime-characters.go index 8a54bfed..a99565b9 100644 --- a/jobs/anime-characters/anime-characters.go +++ b/jobs/anime-characters/anime-characters.go @@ -10,6 +10,7 @@ import ( func main() { color.Yellow("Refreshing anime characters...") + defer arn.Node.Close() allAnime, _ := arn.AllAnime() rateLimiter := time.NewTicker(500 * time.Millisecond) diff --git a/jobs/anime-images/anime-images.go b/jobs/anime-images/anime-images.go index 08a68f46..a7390908 100644 --- a/jobs/anime-images/anime-images.go +++ b/jobs/anime-images/anime-images.go @@ -17,6 +17,8 @@ var ticker = time.NewTicker(50 * time.Millisecond) func main() { color.Yellow("Downloading anime images") + defer arn.Node.Close() + jobs := jobqueue.New(work) allAnime, _ := arn.AllAnime() From 94b3fd997e31d2e765b746ea82acc81c10461316 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 2 Nov 2017 04:08:45 +0100 Subject: [PATCH 505/527] Added node close on shutdown --- main.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 48f4f1cf..3bcc3bef 100644 --- a/main.go +++ b/main.go @@ -207,9 +207,6 @@ func configure(app *aero.Application) *aero.Application { middleware.UserInfo(), ) - // Database - arn.DB.PrefetchData() - // API arn.API.Install(app) @@ -221,6 +218,12 @@ func configure(app *aero.Application) *aero.Application { // Authentication auth.Install(app) + // Close the database node on shutdown + app.OnShutdown(arn.Node.Close) + + // Prefetch data from all collections + arn.DB.PrefetchData() + // Specify test routes for route, examples := range routeTests { app.Test(route, examples) From f63aaa343c1362f0e93834250f28483ca21b83ee Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 2 Nov 2017 07:25:40 +0100 Subject: [PATCH 506/527] Minor change --- jobs/avatars/avatars.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jobs/avatars/avatars.go b/jobs/avatars/avatars.go index 60c82d64..f0fb8c7c 100644 --- a/jobs/avatars/avatars.go +++ b/jobs/avatars/avatars.go @@ -45,10 +45,8 @@ func main() { usersQueue := make(chan *arn.User, runtime.NumCPU()) StartWorkers(usersQueue, lib.RefreshAvatar) - allUsers, _ := arn.AllUsers() - // We'll send each user to one of the worker threads - for _, user := range allUsers { + for user := range arn.StreamUsers() { wg.Add(1) usersQueue <- user } From dd4ba190d7a676c5d03af27431f64ad4d36613b2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 2 Nov 2017 09:26:05 +0100 Subject: [PATCH 507/527] Minor update --- jobs/refresh-osu/refresh-osu.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jobs/refresh-osu/refresh-osu.go b/jobs/refresh-osu/refresh-osu.go index 8794c2d6..04639099 100644 --- a/jobs/refresh-osu/refresh-osu.go +++ b/jobs/refresh-osu/refresh-osu.go @@ -13,9 +13,7 @@ func main() { ticker := time.NewTicker(500 * time.Millisecond) - allUsers, _ := arn.AllUsers() - - for _, user := range allUsers { + for user := range arn.StreamUsers() { // Get osu info if user.RefreshOsuInfo() == nil { arn.PrettyPrint(user.Accounts.Osu) From c58679834dc9f01cb4aa3e624d7e1d5170956c91 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 2 Nov 2017 10:39:07 +0100 Subject: [PATCH 508/527] Fixed assets --- assets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets.go b/assets.go index 916b240e..f613eb03 100644 --- a/assets.go +++ b/assets.go @@ -66,7 +66,7 @@ func configureAssets(app *aero.Application) { // Avatars app.Get("/images/avatars/small/:file", func(ctx *aero.Context) string { - return ctx.File("images/avatars/large/" + ctx.Get("file")) + return ctx.File("images/avatars/small/" + ctx.Get("file")) }) // Elements From da5c900e3dc082e49a8403f68d342b9f936bcf88 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 2 Nov 2017 11:07:33 +0100 Subject: [PATCH 509/527] Disabled statistics --- mixins/Sidebar.pixy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index 237f062b..ba011078 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -25,7 +25,10 @@ component Sidebar(user *arn.User) SidebarButton("Groups", "/groups", "users") SidebarButton("Shop", "/shop", "shopping-cart") - SidebarButton("Statistics", "/statistics", "pie-chart") + + if user.Role == "admin" || user.Role == "editor" + SidebarButton("Statistics", "/statistics", "pie-chart") + SidebarButton("Settings", "/settings", "cog") .spacer From 3f7d297a5f26628d1fa5814bf0b22ce94ca4f4f9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 2 Nov 2017 22:01:11 +0100 Subject: [PATCH 510/527] Fixed image errors --- pages/profile/profile.pixy | 2 +- utils/EmptyImage.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 94b0f040..ef2967ed 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -1,6 +1,6 @@ component ProfileHeader(viewUser *arn.User, user *arn.User, uri string) .profile - img.profile-cover(src=viewUser.CoverImageURL(), alt="Cover image") + img.profile-cover.lazy(data-src=viewUser.CoverImageURL(), data-webp="true", alt="Cover image") .profile-image-container.mountable.never-unmount ProfileImage(viewUser) diff --git a/utils/EmptyImage.go b/utils/EmptyImage.go index f7c3edf7..d35ff481 100644 --- a/utils/EmptyImage.go +++ b/utils/EmptyImage.go @@ -2,5 +2,6 @@ package utils // EmptyImage returns the smallest possible 1x1 pixel image encoded in Base64. func EmptyImage() string { - return "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + return "" + // return "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" } From 6ec1cd569290aa6f2a111028e8a11327d96e477d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 2 Nov 2017 22:17:11 +0100 Subject: [PATCH 511/527] Updated twist job --- jobs/twist/twist.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index b86631cf..c0c1bf11 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -18,10 +18,10 @@ func main() { // Replace this with ID list from twist.moe later twistAnime, err := twist.GetAnimeIndex() arn.PanicOnError(err) - idList := twistAnime.KitsuIDs() + idList := arn.IDList(twistAnime.KitsuIDs()) // Save index in cache - arn.DB.Set("IDList", "animetwist index", idList) + arn.DB.Set("IDList", "animetwist index", &idList) color.Yellow("Refreshing twist.moe links for %d anime", len(idList)) From 2b51baf236f0d51e1a9ec185dc1fbab6f2675226 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 3 Nov 2017 09:34:21 +0100 Subject: [PATCH 512/527] Started working on dark theme --- config.json | 1 + layout/layout.pixy | 12 ++-- main.go | 1 - mixins/Sidebar.pixy | 85 ++++++++++++++------------- pages/admin/admin.go | 48 +++++++-------- pages/admin/admin.pixy | 40 ++++++------- pages/anime/anime.scarlet | 2 +- pages/soundtracks/soundtracks.scarlet | 3 +- pages/users/users.go | 19 ++---- pages/users/users.pixy | 1 - styles/base.scarlet | 1 + styles/include/config.scarlet | 5 +- styles/include/dark.scarlet | 14 +++++ styles/include/mixins.scarlet | 5 ++ styles/input.scarlet | 4 +- styles/navigation.scarlet | 1 + styles/sidebar.scarlet | 8 ++- styles/table.scarlet | 2 +- styles/widgets.scarlet | 38 +++++++----- 19 files changed, 155 insertions(+), 135 deletions(-) create mode 100644 styles/include/dark.scarlet diff --git a/config.json b/config.json index c3bbc092..8e3794a9 100644 --- a/config.json +++ b/config.json @@ -6,6 +6,7 @@ ], "styles": [ "include/config", + "include/dark", "include/mixins", "reset", "base", diff --git a/layout/layout.pixy b/layout/layout.pixy index b8241502..d63fa6d9 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -23,12 +23,14 @@ component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, openG //- #header //- Navigation(user) #columns - aside#sidebar - Sidebar(user) - #content-container - main#content.fade!= content + Sidebar(user) + Content(content) LoadingAnimation StatusMessage if user != nil #user(data-id=user.ID) - script(src="/scripts") \ No newline at end of file + script(src="/scripts") + +component Content(content string) + #content-container + main#content.fade!= content \ No newline at end of file diff --git a/main.go b/main.go index 3bcc3bef..32eb06f3 100644 --- a/main.go +++ b/main.go @@ -100,7 +100,6 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/users", users.Active) app.Ajax("/users/osu", users.Osu) app.Ajax("/users/staff", users.Staff) - app.Ajax("/users/anime/watching", users.AnimeWatching) app.Ajax("/statistics", statistics.Get) app.Ajax("/statistics/anime", statistics.Anime) app.Ajax("/login", login.Get) diff --git a/mixins/Sidebar.pixy b/mixins/Sidebar.pixy index ba011078..f3a45da5 100644 --- a/mixins/Sidebar.pixy +++ b/mixins/Sidebar.pixy @@ -1,56 +1,57 @@ component Sidebar(user *arn.User) - .user-image-container + aside#sidebar + .user-image-container + if user != nil + Avatar(user) + else + img.user-image.lazy(src=utils.EmptyImage(), data-src="/images/brand/64.png", data-webp="true", alt="Anime Notifier") + if user != nil - Avatar(user) + SidebarButton("Home", "/animelist/watching", "home") + SidebarButton("Dash", "/dashboard", "tachometer") else - img.user-image.lazy(src=utils.EmptyImage(), data-src="/images/brand/64.png", data-webp="true", alt="Anime Notifier") - - if user != nil - SidebarButton("Home", "/animelist/watching", "home") - SidebarButton("Dash", "/dashboard", "tachometer") - else - SidebarButton("Home", "/", "home") - - SidebarButton("Forum", "/forum", "comment") - SidebarButton("Explore", "/explore", "th") - //- SidebarButton("Artworks", "/artworks", "paint-brush") - SidebarButton("Soundtracks", "/soundtracks", "headphones") - //- SidebarButton("AMVs", "/amvs", "video-camera") - //- SidebarButton("Games", "/games", "gamepad") - SidebarButton("Users", "/users", "globe") - //- SidebarButton("Search", "/search", "search") - - if user != nil - if user.Role == "admin" - SidebarButton("Groups", "/groups", "users") + SidebarButton("Home", "/", "home") - SidebarButton("Shop", "/shop", "shopping-cart") + SidebarButton("Forum", "/forum", "comment") + SidebarButton("Explore", "/explore", "th") + //- SidebarButton("Artworks", "/artworks", "paint-brush") + SidebarButton("Soundtracks", "/soundtracks", "headphones") + //- SidebarButton("AMVs", "/amvs", "video-camera") + //- SidebarButton("Games", "/games", "gamepad") + SidebarButton("Users", "/users", "globe") + //- SidebarButton("Search", "/search", "search") - if user.Role == "admin" || user.Role == "editor" - SidebarButton("Statistics", "/statistics", "pie-chart") + if user != nil + if user.Role == "admin" + SidebarButton("Groups", "/groups", "users") + + SidebarButton("Shop", "/shop", "shopping-cart") - SidebarButton("Settings", "/settings", "cog") + if user.Role == "admin" || user.Role == "editor" + SidebarButton("Statistics", "/statistics", "pie-chart") - .spacer + SidebarButton("Settings", "/settings", "cog") - .sidebar-link(aria-label="Search") - .sidebar-button - Icon("search") - FuzzySearch + .spacer - if user != nil - if user.Role == "admin" - SidebarButton("Admin", "/admin", "wrench") - - if user.Role == "editor" - SidebarButton("Editor", "/editor", "pencil") + .sidebar-link(aria-label="Search") + .sidebar-button + Icon("search") + FuzzySearch - SidebarButton("Help", "/thread/I3MMiOtzR", "question-circle") + if user != nil + if user.Role == "admin" + SidebarButton("Admin", "/admin", "wrench") + + if user.Role == "editor" + SidebarButton("Editor", "/editor", "pencil") - if user != nil - SidebarButtonNoAJAX("Logout", "/logout", "sign-out") - else - SidebarButton("Login", "/login", "sign-in") + SidebarButton("Help", "/thread/I3MMiOtzR", "question-circle") + + if user != nil + SidebarButtonNoAJAX("Logout", "/logout", "sign-out") + else + SidebarButton("Login", "/login", "sign-in") component SidebarButton(name string, target string, icon string) a.sidebar-link.ajax(href=target, aria-label=name, data-bubble="true") diff --git a/pages/admin/admin.go b/pages/admin/admin.go index cf674966..122bbd44 100644 --- a/pages/admin/admin.go +++ b/pages/admin/admin.go @@ -1,16 +1,10 @@ package admin import ( - "time" - "github.com/aerogo/aero" - "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" - "github.com/shirou/gopsutil/cpu" - "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/host" - "github.com/shirou/gopsutil/mem" ) // Get admin page. @@ -21,40 +15,40 @@ func Get(ctx *aero.Context) string { return ctx.Redirect("/") } - // CPU - cpuUsage := 0.0 - cpuUsages, err := cpu.Percent(1*time.Second, false) + // // CPU + // cpuUsage := 0.0 + // cpuUsages, err := cpu.Percent(1*time.Second, false) - if err == nil { - cpuUsage = cpuUsages[0] - } + // if err == nil { + // cpuUsage = cpuUsages[0] + // } - // Memory - memUsage := 0.0 - memInfo, _ := mem.VirtualMemory() + // // Memory + // memUsage := 0.0 + // memInfo, _ := mem.VirtualMemory() - if err == nil { - memUsage = memInfo.UsedPercent - } + // if err == nil { + // memUsage = memInfo.UsedPercent + // } - // Disk - diskUsage := 0.0 - diskInfo, err := disk.Usage("/") + // // Disk + // diskUsage := 0.0 + // diskInfo, err := disk.Usage("/") - if err == nil { - diskUsage = diskInfo.UsedPercent - } + // if err == nil { + // diskUsage = diskInfo.UsedPercent + // } // Host platform, family, platformVersion, _ := host.PlatformInformation() - kernelVersion, err := host.KernelVersion() + kernelVersion, _ := host.KernelVersion() - return ctx.HTML(components.Admin(user, cpuUsage, memUsage, diskUsage, platform, family, platformVersion, kernelVersion)) + return ctx.HTML(components.Admin(user, platform, family, platformVersion, kernelVersion)) } func average(floatSlice []float64) float64 { if len(floatSlice) == 0 { - return arn.DefaultAverageRating + return 0 } var sum float64 diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index aa6cd7f1..f12146f5 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -8,32 +8,32 @@ component AdminTabs Icon("pencil") span.tab-text Editor -component Admin(user *arn.User, cpuUsage, memUsage, diskUsage float64, platform, family, platformVersion, kernelVersion string) +component Admin(user *arn.User, platform, family, platformVersion, kernelVersion string) h1.page-title Admin Panel AdminTabs .widgets - .widget.mountable - h3.widget-title Usage + //- .widget.mountable + //- h3.widget-title Usage - table - tbody - tr - td CPU usage: - td - span= int(cpuUsage + 0.5) - span % - tr - td Memory usage: - td - span= int(memUsage + 0.5) - span % - tr - td Disk usage: - td - span= int(diskUsage + 0.5) - span % + //- table + //- tbody + //- tr + //- td CPU usage: + //- td + //- span= int(cpuUsage + 0.5) + //- span % + //- tr + //- td Memory usage: + //- td + //- span= int(memUsage + 0.5) + //- span % + //- tr + //- td Disk usage: + //- td + //- span= int(diskUsage + 0.5) + //- span % .widget.mountable h3.widget-title OS diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 3de2745c..85dc661b 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -51,7 +51,7 @@ line-height content-line-height .japanese - color rgba(60, 60, 60, 0.5) !important + color rgba(255, 255, 255, 0.5) !important .anime-actions horizontal diff --git a/pages/soundtracks/soundtracks.scarlet b/pages/soundtracks/soundtracks.scarlet index f4a06f33..e9cab0eb 100644 --- a/pages/soundtracks/soundtracks.scarlet +++ b/pages/soundtracks/soundtracks.scarlet @@ -17,8 +17,9 @@ box-shadow shadow-light .sound-track-footer - text-align right + text-align center margin-bottom 1rem + margin-top 0.4rem font-size 0.9em span diff --git a/pages/users/users.go b/pages/users/users.go index 7004e18e..cdc3d706 100644 --- a/pages/users/users.go +++ b/pages/users/users.go @@ -14,7 +14,11 @@ func Active(ctx *aero.Context) string { return user.IsActive() && user.HasAvatar() }) - arn.SortUsersLastSeen(users) + sort.Slice(users, func(i, j int) bool { + return len(users[i].AnimeList().Watching().Items) > len(users[j].AnimeList().Watching().Items) + }) + + // arn.SortUsersLastSeen(users) return ctx.HTML(components.Users(users)) } @@ -57,16 +61,3 @@ func Staff(ctx *aero.Context) string { return ctx.HTML(components.Users(users)) } - -// AnimeWatching ... -func AnimeWatching(ctx *aero.Context) string { - users := arn.FilterUsers(func(user *arn.User) bool { - return user.IsActive() && user.HasAvatar() - }) - - sort.Slice(users, func(i, j int) bool { - return len(users[i].AnimeList().Watching().Items) > len(users[j].AnimeList().Watching().Items) - }) - - return ctx.HTML(components.Users(users)) -} diff --git a/pages/users/users.pixy b/pages/users/users.pixy index af6f65d8..4a12d8f5 100644 --- a/pages/users/users.pixy +++ b/pages/users/users.pixy @@ -11,6 +11,5 @@ component Users(users []*arn.User) component UsersTabs .tabs Tab("Active", "users", "/users") - Tab("Watching", "tv", "/users/anime/watching") Tab("Osu", "gamepad", "/users/osu") Tab("Staff", "user-secret", "/users/staff") \ No newline at end of file diff --git a/styles/base.scarlet b/styles/base.scarlet index 0840795d..8326f56d 100644 --- a/styles/base.scarlet +++ b/styles/base.scarlet @@ -18,6 +18,7 @@ a :hover color link-hover-color + text-shadow link-hover-text-shadow text-decoration none :active diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 6750fb15..d24b7563 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -1,10 +1,10 @@ // Colors text-color = rgb(60, 60, 60) +bg-color = rgb(246, 246, 246) main-color = rgb(248, 165, 130) link-color = rgb(215, 38, 15) link-hover-color = rgb(242, 60, 30) link-active-color = link-hover-color -bg-color = rgb(246, 246, 246) pro-color = hsla(0, 100%, 73%, 0.87) // UI @@ -23,7 +23,8 @@ ui-element-border-radius = 3px input-focus-border-color = rgb(248, 165, 130) // Button -button-hover-color = link-hover-color +button-hover-color = white +button-hover-background = link-hover-color forum-tag-hover-color = #225db5 // forum-tag-hover-color = rgb(46, 85, 160) diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet new file mode 100644 index 00000000..bae6f9cc --- /dev/null +++ b/styles/include/dark.scarlet @@ -0,0 +1,14 @@ +// Dark theme +text-color = hsl(0, 0%, 90%) +bg-color = hsl(0, 0%, 24%) +link-color = hsl(81, 100%, 56%) +link-hover-color = hsl(81, 100%, 66%) +ui-background = hsl(0, 0%, 18%) + +link-hover-text-shadow = 0 0 8px hsla(81, 100%, 56%, 0.5) + +main-color = link-color +link-active-color = link-hover-color +button-hover-color = bg-color +button-hover-background = link-hover-color +loading-anim-color = link-color \ No newline at end of file diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 19d133db..8bde641d 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -60,6 +60,11 @@ mixin bg-dark-up :hover background-color rgba(0, 0, 0, 0.015) +mixin bg-light-up + background-color transparent + :hover + background-color rgba(255, 255, 255, 0.015) + mixin light-up filter brightness(0.4) saturate(1) :hover diff --git a/styles/input.scarlet b/styles/input.scarlet index f4e2beed..c3bcff50 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -35,8 +35,8 @@ button, .button :hover, &.active cursor pointer - color white - background-color button-hover-color + color button-hover-color + background button-hover-background :active transform translateY(3px) diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index b8084275..4bf731d1 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -44,6 +44,7 @@ #search background transparent border none + box-shadow none font-size 1em padding 0 width 0 diff --git a/styles/sidebar.scarlet b/styles/sidebar.scarlet index 3671d382..5887abf2 100644 --- a/styles/sidebar.scarlet +++ b/styles/sidebar.scarlet @@ -41,13 +41,17 @@ sidebar-spacing-y = 0.7rem .sidebar-link color text-color + &.active .sidebar-button - background forum-tag-hover-color - color white + // background forum-tag-hover-color + // color white + color link-color + text-shadow link-hover-text-shadow .sidebar-button horizontal + default-transition align-items center padding sidebar-spacing-y 1rem // background ui-background diff --git a/styles/table.scarlet b/styles/table.scarlet index 713df6bc..66593695 100644 --- a/styles/table.scarlet +++ b/styles/table.scarlet @@ -21,4 +21,4 @@ th tbody tr - bg-dark-up \ No newline at end of file + bg-light-up \ No newline at end of file diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index d3fe8f3f..0b5bcaba 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -1,23 +1,29 @@ +// .widgets +// display grid +// grid-template-columns 1fr + +// > 810px +// .widgets +// grid-template-columns repeat(2, 1fr) +// grid-gap content-padding + +// > 1240px +// .widgets +// grid-template-columns repeat(3, 1fr) + +// > 1640px +// .widgets +// grid-template-columns repeat(4, 1fr) + .widgets - display grid - grid-template-columns 1fr - -> 810px - .widgets - grid-template-columns repeat(2, 1fr) - grid-gap content-padding - -> 1240px - .widgets - grid-template-columns repeat(3, 1fr) - -> 1640px - .widgets - grid-template-columns repeat(4, 1fr) + horizontal-wrap + justify-content center .widget vertical - margin-bottom 1rem + width 100% + max-width 300px + margin calc(content-padding / 2) overflow hidden .widget-section From a459f2aa9a6828704bced0c4c3d7fe0b762e0740 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 3 Nov 2017 12:02:13 +0100 Subject: [PATCH 513/527] Redesign anime pages --- pages/admin/admin.pixy | 2 +- pages/admin/webdev.pixy | 2 +- pages/anime/anime.pixy | 267 +++++++---------------- pages/anime/anime.scarlet | 3 +- pages/dashboard/dashboard.pixy | 2 +- pages/profile/stats.pixy | 2 +- pages/search/search.pixy | 2 +- pages/settings/settings.pixy | 2 +- pages/shop/shop.pixy | 2 +- pages/statistics/statistics.pixy | 9 +- pages/statistics/statistics.scarlet | 1 + patches/export-aero-db/export-aero-db.go | 4 +- styles/include/config.scarlet | 4 +- styles/include/dark.scarlet | 10 +- styles/include/mixins.scarlet | 8 + styles/sidebar.scarlet | 2 +- styles/status-message.scarlet | 2 +- styles/tabs.scarlet | 4 +- 18 files changed, 118 insertions(+), 210 deletions(-) diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index f12146f5..d8fcb7c3 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -13,7 +13,7 @@ component Admin(user *arn.User, platform, family, platformVersion, kernelVersion AdminTabs - .widgets + .admin //- .widget.mountable //- h3.widget-title Usage diff --git a/pages/admin/webdev.pixy b/pages/admin/webdev.pixy index 0be46d3a..f7795b0a 100644 --- a/pages/admin/webdev.pixy +++ b/pages/admin/webdev.pixy @@ -3,7 +3,7 @@ component WebDev h1.page-title WebDev - .widgets + .webdev .widget.mountable h3.widget-title Tests diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 9efa25f4..3946fc4f 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,5 +1,5 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) - AnimeTabs(anime) + //- AnimeTabs(anime) .anime-header(data-id=anime.ID) if anime.Image.Small != "" @@ -18,9 +18,6 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* .anime-info h1.anime-title(title=anime.Type)= anime.Title.ByUser(user) - //- if user && user.titleLanguage === "japanese" - //- span.second-title(title=anime.Title.English !== anime.Title.Romaji ? anime.Title.English : null)= anime.Title.Romaji - //- else if anime.Title.Japanese != anime.Title.Canonical h2.anime-alternative-title Japanese(anime.Title.Japanese) @@ -44,198 +41,94 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]* Icon("plus") span Add to collection - h3.anime-section-name Ratings - .anime-rating-categories - .anime-rating-category(title=toString(anime.Rating.Overall)) - if anime.Status == "upcoming" - .anime-rating-category-name Hype - else - .anime-rating-category-name Overall - Rating(anime.Rating.Overall, user) - .anime-rating-category(title=toString(anime.Rating.Story)) - .anime-rating-category-name Story - Rating(anime.Rating.Story, user) - .anime-rating-category(title=toString(anime.Rating.Visuals)) - .anime-rating-category-name Visuals - Rating(anime.Rating.Visuals, user) - .anime-rating-category(title=toString(anime.Rating.Soundtrack)) - .anime-rating-category-name Soundtrack - Rating(anime.Rating.Soundtrack, user) + //- h3.anime-section-name Ratings + //- .anime-rating-categories + //- .anime-rating-category(title=toString(anime.Rating.Overall)) + //- if anime.Status == "upcoming" + //- .anime-rating-category-name Hype + //- else + //- .anime-rating-category-name Overall + //- Rating(anime.Rating.Overall, user) + //- .anime-rating-category(title=toString(anime.Rating.Story)) + //- .anime-rating-category-name Story + //- Rating(anime.Rating.Story, user) + //- .anime-rating-category(title=toString(anime.Rating.Visuals)) + //- .anime-rating-category-name Visuals + //- Rating(anime.Rating.Visuals, user) + //- .anime-rating-category(title=toString(anime.Rating.Soundtrack)) + //- .anime-rating-category-name Soundtrack + //- Rating(anime.Rating.Soundtrack, user) - if len(friends) > 0 - h3.anime-section-name Friends + //- if len(friends) > 0 + //- h3.anime-section-name Friends - .anime-friends - .user-avatars - each friend in friends - if friend.Nick != "" - if friend.IsActive() - .mountable - FriendEntry(friend, listItems) - else - .mountable - .inactive-user - FriendEntry(friend, listItems) + //- .anime-friends + //- .user-avatars + //- each friend in friends + //- if friend.Nick != "" + //- if friend.IsActive() + //- .mountable + //- FriendEntry(friend, listItems) + //- else + //- .mountable + //- .inactive-user + //- FriendEntry(friend, listItems) - if anime.Relations() != nil && len(anime.Relations().Items) > 0 - h3.anime-section-name Relations - .anime-relations - each relation in anime.Relations().Items - a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) - img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) - .anime-relation-type= relation.HumanReadableType() - .anime-relation-year - if relation.Anime().StartDate != "" - span= relation.Anime().StartDate[:4] - - if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" - h3.anime-section-name Video - .anime-trailer.video-container - iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") - - //- if anime.Tracks != nil && anime.Tracks.Opening != nil - //- h3.anime-section-name Tracks - //- iframe.anime-track(src="https://w.soundcloud.com/player/?url=" + anime.Tracks.Opening.URI + "?auto_play=false&hide_related=true&show_comments=true&show_user=true&show_reposts=false&visual=true") - - //- if user && friendsWatching && friendsWatching.length > 0 - //- include ../messages/avatar.pug - - //- h3.anime-section-name Watching - //- .user-list - //- each watcher in friendsWatching - //- +avatar(watcher) - - //- if len(anime.Relations) > 0 + //- if anime.Relations() != nil && len(anime.Relations().Items) > 0 //- h3.anime-section-name Relations - //- .relations - //- each relation in anime.Relations - //- a.relation.ajax(href="/anime/" + toString(relation.ID), title=relation.Anime().Title.Romaji) - //- img.anime-image.relation-image(src=relation.Anime().Image, alt=relation.Anime().Title.Romaji) - //- .relation-type= arn.Capitalize(relation.Type) + //- .anime-relations + //- each relation in anime.Relations().Items + //- a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) + //- img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) + //- .anime-relation-type= relation.HumanReadableType() + //- .anime-relation-year + //- if relation.Anime().StartDate != "" + //- span= relation.Anime().StartDate[:4] - //- if len(anime.Genres) > 0 - //- h3.anime-section-name Genres - //- .light-button-group - //- each genre in anime.Genres - //- if genre != "" - //- a.light-button.ajax(href="/genres/" + arn.GetGenreIDByName(genre)) - //- Icon(arn.GetGenreIcon(genre)) - //- span= genre + //- if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" + //- h3.anime-section-name Video + //- .anime-trailer.video-container + //- iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") + + //- h3.anime-section-name Popularity + //- .anime-rating-categories + //- .anime-rating-category + //- .anime-rating-category-name Watching + //- .anime-rating= anime.Popularity.Watching + //- .anime-rating-category + //- .anime-rating-category-name Completed + //- .anime-rating= anime.Popularity.Completed + //- .anime-rating-category + //- .anime-rating-category-name Planned + //- .anime-rating= anime.Popularity.Planned + //- .anime-rating-category + //- .anime-rating-category-name Hold + //- .anime-rating= anime.Popularity.Hold + //- .anime-rating-category + //- .anime-rating-category-name Dropped + //- .anime-rating= anime.Popularity.Dropped - //- if len(anime.Studios) > 0 - //- h3.anime-section-name Studios - //- .light-button-group - //- each studio in anime.Studios - //- a.light-button(href="https://anilist.co/studio/" + toString(studio.ID), target="_blank") - //- Icon("building") - //- span= studio.Name + //- //- h3.anime-section-name Reviews + //- //- p Coming soon. - //- //-if crunchy - //- //- h3.anime-section-name Episodes - - //- if canEdit - //- #staff-info - //- h3.anime-section-name Links - //- table - //- tbody - //- tr - //- td MyAnimeList - //- td - //- input.save-on-change(id="MyAnimeList", type="text", value=providers.MyAnimeList ? providers.MyAnimeList.providerId : ", disabled=(providers.MyAnimeList && providers.MyAnimeList.similarity === 1) ? true : false) - //- td - //- a(href="https://www.google.co.jp/search?q=site:myanimelist.net/anime+" + anime.title.romaji.replace(/ /g, "+"), target="_blank") - //- .fa.fa-search - //- td - //- tr - //- td HummingBird - //- td - //- input.save-on-change(id="HummingBird", type="text", value=providers.HummingBird ? providers.HummingBird.providerId : ", disabled=(providers.HummingBird && providers.HummingBird.similarity === 1) ? true : false) - //- td - //- a(href="https://www.google.co.jp/search?q=site:hummingbird.me/anime+" + anime.title.romaji.replace(/ /g, "+"), target="_blank") - //- .fa.fa-search - //- td - //- tr - //- td AnimePlanet - //- td - //- input.save-on-change(id="AnimePlanet", type="text", value=providers.AnimePlanet ? providers.AnimePlanet.providerId : ", disabled=(providers.AnimePlanet && providers.AnimePlanet.similarity === 1) ? true : false) - //- td - //- a(href="https://www.google.co.jp/search?q=site:anime-planet.com/anime+" + anime.title.english.replace(/ /g, "+"), target="_blank") - //- .fa.fa-search - //- td - - //- - var title = providers.Nyaa ? providers.Nyaa.title : " - //- - var proposedTitle = nyaa.buildNyaaTitle(anime.title.romaji) - //- tr - //- td Nyaa - //- td - //- input.save-on-change(id="Nyaa", type="text", value=title, placeholder=proposedTitle) - //- td - //- a(href="https://www.nyaa.se/?page=search&cats=1_37&filter=0&sort=2&term=" + (title ? title.replace(/ /g, "+") : proposedTitle), target="_blank") - //- .fa.fa-search - //- td - //- if providers.Nyaa && providers.Nyaa.episodes !== undefined - //- span(class=providers.Nyaa.episodes === 0 ? "entry-error" : "entry-ok")= providers.Nyaa.episodes + " eps" - - h3.anime-section-name Popularity - .anime-rating-categories - .anime-rating-category - .anime-rating-category-name Watching - .anime-rating= anime.Popularity.Watching - .anime-rating-category - .anime-rating-category-name Completed - .anime-rating= anime.Popularity.Completed - .anime-rating-category - .anime-rating-category-name Planned - .anime-rating= anime.Popularity.Planned - .anime-rating-category - .anime-rating-category-name Hold - .anime-rating= anime.Popularity.Hold - .anime-rating-category - .anime-rating-category-name Dropped - .anime-rating= anime.Popularity.Dropped - - //- h3.anime-section-name Reviews - //- p Coming soon. - - h3.anime-section-name Links - .light-button-group - //- if anime.Links != nil - //- each link in anime.Links - //- a.light-button(href=link.URL, target="_blank") - //- Icon("external-link") - //- span= link.Title - a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") - Icon("external-link") - span Kitsu + //- h3.anime-section-name Links + //- .light-button-group + //- //- if anime.Links != nil + //- //- each link in anime.Links + //- //- a.light-button(href=link.URL, target="_blank") + //- //- Icon("external-link") + //- //- span= link.Title + //- a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") + //- Icon("external-link") + //- span Kitsu - each mapping in anime.Mappings - a.light-button(href=mapping.Link(), target="_blank", rel="noopener") - Icon("external-link") - span= mapping.Name() + //- each mapping in anime.Mappings + //- a.light-button(href=mapping.Link(), target="_blank", rel="noopener") + //- Icon("external-link") + //- span= mapping.Name() - //- if providers.HummingBird - //- a.light-button(href="https://hummingbird.me/anime/" + providers.HummingBird.providerId, target="_blank") HummingBird - - //- if providers.MyAnimeList - //- a.light-button(href="http://myanimelist.net/anime/" + providers.MyAnimeList.providerId, target="_blank") MyAnimeList - - //- if providers.AnimePlanet - //- a.light-button(href="http://www.anime-planet.com/anime/" + providers.AnimePlanet.providerId, target="_blank") AnimePlanet - - .footer - //- if user != nil && user.Role == "admin" - //- a(href="/api/anime/" + anime.ID) Anime API - //- span | - span Powered by Kitsu. - //- if descriptionSource - //- span= " Summary by " + summarySource + "." - //- //- - //- h3.anime-section-name Synonyms - //- if anime.title.synonyms - //- ul.anime-synonyms - //- li.anime-japanese-title= anime.title.japanese - //- each synonym in anime.title.synonyms - //- li= synonym + //- .footer + //- span Powered by Kitsu. component FriendEntry(friend *arn.User, listItems map[*arn.User]*arn.AnimeListItem) CustomAvatar(friend, listItems[friend].Link(friend.Nick), friend.Nick + " => " + listItems[friend].Status + " | " + toString(listItems[friend].Episodes) + " eps | " + fmt.Sprintf("%.1f", listItems[friend].Rating.Overall) + " rating") diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 85dc661b..7e2f260f 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -20,7 +20,8 @@ margin-top 0.5rem .anime-cover-image - width 142px + // width 142px + width 180px height auto border-radius 3px diff --git a/pages/dashboard/dashboard.pixy b/pages/dashboard/dashboard.pixy index de34ab12..a40b6bd8 100644 --- a/pages/dashboard/dashboard.pixy +++ b/pages/dashboard/dashboard.pixy @@ -1,7 +1,7 @@ component Dashboard(schedule []*arn.UpcomingEpisode, posts []arn.Postable, soundTracks []*arn.SoundTrack, following []*arn.User, user *arn.User) h1.page-title Dashboard - .widgets + .dashboard .widget.mountable h3.widget-title Schedule diff --git a/pages/profile/stats.pixy b/pages/profile/stats.pixy index 82e1366f..8954e756 100644 --- a/pages/profile/stats.pixy +++ b/pages/profile/stats.pixy @@ -1,7 +1,7 @@ component ProfileStats(stats *utils.UserStats, viewUser *arn.User, user *arn.User, uri string) ProfileHeader(viewUser, user, uri) - .widgets + .stats each pie in stats.PieCharts .widget.mountable h3.widget-title diff --git a/pages/search/search.pixy b/pages/search/search.pixy index ca32fef6..caae73aa 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -1,7 +1,7 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anime, postResults []*arn.Post, threadResults []*arn.Thread) h1.page-title= "Search: " + term - .widgets + .search .widget h3.widget-title Icon("user") diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 5521671e..a42c422c 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -1,7 +1,7 @@ component Settings(user *arn.User) h1.page-title Settings - .widgets + .settings .widget.mountable(data-api="/api/user/" + user.ID) h3.widget-title Icon("user") diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy index 750616a4..db8edda8 100644 --- a/pages/shop/shop.pixy +++ b/pages/shop/shop.pixy @@ -3,7 +3,7 @@ component Shop(user *arn.User, items []*arn.Item) h1.page-title Shop - .widgets.shop-items + .shop-items each item in items ShopItem(item) diff --git a/pages/statistics/statistics.pixy b/pages/statistics/statistics.pixy index 7abdc72f..e4697f06 100644 --- a/pages/statistics/statistics.pixy +++ b/pages/statistics/statistics.pixy @@ -4,11 +4,10 @@ component Statistics(pieCharts []*arn.PieChart) StatisticsHeader .statistics - .widgets - each pie in pieCharts - .widget - h3.widget-title= pie.Title - PieChart(pie.Slices) + each pie in pieCharts + .widget + h3.widget-title= pie.Title + PieChart(pie.Slices) .footer p Data is collected for statistical purposes only. We respect user privacy and we will never display or sell critical data to 3rd party services. diff --git a/pages/statistics/statistics.scarlet b/pages/statistics/statistics.scarlet index f432ec2f..9416327d 100644 --- a/pages/statistics/statistics.scarlet +++ b/pages/statistics/statistics.scarlet @@ -1,4 +1,5 @@ .statistics + horizontal-wrap-center text-align center .pie-chart diff --git a/patches/export-aero-db/export-aero-db.go b/patches/export-aero-db/export-aero-db.go index 553e3c23..cb464a0d 100644 --- a/patches/export-aero-db/export-aero-db.go +++ b/patches/export-aero-db/export-aero-db.go @@ -1,4 +1,6 @@ -// package main +package main + +func main() {} // import ( // "time" diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index d24b7563..b3a3bf96 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -25,8 +25,8 @@ input-focus-border-color = rgb(248, 165, 130) // Button button-hover-color = white button-hover-background = link-hover-color -forum-tag-hover-color = #225db5 -// forum-tag-hover-color = rgb(46, 85, 160) +tab-hover-background = #225db5 +// tab-hover-background = rgb(46, 85, 160) // Forum forum-width = 830px diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index bae6f9cc..f12ae59e 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -3,12 +3,16 @@ text-color = hsl(0, 0%, 90%) bg-color = hsl(0, 0%, 24%) link-color = hsl(81, 100%, 56%) link-hover-color = hsl(81, 100%, 66%) -ui-background = hsl(0, 0%, 18%) +ui-background = hsla(0, 0%, 18%, 0.5) +tab-hover-background = link-hover-color + +theme-white = bg-color +theme-black = text-color link-hover-text-shadow = 0 0 8px hsla(81, 100%, 56%, 0.5) main-color = link-color link-active-color = link-hover-color -button-hover-color = bg-color -button-hover-background = link-hover-color +button-hover-color = link-hover-color +button-hover-background = hsla(0, 0%, 12%, 0.5) loading-anim-color = link-color \ No newline at end of file diff --git a/styles/include/mixins.scarlet b/styles/include/mixins.scarlet index 8bde641d..02dc0861 100644 --- a/styles/include/mixins.scarlet +++ b/styles/include/mixins.scarlet @@ -9,6 +9,10 @@ mixin horizontal-wrap display flex flex-flow row wrap +mixin horizontal-wrap-center + horizontal-wrap + justify-content center + mixin vertical display flex flex-direction column @@ -17,6 +21,10 @@ mixin vertical-wrap display flex flex-flow column wrap +mixin vertical-wrap-center + horizontal-wrap + align-items center + mixin noise-light background-image url("/images/elements/noise-light.png") diff --git a/styles/sidebar.scarlet b/styles/sidebar.scarlet index 5887abf2..cbd0dcf1 100644 --- a/styles/sidebar.scarlet +++ b/styles/sidebar.scarlet @@ -44,7 +44,7 @@ sidebar-spacing-y = 0.7rem &.active .sidebar-button - // background forum-tag-hover-color + // background tab-hover-background // color white color link-color text-shadow link-hover-text-shadow diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet index d433119b..8a99505f 100644 --- a/styles/status-message.scarlet +++ b/styles/status-message.scarlet @@ -22,4 +22,4 @@ .info-message color white - background-color forum-tag-hover-color \ No newline at end of file + background tab-hover-background \ No newline at end of file diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index e6c78694..37793fc4 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -14,8 +14,8 @@ transform none &.active - background-color forum-tag-hover-color - color white + background tab-hover-background + color theme-white :first-child border-left ui-border From 8af94d4800462cfb7237b9aa1a1c9710f33e5a0a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 3 Nov 2017 18:10:31 +0100 Subject: [PATCH 514/527] Redesign --- .../sidebar/sidebar.pixy | 10 +- {styles => layout/sidebar}/sidebar.scarlet | 0 pages/anime/anime.pixy | 231 +++++++++--------- pages/anime/anime.scarlet | 55 +++-- pages/settings/settings.scarlet | 3 + scripts/Actions/Diff.ts | 4 +- scripts/AnimeNotifier.ts | 4 +- styles/headers.scarlet | 2 +- styles/include/config.scarlet | 10 +- styles/include/dark.scarlet | 4 +- 10 files changed, 172 insertions(+), 151 deletions(-) rename mixins/Sidebar.pixy => layout/sidebar/sidebar.pixy (87%) rename {styles => layout/sidebar}/sidebar.scarlet (100%) diff --git a/mixins/Sidebar.pixy b/layout/sidebar/sidebar.pixy similarity index 87% rename from mixins/Sidebar.pixy rename to layout/sidebar/sidebar.pixy index f3a45da5..df34916a 100644 --- a/mixins/Sidebar.pixy +++ b/layout/sidebar/sidebar.pixy @@ -8,7 +8,7 @@ component Sidebar(user *arn.User) if user != nil SidebarButton("Home", "/animelist/watching", "home") - SidebarButton("Dash", "/dashboard", "tachometer") + //- SidebarButton("Dash", "/dashboard", "tachometer") else SidebarButton("Home", "/", "home") @@ -22,13 +22,13 @@ component Sidebar(user *arn.User) //- SidebarButton("Search", "/search", "search") if user != nil - if user.Role == "admin" - SidebarButton("Groups", "/groups", "users") + //- if user.Role == "admin" + //- SidebarButton("Groups", "/groups", "users") SidebarButton("Shop", "/shop", "shopping-cart") - if user.Role == "admin" || user.Role == "editor" - SidebarButton("Statistics", "/statistics", "pie-chart") + //- if user.Role == "admin" || user.Role == "editor" + //- SidebarButton("Statistics", "/statistics", "pie-chart") SidebarButton("Settings", "/settings", "cog") diff --git a/styles/sidebar.scarlet b/layout/sidebar/sidebar.scarlet similarity index 100% rename from styles/sidebar.scarlet rename to layout/sidebar/sidebar.scarlet diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 3946fc4f..78a323fa 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,134 +1,133 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) //- AnimeTabs(anime) - - .anime-header(data-id=anime.ID) - if anime.Image.Small != "" - .anime-image-container - img.anime-cover-image(src=anime.Image.Small, alt=anime.Title.ByUser(user)) + .anime + .anime-header(data-id=anime.ID) + if anime.Image.Large != "" + .anime-image-container + img.anime-cover-image(src=anime.Image.Large, alt=anime.Title.ByUser(user)) - if anime.StartDate != "" - .anime-start-date - span(title="Start date: " + anime.StartDate)= anime.StartDate[:4] - if anime.EndDate != "" && anime.StartDate[:4] != anime.EndDate[:4] - span - - span(title="End date: " + anime.EndDate)= anime.EndDate[:4] - - .space + //- if anime.StartDate != "" + //- .anime-start-date + //- span(title="Start date: " + anime.StartDate)= anime.StartDate[:4] + //- if anime.EndDate != "" && anime.StartDate[:4] != anime.EndDate[:4] + //- span - + //- span(title="End date: " + anime.EndDate)= anime.EndDate[:4] + + .space - .anime-info - h1.anime-title(title=anime.Type)= anime.Title.ByUser(user) + .anime-info + h1.anime-title(title=anime.Type)= anime.Title.ByUser(user) - if anime.Title.Japanese != anime.Title.Canonical h2.anime-alternative-title Japanese(anime.Title.Japanese) - //- h3.anime-section-name.anime-summary-header Summary - p.anime-summary= anime.Summary - - if user != nil - .buttons.anime-actions - if user.Role == "editor" || user.Role == "admin" - a.button.ajax(href=anime.Link() + "/edit") - Icon("pencil-square-o") - span Edit anime - - if user.AnimeList().Contains(anime.ID) - a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) - Icon("pencil") - span Edit in collection - else - button.action(data-api="/api/animelist/" + user.ID, data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID) - Icon("plus") - span Add to collection - - //- h3.anime-section-name Ratings - //- .anime-rating-categories - //- .anime-rating-category(title=toString(anime.Rating.Overall)) - //- if anime.Status == "upcoming" - //- .anime-rating-category-name Hype - //- else - //- .anime-rating-category-name Overall - //- Rating(anime.Rating.Overall, user) - //- .anime-rating-category(title=toString(anime.Rating.Story)) - //- .anime-rating-category-name Story - //- Rating(anime.Rating.Story, user) - //- .anime-rating-category(title=toString(anime.Rating.Visuals)) - //- .anime-rating-category-name Visuals - //- Rating(anime.Rating.Visuals, user) - //- .anime-rating-category(title=toString(anime.Rating.Soundtrack)) - //- .anime-rating-category-name Soundtrack - //- Rating(anime.Rating.Soundtrack, user) - - //- if len(friends) > 0 - //- h3.anime-section-name Friends + //- h3.anime-section-name.anime-summary-header Summary + p.anime-summary= anime.Summary - //- .anime-friends - //- .user-avatars - //- each friend in friends - //- if friend.Nick != "" - //- if friend.IsActive() - //- .mountable - //- FriendEntry(friend, listItems) - //- else - //- .mountable - //- .inactive-user - //- FriendEntry(friend, listItems) + if user != nil + .buttons.anime-actions + if user.Role == "editor" || user.Role == "admin" + a.button.ajax(href=anime.Link() + "/edit") + Icon("pencil-square-o") + span Edit anime - //- if anime.Relations() != nil && len(anime.Relations().Items) > 0 - //- h3.anime-section-name Relations - //- .anime-relations - //- each relation in anime.Relations().Items - //- a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) - //- img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) - //- .anime-relation-type= relation.HumanReadableType() - //- .anime-relation-year - //- if relation.Anime().StartDate != "" - //- span= relation.Anime().StartDate[:4] + if user.AnimeList().Contains(anime.ID) + a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) + Icon("pencil") + span Edit in collection + else + button.action(data-api="/api/animelist/" + user.ID, data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID) + Icon("plus") + span Add to collection - //- if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" - //- h3.anime-section-name Video - //- .anime-trailer.video-container - //- iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") + //- h3.anime-section-name Ratings + //- .anime-rating-categories + //- .anime-rating-category(title=toString(anime.Rating.Overall)) + //- if anime.Status == "upcoming" + //- .anime-rating-category-name Hype + //- else + //- .anime-rating-category-name Overall + //- Rating(anime.Rating.Overall, user) + //- .anime-rating-category(title=toString(anime.Rating.Story)) + //- .anime-rating-category-name Story + //- Rating(anime.Rating.Story, user) + //- .anime-rating-category(title=toString(anime.Rating.Visuals)) + //- .anime-rating-category-name Visuals + //- Rating(anime.Rating.Visuals, user) + //- .anime-rating-category(title=toString(anime.Rating.Soundtrack)) + //- .anime-rating-category-name Soundtrack + //- Rating(anime.Rating.Soundtrack, user) - //- h3.anime-section-name Popularity - //- .anime-rating-categories - //- .anime-rating-category - //- .anime-rating-category-name Watching - //- .anime-rating= anime.Popularity.Watching - //- .anime-rating-category - //- .anime-rating-category-name Completed - //- .anime-rating= anime.Popularity.Completed - //- .anime-rating-category - //- .anime-rating-category-name Planned - //- .anime-rating= anime.Popularity.Planned - //- .anime-rating-category - //- .anime-rating-category-name Hold - //- .anime-rating= anime.Popularity.Hold - //- .anime-rating-category - //- .anime-rating-category-name Dropped - //- .anime-rating= anime.Popularity.Dropped - - //- //- h3.anime-section-name Reviews - //- //- p Coming soon. + //- if len(friends) > 0 + //- h3.anime-section-name Friends + + //- .anime-friends + //- .user-avatars + //- each friend in friends + //- if friend.Nick != "" + //- if friend.IsActive() + //- .mountable + //- FriendEntry(friend, listItems) + //- else + //- .mountable + //- .inactive-user + //- FriendEntry(friend, listItems) - //- h3.anime-section-name Links - //- .light-button-group - //- //- if anime.Links != nil - //- //- each link in anime.Links - //- //- a.light-button(href=link.URL, target="_blank") - //- //- Icon("external-link") - //- //- span= link.Title - //- a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") - //- Icon("external-link") - //- span Kitsu + //- if anime.Relations() != nil && len(anime.Relations().Items) > 0 + //- h3.anime-section-name Relations + //- .anime-relations + //- each relation in anime.Relations().Items + //- a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) + //- img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) + //- .anime-relation-type= relation.HumanReadableType() + //- .anime-relation-year + //- if relation.Anime().StartDate != "" + //- span= relation.Anime().StartDate[:4] + + //- if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" + //- h3.anime-section-name Video + //- .anime-trailer.video-container + //- iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") + + //- h3.anime-section-name Popularity + //- .anime-rating-categories + //- .anime-rating-category + //- .anime-rating-category-name Watching + //- .anime-rating= anime.Popularity.Watching + //- .anime-rating-category + //- .anime-rating-category-name Completed + //- .anime-rating= anime.Popularity.Completed + //- .anime-rating-category + //- .anime-rating-category-name Planned + //- .anime-rating= anime.Popularity.Planned + //- .anime-rating-category + //- .anime-rating-category-name Hold + //- .anime-rating= anime.Popularity.Hold + //- .anime-rating-category + //- .anime-rating-category-name Dropped + //- .anime-rating= anime.Popularity.Dropped - //- each mapping in anime.Mappings - //- a.light-button(href=mapping.Link(), target="_blank", rel="noopener") - //- Icon("external-link") - //- span= mapping.Name() + //- //- h3.anime-section-name Reviews + //- //- p Coming soon. - //- .footer - //- span Powered by Kitsu. + //- h3.anime-section-name Links + //- .light-button-group + //- //- if anime.Links != nil + //- //- each link in anime.Links + //- //- a.light-button(href=link.URL, target="_blank") + //- //- Icon("external-link") + //- //- span= link.Title + //- a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") + //- Icon("external-link") + //- span Kitsu + + //- each mapping in anime.Mappings + //- a.light-button(href=mapping.Link(), target="_blank", rel="noopener") + //- Icon("external-link") + //- span= mapping.Name() + + //- .footer + //- span Powered by Kitsu. component FriendEntry(friend *arn.User, listItems map[*arn.User]*arn.AnimeListItem) CustomAvatar(friend, listItems[friend].Link(friend.Nick), friend.Nick + " => " + listItems[friend].Status + " | " + toString(listItems[friend].Episodes) + " eps | " + fmt.Sprintf("%.1f", listItems[friend].Rating.Overall) + " rating") diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 7e2f260f..b3f69819 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -1,9 +1,20 @@ -.anime-header - horizontal +.anime + max-width 1100px + margin 0 auto -< 800px +.anime-header + vertical + +.anime-title + text-align center + margin-bottom 0.5rem + +> 800px .anime-header - vertical + horizontal + + .anime-title + text-align left .anime-section-name font-weight bold @@ -21,7 +32,7 @@ .anime-cover-image // width 142px - width 180px + width 225px height auto border-radius 3px @@ -30,7 +41,10 @@ saturate-up shadow-up - + +.anime-summary + // ... + .anime-info vertical flex 1 @@ -39,10 +53,6 @@ width content-padding height content-padding -.anime-title - text-align left - margin-bottom 0.5rem - .anime-alternative-title font-size 0.9em margin-top 0 @@ -55,20 +65,23 @@ color rgba(255, 255, 255, 0.5) !important .anime-actions - horizontal - justify-content center + display none !important - // Action button margin - margin calc(content-padding - 0.5rem) -0.5rem +// .anime-actions +// horizontal +// justify-content center - // Setting z-index requires setting a background as well - z-index 10 +// // Action button margin +// margin calc(content-padding - 0.5rem) -0.5rem -> 1450px - .anime-actions - position absolute - top 0 - right content-padding +// // Setting z-index requires setting a background as well +// z-index 10 + +// > 1450px +// .anime-actions +// position absolute +// top 0 +// right content-padding .anime-rating-categories horizontal diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet index 5edda9ce..5425717f 100644 --- a/pages/settings/settings.scarlet +++ b/pages/settings/settings.scarlet @@ -1,3 +1,6 @@ +.settings + horizontal-wrap-center + .widget-section > button, .widget-section > .button margin-bottom 1rem diff --git a/scripts/Actions/Diff.ts b/scripts/Actions/Diff.ts index 66e35c81..b3213edc 100644 --- a/scripts/Actions/Diff.ts +++ b/scripts/Actions/Diff.ts @@ -10,5 +10,7 @@ export function load(arn: AnimeNotifier, element: HTMLElement) { export function diff(arn: AnimeNotifier, element: HTMLElement) { let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") - arn.diff(url).then(() => arn.scrollTo(element)) + arn.diff(url) + .then(() => arn.scrollTo(element)) + .catch(console.error) } \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 46c2bc33..f669dd98 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -509,7 +509,7 @@ export class AnimeNotifier { modifyDelayed(className: string, func: (element: HTMLElement) => void) { const maxDelay = 1000 - const delay = 20 + const delay = 18 let time = 0 let start = Date.now() @@ -596,7 +596,7 @@ export class AnimeNotifier { this.loading(true) // Delay by transition-speed - return delay(300).then(() => request) + return delay(200).then(() => request) .then(html => this.app.setContent(html, true)) .then(() => this.app.emit("DOMContentLoaded")) .then(() => this.loading(false)) diff --git a/styles/headers.scarlet b/styles/headers.scarlet index 5842d2cf..8dd648f6 100644 --- a/styles/headers.scarlet +++ b/styles/headers.scarlet @@ -2,7 +2,7 @@ h1, h2 font-size 2em font-weight bold text-align center - line-height 1.2em + line-height 1.3em h3 font-size 1.5em diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index b3a3bf96..0fdfacb0 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -7,6 +7,10 @@ link-hover-color = rgb(242, 60, 30) link-active-color = link-hover-color pro-color = hsla(0, 100%, 73%, 0.87) +theme-white = bg-color +theme-black = text-color +link-hover-text-shadow = none + // UI ui-border-color = rgba(0, 0, 0, 0.1) ui-border = 1px solid ui-border-color @@ -68,6 +72,6 @@ typography-margin = 0.4rem // nav-height = 3.11rem // Timings -fade-speed = 250ms -transition-speed = 200ms -mountable-transition-speed = 250ms +fade-speed = 200ms +transition-speed = 150ms +mountable-transition-speed = 200ms diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index f12ae59e..f328ed9d 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -3,8 +3,7 @@ text-color = hsl(0, 0%, 90%) bg-color = hsl(0, 0%, 24%) link-color = hsl(81, 100%, 56%) link-hover-color = hsl(81, 100%, 66%) -ui-background = hsla(0, 0%, 18%, 0.5) -tab-hover-background = link-hover-color +ui-background = hsla(0, 0%, 8%, 0.5) theme-white = bg-color theme-black = text-color @@ -15,4 +14,5 @@ main-color = link-color link-active-color = link-hover-color button-hover-color = link-hover-color button-hover-background = hsla(0, 0%, 12%, 0.5) +tab-hover-background = link-hover-color loading-anim-color = link-color \ No newline at end of file From 33d7ce593cd700a15985762972a9c8da09f181fa Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 4 Nov 2017 08:45:39 +0100 Subject: [PATCH 515/527] Redesign --- layout/sidebar/sidebar.scarlet | 7 +- pages/anime/anime.pixy | 289 ++++++++++++++++++++------------- pages/anime/anime.scarlet | 65 ++++++-- pages/anime/character.scarlet | 3 +- pages/anime/characters.pixy | 16 +- styles/include/config.scarlet | 6 +- styles/include/dark.scarlet | 18 +- styles/status-message.scarlet | 2 +- styles/tabs.scarlet | 2 +- styles/video.scarlet | 14 +- 10 files changed, 265 insertions(+), 157 deletions(-) diff --git a/layout/sidebar/sidebar.scarlet b/layout/sidebar/sidebar.scarlet index cbd0dcf1..1a6eba09 100644 --- a/layout/sidebar/sidebar.scarlet +++ b/layout/sidebar/sidebar.scarlet @@ -44,14 +44,13 @@ sidebar-spacing-y = 0.7rem &.active .sidebar-button - // background tab-hover-background - // color white - color link-color + color tab-active-color + background tab-active-background text-shadow link-hover-text-shadow + background tab-active-background .sidebar-button horizontal - default-transition align-items center padding sidebar-spacing-y 1rem // background ui-background diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 78a323fa..e07cb0a8 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,133 +1,192 @@ component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) - //- AnimeTabs(anime) .anime - .anime-header(data-id=anime.ID) - if anime.Image.Large != "" - .anime-image-container - img.anime-cover-image(src=anime.Image.Large, alt=anime.Title.ByUser(user)) + .anime-main-column + AnimeMainColumn(anime, user) + .anime-side-column + AnimeSideColumn(anime, friends, listItems, user) - //- if anime.StartDate != "" - //- .anime-start-date - //- span(title="Start date: " + anime.StartDate)= anime.StartDate[:4] - //- if anime.EndDate != "" && anime.StartDate[:4] != anime.EndDate[:4] - //- span - - //- span(title="End date: " + anime.EndDate)= anime.EndDate[:4] - - .space +component AnimeMainColumn(anime *arn.Anime, user *arn.User) + .anime-header(data-id=anime.ID) + if anime.Image.Large != "" + .anime-image-container.mountable + img.anime-cover-image(src=anime.Image.Large, alt=anime.Title.ByUser(user)) - .anime-info - h1.anime-title(title=anime.Type)= anime.Title.ByUser(user) - - h2.anime-alternative-title - Japanese(anime.Title.Japanese) - - //- h3.anime-section-name.anime-summary-header Summary - p.anime-summary= anime.Summary + //- if anime.StartDate != "" + //- .anime-start-date + //- span(title="Start date: " + anime.StartDate)= anime.StartDate[:4] + //- if anime.EndDate != "" && anime.StartDate[:4] != anime.EndDate[:4] + //- span - + //- span(title="End date: " + anime.EndDate)= anime.EndDate[:4] - if user != nil - .buttons.anime-actions - if user.Role == "editor" || user.Role == "admin" - a.button.ajax(href=anime.Link() + "/edit") - Icon("pencil-square-o") - span Edit anime + .space - if user.AnimeList().Contains(anime.ID) - a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) - Icon("pencil") - span Edit in collection - else - button.action(data-api="/api/animelist/" + user.ID, data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID) - Icon("plus") - span Add to collection + .anime-info + h1.anime-title.mountable(title=anime.Type)= anime.Title.ByUser(user) - //- h3.anime-section-name Ratings - //- .anime-rating-categories - //- .anime-rating-category(title=toString(anime.Rating.Overall)) - //- if anime.Status == "upcoming" - //- .anime-rating-category-name Hype - //- else - //- .anime-rating-category-name Overall - //- Rating(anime.Rating.Overall, user) - //- .anime-rating-category(title=toString(anime.Rating.Story)) - //- .anime-rating-category-name Story - //- Rating(anime.Rating.Story, user) - //- .anime-rating-category(title=toString(anime.Rating.Visuals)) - //- .anime-rating-category-name Visuals - //- Rating(anime.Rating.Visuals, user) - //- .anime-rating-category(title=toString(anime.Rating.Soundtrack)) - //- .anime-rating-category-name Soundtrack - //- Rating(anime.Rating.Soundtrack, user) + h2.anime-alternative-title.mountable + Japanese(anime.Title.Japanese) - //- if len(friends) > 0 - //- h3.anime-section-name Friends - - //- .anime-friends - //- .user-avatars - //- each friend in friends - //- if friend.Nick != "" - //- if friend.IsActive() - //- .mountable - //- FriendEntry(friend, listItems) - //- else - //- .mountable - //- .inactive-user - //- FriendEntry(friend, listItems) + //- h3.anime-section-name.anime-summary-header Summary + p.anime-summary.mountable= anime.Summary - //- if anime.Relations() != nil && len(anime.Relations().Items) > 0 - //- h3.anime-section-name Relations - //- .anime-relations - //- each relation in anime.Relations().Items - //- a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) - //- img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) - //- .anime-relation-type= relation.HumanReadableType() - //- .anime-relation-year - //- if relation.Anime().StartDate != "" - //- span= relation.Anime().StartDate[:4] + //- AnimeTabs(anime) + + if user != nil + .buttons.anime-actions + if user.Role == "editor" || user.Role == "admin" + a.button.ajax(href=anime.Link() + "/edit") + Icon("pencil-square-o") + span Edit anime - //- if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" - //- h3.anime-section-name Video - //- .anime-trailer.video-container - //- iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") + if user.AnimeList().Contains(anime.ID) + a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) + Icon("pencil") + span Edit in collection + else + button.action(data-api="/api/animelist/" + user.ID, data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID) + Icon("plus") + span Add to collection - //- h3.anime-section-name Popularity - //- .anime-rating-categories - //- .anime-rating-category - //- .anime-rating-category-name Watching - //- .anime-rating= anime.Popularity.Watching - //- .anime-rating-category - //- .anime-rating-category-name Completed - //- .anime-rating= anime.Popularity.Completed - //- .anime-rating-category - //- .anime-rating-category-name Planned - //- .anime-rating= anime.Popularity.Planned - //- .anime-rating-category - //- .anime-rating-category-name Hold - //- .anime-rating= anime.Popularity.Hold - //- .anime-rating-category - //- .anime-rating-category-name Dropped - //- .anime-rating= anime.Popularity.Dropped + //- h3.anime-section-name Ratings + //- .anime-rating-categories + //- .anime-rating-category(title=toString(anime.Rating.Overall)) + //- if anime.Status == "upcoming" + //- .anime-rating-category-name Hype + //- else + //- .anime-rating-category-name Overall + //- Rating(anime.Rating.Overall, user) + //- .anime-rating-category(title=toString(anime.Rating.Story)) + //- .anime-rating-category-name Story + //- Rating(anime.Rating.Story, user) + //- .anime-rating-category(title=toString(anime.Rating.Visuals)) + //- .anime-rating-category-name Visuals + //- Rating(anime.Rating.Visuals, user) + //- .anime-rating-category(title=toString(anime.Rating.Soundtrack)) + //- .anime-rating-category-name Soundtrack + //- Rating(anime.Rating.Soundtrack, user) + + if anime.Relations() != nil && len(anime.Relations().Items) > 0 + section.anime-section.mountable + h3.anime-section-name Relations + .anime-relations + each relation in anime.Relations().Items + a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) + img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) + .anime-relation-type= relation.HumanReadableType() + .anime-relation-year + if relation.Anime().StartDate != "" + span= relation.Anime().StartDate[:4] + + AnimeCharacters(anime) + + //- h3.anime-section-name Popularity + //- .anime-rating-categories + //- .anime-rating-category + //- .anime-rating-category-name Watching + //- .anime-rating= anime.Popularity.Watching + //- .anime-rating-category + //- .anime-rating-category-name Completed + //- .anime-rating= anime.Popularity.Completed + //- .anime-rating-category + //- .anime-rating-category-name Planned + //- .anime-rating= anime.Popularity.Planned + //- .anime-rating-category + //- .anime-rating-category-name Hold + //- .anime-rating= anime.Popularity.Hold + //- .anime-rating-category + //- .anime-rating-category-name Dropped + //- .anime-rating= anime.Popularity.Dropped + + //- //- h3.anime-section-name Reviews + //- //- p Coming soon. + + //- h3.anime-section-name Links + //- .light-button-group + //- //- if anime.Links != nil + //- //- each link in anime.Links + //- //- a.light-button(href=link.URL, target="_blank") + //- //- Icon("external-link") + //- //- span= link.Title + //- a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") + //- Icon("external-link") + //- span Kitsu - //- //- h3.anime-section-name Reviews - //- //- p Coming soon. + //- each mapping in anime.Mappings + //- a.light-button(href=mapping.Link(), target="_blank", rel="noopener") + //- Icon("external-link") + //- span= mapping.Name() - //- h3.anime-section-name Links - //- .light-button-group - //- //- if anime.Links != nil - //- //- each link in anime.Links - //- //- a.light-button(href=link.URL, target="_blank") - //- //- Icon("external-link") - //- //- span= link.Title - //- a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") - //- Icon("external-link") - //- span Kitsu + //- .footer + //- span Powered by Kitsu. + +component AnimeSideColumn(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) + AnimeTrailer(anime) + AnimeInformation(anime) + AnimeFriends(friends, listItems) + +component AnimeTrailer(anime *arn.Anime) + if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" + h3.anime-section-name Trailer + .anime-trailer.video-container + iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") + +component AnimeFriends(friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem) + if len(friends) > 0 + section.anime-section.mountable + h3.anime-section-name Friends - //- each mapping in anime.Mappings - //- a.light-button(href=mapping.Link(), target="_blank", rel="noopener") - //- Icon("external-link") - //- span= mapping.Name() + .anime-friends + .user-avatars + each friend in friends + if friend.Nick != "" + if friend.IsActive() + FriendEntry(friend, listItems) + else + .inactive-user + FriendEntry(friend, listItems) - //- .footer - //- span Powered by Kitsu. +component AnimeInformation(anime *arn.Anime) + section.anime-section.mountable + h3.anime-section-name Information + table.anime-info-table + tr + td Type: + td= anime.TypeHumanReadable() + + if anime.EpisodeCount != 0 + tr + td Episodes: + td= anime.EpisodeCount + + if anime.EpisodeLength != 0 + tr + td Episode length: + td= strconv.Itoa(anime.EpisodeLength) + " min." + + tr + td Status: + td= anime.StatusHumanReadable() + + if anime.StartDate == anime.EndDate && anime.StartDate != "" && anime.EndDate != "" + if anime.StartDate != "" + tr + td Airing date: + td= anime.StartDate + else + if anime.StartDate != "" + tr + td Start date: + td= anime.StartDate + + if anime.EndDate != "" + tr + td End date: + td= anime.EndDate + + if anime.FirstChannel != "" + tr + td Channel: + td= anime.FirstChannel component FriendEntry(friend *arn.User, listItems map[*arn.User]*arn.AnimeListItem) CustomAvatar(friend, listItems[friend].Link(friend.Nick), friend.Nick + " => " + listItems[friend].Status + " | " + toString(listItems[friend].Episodes) + " eps | " + fmt.Sprintf("%.1f", listItems[friend].Rating.Overall) + " rating") diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index b3f69819..0e6e152c 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -1,6 +1,23 @@ .anime - max-width 1100px - margin 0 auto + vertical + +.anime-main-column + vertical + flex 1 + +.anime-side-column + vertical + margin-left 0 + margin-top 1rem + flex-basis 300px + +> 1500px + .anime + horizontal + + .anime-side-column + margin-left content-padding + margin-top 0 .anime-header vertical @@ -16,6 +33,20 @@ .anime-title text-align left + .anime-alternative-title + text-align left + +.anime-info-table + margin 0 + width 100% + max-width 600px + +.anime-section + margin-top 1rem + + :first-child + margin-top 0 !important + .anime-section-name font-weight bold @@ -32,7 +63,7 @@ .anime-cover-image // width 142px - width 225px + width 250px height auto border-radius 3px @@ -57,31 +88,29 @@ font-size 0.9em margin-top 0 margin-bottom 0.5rem - text-align left + text-align center font-weight normal line-height content-line-height .japanese - color rgba(255, 255, 255, 0.5) !important + color anime-alternative-title-color !important .anime-actions display none !important + // horizontal + // justify-content center -// .anime-actions -// horizontal -// justify-content center + // // Action button margin + // margin calc(content-padding - 0.5rem) -0.5rem -// // Action button margin -// margin calc(content-padding - 0.5rem) -0.5rem + // // Setting z-index requires setting a background as well + // z-index 10 -// // Setting z-index requires setting a background as well -// z-index 10 - -// > 1450px -// .anime-actions -// position absolute -// top 0 -// right content-padding +> 1450px + .anime-actions + position absolute + bottom 0 + right content-padding .anime-rating-categories horizontal diff --git a/pages/anime/character.scarlet b/pages/anime/character.scarlet index 340e2108..39cddcba 100644 --- a/pages/anime/character.scarlet +++ b/pages/anime/character.scarlet @@ -27,4 +27,5 @@ .character-image width 112px - height 175px \ No newline at end of file + height 112px + border-radius 10% \ No newline at end of file diff --git a/pages/anime/characters.pixy b/pages/anime/characters.pixy index b1e04ba9..48a8d9f6 100644 --- a/pages/anime/characters.pixy +++ b/pages/anime/characters.pixy @@ -1,9 +1,11 @@ component AnimeCharacters(anime *arn.Anime) - AnimeTabs(anime) + //- AnimeTabs(anime) - h3.anime-section-name Characters - .characters - each character in anime.Characters().Items - if character.Character() != nil - .mountable - Character(character.Character()) \ No newline at end of file + if len(anime.Characters().Items) > 0 + .anime-section + h3.anime-section-name Characters + .characters + each character in anime.Characters().Items + if character.Role == "main" && character.Character() != nil + .mountable + Character(character.Character()) \ No newline at end of file diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 0fdfacb0..5f8d3be0 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -6,6 +6,7 @@ link-color = rgb(215, 38, 15) link-hover-color = rgb(242, 60, 30) link-active-color = link-hover-color pro-color = hsla(0, 100%, 73%, 0.87) +anime-alternative-title-color = hsla(0, 0%, 0%, 0.5) theme-white = bg-color theme-black = text-color @@ -29,8 +30,9 @@ input-focus-border-color = rgb(248, 165, 130) // Button button-hover-color = white button-hover-background = link-hover-color -tab-hover-background = #225db5 -// tab-hover-background = rgb(46, 85, 160) +tab-active-color = white +tab-active-background = hsl(216, 68%, 42%) +// tab-active-background = rgb(46, 85, 160) // Forum forum-width = 830px diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index f328ed9d..bfa78661 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -1,18 +1,26 @@ // Dark theme + +// Main color +hue = 45 +saturation = 100% + +// Derived colors text-color = hsl(0, 0%, 90%) bg-color = hsl(0, 0%, 24%) -link-color = hsl(81, 100%, 56%) -link-hover-color = hsl(81, 100%, 66%) +link-color = hsl(hue, saturation, 56%) +link-hover-color = hsl(hue, saturation, 66%) ui-background = hsla(0, 0%, 8%, 0.5) theme-white = bg-color theme-black = text-color -link-hover-text-shadow = 0 0 8px hsla(81, 100%, 56%, 0.5) +link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 56%, 0.5) main-color = link-color link-active-color = link-hover-color button-hover-color = link-hover-color button-hover-background = hsla(0, 0%, 12%, 0.5) -tab-hover-background = link-hover-color -loading-anim-color = link-color \ No newline at end of file +tab-active-color = bg-color +tab-active-background = white +loading-anim-color = link-color +anime-alternative-title-color = hsla(0, 0%, 100%, 0.5) \ No newline at end of file diff --git a/styles/status-message.scarlet b/styles/status-message.scarlet index 8a99505f..69b84660 100644 --- a/styles/status-message.scarlet +++ b/styles/status-message.scarlet @@ -22,4 +22,4 @@ .info-message color white - background tab-hover-background \ No newline at end of file + background tab-active-background \ No newline at end of file diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index 37793fc4..b64c0ea3 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -14,7 +14,7 @@ transform none &.active - background tab-hover-background + background tab-active-background color theme-white :first-child diff --git a/styles/video.scarlet b/styles/video.scarlet index 3e84a471..51447e40 100644 --- a/styles/video.scarlet +++ b/styles/video.scarlet @@ -1,9 +1,17 @@ -iframe - min-height 200px +// iframe +// min-height 200px .video-container + position relative width 100% + height 0 + padding-bottom 56.25% + border-radius ui-element-border-radius + overflow hidden .video + position absolute width 100% - height 100vh \ No newline at end of file + height 100% + left 0 + top 0 \ No newline at end of file From b12c4130ba398011a2c5c25612e65c6b2f8af6c1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 4 Nov 2017 11:09:19 +0100 Subject: [PATCH 516/527] Redesign --- mixins/SoundTrack.pixy | 4 - pages/anime/anime.go | 11 +- pages/anime/anime.pixy | 230 ++++++++++++++++++-------------- pages/anime/anime.scarlet | 39 +++--- pages/anime/characters.pixy | 2 +- pages/anime/tracks.pixy | 12 +- pages/profile/followers.scarlet | 2 +- styles/include/dark.scarlet | 6 +- styles/light-button.scarlet | 2 +- 9 files changed, 175 insertions(+), 133 deletions(-) diff --git a/mixins/SoundTrack.pixy b/mixins/SoundTrack.pixy index fa96421a..865c648d 100644 --- a/mixins/SoundTrack.pixy +++ b/mixins/SoundTrack.pixy @@ -1,9 +1,5 @@ component SoundTrack(track *arn.SoundTrack) SoundTrackMedia(track, track.Media[0]) - -component SoundTrackAllMedia(track *arn.SoundTrack) - each media in track.Media - SoundTrackMedia(track, media) component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia) .sound-track.mountable(id=track.ID) diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 0277f053..fd1eee5f 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -70,6 +70,15 @@ func Get(ctx *aero.Context) string { }) } + // Soundtracks + tracks, err := arn.FilterSoundTracks(func(track *arn.SoundTrack) bool { + return !track.IsDraft && len(track.Media) > 0 && arn.Contains(track.Tags, "anime:"+anime.ID) + }) + + if err != nil { + return ctx.Error(http.StatusNotFound, "Error fetching soundtracks", err) + } + // Open Graph description := anime.Summary @@ -100,5 +109,5 @@ func Get(ctx *aero.Context) string { ctx.Data = openGraph - return ctx.HTML(components.Anime(anime, friends, friendsAnimeListItems, user)) + return ctx.HTML(components.Anime(anime, tracks, friends, friendsAnimeListItems, user)) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index e07cb0a8..78134249 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,11 +1,11 @@ -component Anime(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) +component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) .anime .anime-main-column - AnimeMainColumn(anime, user) + AnimeMainColumn(anime, tracks, user) .anime-side-column AnimeSideColumn(anime, friends, listItems, user) -component AnimeMainColumn(anime *arn.Anime, user *arn.User) +component AnimeMainColumn(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) .anime-header(data-id=anime.ID) if anime.Image.Large != "" .anime-image-container.mountable @@ -47,88 +47,119 @@ component AnimeMainColumn(anime *arn.Anime, user *arn.User) Icon("plus") span Add to collection - //- h3.anime-section-name Ratings - //- .anime-rating-categories - //- .anime-rating-category(title=toString(anime.Rating.Overall)) - //- if anime.Status == "upcoming" - //- .anime-rating-category-name Hype - //- else - //- .anime-rating-category-name Overall - //- Rating(anime.Rating.Overall, user) - //- .anime-rating-category(title=toString(anime.Rating.Story)) - //- .anime-rating-category-name Story - //- Rating(anime.Rating.Story, user) - //- .anime-rating-category(title=toString(anime.Rating.Visuals)) - //- .anime-rating-category-name Visuals - //- Rating(anime.Rating.Visuals, user) - //- .anime-rating-category(title=toString(anime.Rating.Soundtrack)) - //- .anime-rating-category-name Soundtrack - //- Rating(anime.Rating.Soundtrack, user) + AnimeCharacters(anime) + AnimeRelations(anime, user) + AnimeTracks(anime, tracks) + + //- //- h3.anime-section-name Reviews + //- //- p Coming soon. + //- .footer + //- span Powered by Kitsu. + +component AnimeRatings(anime *arn.Anime, user *arn.User) + section.anime-section.mountable + h3.anime-section-name Ratings + + table.anime-info-table + tr.mountable(data-mountable-type="info") + td.anime-info-key + if anime.Status == "upcoming" + span Hype: + else + span Overall: + td.anime-info-value + Rating(anime.Rating.Overall, user) + + if anime.Rating.Story > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key Story: + td.anime-info-value + Rating(anime.Rating.Story, user) + + if anime.Rating.Visuals > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key Visuals: + td.anime-info-value + Rating(anime.Rating.Visuals, user) + + if anime.Rating.Soundtrack > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key Soundtrack: + td.anime-info-value + Rating(anime.Rating.Soundtrack, user) + +component AnimePopularity(anime *arn.Anime) + if anime.Popularity.Total() > 0 + section.anime-section.mountable + h3.anime-section-name Popularity + + table.anime-info-table + if anime.Popularity.Watching > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key Watching: + td.anime-info-value= anime.Popularity.Watching + + if anime.Popularity.Completed > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key Completed: + td.anime-info-value= anime.Popularity.Completed + + if anime.Popularity.Planned > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key Planned: + td.anime-info-value= anime.Popularity.Planned + + if anime.Popularity.Hold > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key On hold: + td.anime-info-value= anime.Popularity.Hold + + if anime.Popularity.Dropped > 0 + tr.mountable(data-mountable-type="info") + td.anime-info-key Dropped: + td.anime-info-value= anime.Popularity.Dropped + +component AnimeLinks(anime *arn.Anime) + section.anime-section.mountable + h3.anime-section-name Links + .light-button-group + a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") + Icon("external-link") + span Kitsu + + each mapping in anime.Mappings + a.light-button(href=mapping.Link(), target="_blank", rel="noopener") + Icon("external-link") + span= mapping.Name() + +component AnimeSideColumn(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) + AnimeTrailer(anime) + AnimeInformation(anime) + AnimeRatings(anime, user) + AnimePopularity(anime) + AnimeFriends(friends, listItems) + AnimeLinks(anime) + +component AnimeRelations(anime *arn.Anime, user *arn.User) if anime.Relations() != nil && len(anime.Relations().Items) > 0 section.anime-section.mountable h3.anime-section-name Relations .anime-relations each relation in anime.Relations().Items - a.anime-relation.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user)) + a.anime-relation.mountable.ajax(href=relation.Anime().Link(), title=relation.Anime().Title.ByUser(user), data-mountable-type="relation") img.anime-relation-image.lazy(data-src=relation.Anime().Image.Tiny, alt=relation.Anime().Title.ByUser(user)) .anime-relation-type= relation.HumanReadableType() .anime-relation-year if relation.Anime().StartDate != "" span= relation.Anime().StartDate[:4] - AnimeCharacters(anime) - - //- h3.anime-section-name Popularity - //- .anime-rating-categories - //- .anime-rating-category - //- .anime-rating-category-name Watching - //- .anime-rating= anime.Popularity.Watching - //- .anime-rating-category - //- .anime-rating-category-name Completed - //- .anime-rating= anime.Popularity.Completed - //- .anime-rating-category - //- .anime-rating-category-name Planned - //- .anime-rating= anime.Popularity.Planned - //- .anime-rating-category - //- .anime-rating-category-name Hold - //- .anime-rating= anime.Popularity.Hold - //- .anime-rating-category - //- .anime-rating-category-name Dropped - //- .anime-rating= anime.Popularity.Dropped - - //- //- h3.anime-section-name Reviews - //- //- p Coming soon. - - //- h3.anime-section-name Links - //- .light-button-group - //- //- if anime.Links != nil - //- //- each link in anime.Links - //- //- a.light-button(href=link.URL, target="_blank") - //- //- Icon("external-link") - //- //- span= link.Title - //- a.light-button(href="https://kitsu.io/anime/" + anime.ID, target="_blank", rel="noopener") - //- Icon("external-link") - //- span Kitsu - - //- each mapping in anime.Mappings - //- a.light-button(href=mapping.Link(), target="_blank", rel="noopener") - //- Icon("external-link") - //- span= mapping.Name() - - //- .footer - //- span Powered by Kitsu. - -component AnimeSideColumn(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) - AnimeTrailer(anime) - AnimeInformation(anime) - AnimeFriends(friends, listItems) - component AnimeTrailer(anime *arn.Anime) if len(anime.Trailers) > 0 && anime.Trailers[0].Service == "Youtube" && anime.Trailers[0].ServiceID != "" - h3.anime-section-name Trailer - .anime-trailer.video-container - iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") + section.anime-section.mountable + h3.anime-section-name Trailer + .anime-trailer.video-container + iframe.video(src="https://www.youtube.com/embed/" + anime.Trailers[0].ServiceID + "?showinfo=0", allowfullscreen="allowfullscreen") component AnimeFriends(friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem) if len(friends) > 0 @@ -139,54 +170,55 @@ component AnimeFriends(friends []*arn.User, listItems map[*arn.User]*arn.AnimeLi .user-avatars each friend in friends if friend.Nick != "" - if friend.IsActive() - FriendEntry(friend, listItems) - else - .inactive-user + .mountable(data-mountable-type="friend") + if friend.IsActive() FriendEntry(friend, listItems) + else + .inactive-user + FriendEntry(friend, listItems) component AnimeInformation(anime *arn.Anime) section.anime-section.mountable h3.anime-section-name Information table.anime-info-table - tr - td Type: - td= anime.TypeHumanReadable() + tr.mountable(data-mountable-type="info") + td.anime-info-key Type: + td.anime-info-value= anime.TypeHumanReadable() if anime.EpisodeCount != 0 - tr - td Episodes: - td= anime.EpisodeCount + tr.mountable(data-mountable-type="info") + td.anime-info-key Episodes: + td.anime-info-value= anime.EpisodeCount if anime.EpisodeLength != 0 - tr - td Episode length: - td= strconv.Itoa(anime.EpisodeLength) + " min." + tr.mountable(data-mountable-type="info") + td.anime-info-key Episode length: + td.anime-info-value= strconv.Itoa(anime.EpisodeLength) + " min." - tr - td Status: - td= anime.StatusHumanReadable() + tr.mountable(data-mountable-type="info") + td.anime-info-key Status: + td.anime-info-value= anime.StatusHumanReadable() if anime.StartDate == anime.EndDate && anime.StartDate != "" && anime.EndDate != "" if anime.StartDate != "" - tr - td Airing date: - td= anime.StartDate + tr.mountable(data-mountable-type="info") + td.anime-info-key Airing date: + td.anime-info-value= anime.StartDate else if anime.StartDate != "" - tr - td Start date: - td= anime.StartDate + tr.mountable(data-mountable-type="info") + td.anime-info-key Start date: + td.anime-info-value= anime.StartDate if anime.EndDate != "" - tr - td End date: - td= anime.EndDate + tr.mountable(data-mountable-type="info") + td.anime-info-key End date: + td.anime-info-value= anime.EndDate if anime.FirstChannel != "" - tr - td Channel: - td= anime.FirstChannel + tr.mountable(data-mountable-type="info") + td.anime-info-key Channel: + td.anime-info-value= anime.FirstChannel component FriendEntry(friend *arn.User, listItems map[*arn.User]*arn.AnimeListItem) CustomAvatar(friend, listItems[friend].Link(friend.Nick), friend.Nick + " => " + listItems[friend].Status + " | " + toString(listItems[friend].Episodes) + " eps | " + fmt.Sprintf("%.1f", listItems[friend].Rating.Overall) + " rating") diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 0e6e152c..274f9e50 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -38,8 +38,11 @@ .anime-info-table margin 0 - width 100% - max-width 600px + // width 100% + // max-width 600px + +.anime-info-value + text-align right .anime-section margin-top 1rem @@ -112,25 +115,25 @@ bottom 0 right content-padding -.anime-rating-categories - horizontal - width 100% +// .anime-rating-categories +// horizontal +// width 100% -.anime-rating-category - ui-element - flex 1 - text-align center - margin 0.5rem +// .anime-rating-category +// ui-element +// flex 1 +// text-align center +// margin 0.5rem -.anime-rating-category-name - font-size 1.3rem - margin-top 0.5rem +// .anime-rating-category-name +// font-size 1.3rem +// margin-top 0.5rem -.anime-rating - margin 0.5rem - letter-spacing 3px - font-size 1.3rem - color link-color +// .anime-rating +// margin 0.5rem +// letter-spacing 3px +// font-size 1.3rem +// color link-color .anime-widget margin-top 0.6rem diff --git a/pages/anime/characters.pixy b/pages/anime/characters.pixy index 48a8d9f6..8700c1f4 100644 --- a/pages/anime/characters.pixy +++ b/pages/anime/characters.pixy @@ -7,5 +7,5 @@ component AnimeCharacters(anime *arn.Anime) .characters each character in anime.Characters().Items if character.Role == "main" && character.Character() != nil - .mountable + .mountable(data-mountable-type="character") Character(character.Character()) \ No newline at end of file diff --git a/pages/anime/tracks.pixy b/pages/anime/tracks.pixy index f1a7bea2..01449484 100644 --- a/pages/anime/tracks.pixy +++ b/pages/anime/tracks.pixy @@ -1,7 +1,9 @@ component AnimeTracks(anime *arn.Anime, tracks []*arn.SoundTrack) - AnimeTabs(anime) + //- AnimeTabs(anime) - h3.anime-section-name Tracks - .sound-tracks - each track in tracks - SoundTrack(track) \ No newline at end of file + if len(tracks) > 0 + .anime-section.mountable + h3.anime-section-name Tracks + .anime-soundtracks + each track in tracks + ExternalMedia(track.Media[0]) \ No newline at end of file diff --git a/pages/profile/followers.scarlet b/pages/profile/followers.scarlet index 2550fa2b..adf658da 100644 --- a/pages/profile/followers.scarlet +++ b/pages/profile/followers.scarlet @@ -1,2 +1,2 @@ .inactive-user - opacity 0.25 \ No newline at end of file + // opacity 0.25 \ No newline at end of file diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index bfa78661..e02ea6ff 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -7,14 +7,14 @@ saturation = 100% // Derived colors text-color = hsl(0, 0%, 90%) bg-color = hsl(0, 0%, 24%) -link-color = hsl(hue, saturation, 56%) -link-hover-color = hsl(hue, saturation, 66%) +link-color = hsl(hue, saturation, 66%) +link-hover-color = hsl(hue, saturation, 76%) ui-background = hsla(0, 0%, 8%, 0.5) theme-white = bg-color theme-black = text-color -link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 56%, 0.5) +link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 66%, 0.5) main-color = link-color link-active-color = link-hover-color diff --git a/styles/light-button.scarlet b/styles/light-button.scarlet index 23308078..dbcc2bb8 100644 --- a/styles/light-button.scarlet +++ b/styles/light-button.scarlet @@ -10,5 +10,5 @@ font-size 0.9rem :hover - color white !important + color theme-white !important background-color link-hover-color \ No newline at end of file From 28da13e8f06d39c7e7630f640fb62daccbfc7c28 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 4 Nov 2017 17:11:47 +0100 Subject: [PATCH 517/527] Redesign --- layout/sidebar/sidebar.scarlet | 2 +- pages/anime/anime.go | 4 ++ pages/anime/anime.scarlet | 28 ++++++++++++- pages/anime/characters.pixy | 2 +- pages/anime/tracks.pixy | 5 ++- pages/animelist/animelist.scarlet | 5 +++ pages/settings/settings.pixy | 67 +++++++++++++++---------------- styles/include/config.scarlet | 3 ++ styles/include/dark.scarlet | 6 ++- styles/tabs.scarlet | 10 +++-- styles/video.scarlet | 5 +-- 11 files changed, 90 insertions(+), 47 deletions(-) diff --git a/layout/sidebar/sidebar.scarlet b/layout/sidebar/sidebar.scarlet index 1a6eba09..08af9c02 100644 --- a/layout/sidebar/sidebar.scarlet +++ b/layout/sidebar/sidebar.scarlet @@ -46,7 +46,7 @@ sidebar-spacing-y = 0.7rem .sidebar-button color tab-active-color background tab-active-background - text-shadow link-hover-text-shadow + text-shadow tab-active-text-shadow background tab-active-background .sidebar-button diff --git a/pages/anime/anime.go b/pages/anime/anime.go index fd1eee5f..71ea06d4 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -75,6 +75,10 @@ func Get(ctx *aero.Context) string { return !track.IsDraft && len(track.Media) > 0 && arn.Contains(track.Tags, "anime:"+anime.ID) }) + sort.Slice(tracks, func(i, j int) bool { + return tracks[i].Title < tracks[j].Title + }) + if err != nil { return ctx.Error(http.StatusNotFound, "Error fetching soundtracks", err) } diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 274f9e50..17668ebe 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -11,7 +11,7 @@ margin-top 1rem flex-basis 300px -> 1500px +> 1400px .anime horizontal @@ -44,6 +44,32 @@ .anime-info-value text-align right +.anime-soundtracks + vertical + margin-top 1rem + +.anime-soundtrack + vertical + width 100% + margin-bottom 0.5rem + +> 500px + .anime-soundtracks + horizontal-wrap + justify-content flex-start + margin-top 0 + + .anime-soundtrack + max-width 200px + margin calc(content-padding / 2) + +// .anime-soundtracks +// horizontal-wrap + +// .anime-soundtrack +// flex-basis 400px +// padding-bottom video-padding + .anime-section margin-top 1rem diff --git a/pages/anime/characters.pixy b/pages/anime/characters.pixy index 8700c1f4..2cf9174b 100644 --- a/pages/anime/characters.pixy +++ b/pages/anime/characters.pixy @@ -1,7 +1,7 @@ component AnimeCharacters(anime *arn.Anime) //- AnimeTabs(anime) - if len(anime.Characters().Items) > 0 + if anime.Characters() != nil && len(anime.Characters().Items) > 0 .anime-section h3.anime-section-name Characters .characters diff --git a/pages/anime/tracks.pixy b/pages/anime/tracks.pixy index 01449484..72b93e15 100644 --- a/pages/anime/tracks.pixy +++ b/pages/anime/tracks.pixy @@ -6,4 +6,7 @@ component AnimeTracks(anime *arn.Anime, tracks []*arn.SoundTrack) h3.anime-section-name Tracks .anime-soundtracks each track in tracks - ExternalMedia(track.Media[0]) \ No newline at end of file + .anime-soundtrack.mountable(data-mountable-type="track") + .video-container + iframe.video.lazy(data-src=track.Media[0].EmbedLink(), allowfullscreen="allowfullscreen") + a.sound-track-footer.ajax(href=track.Link())= track.Title \ No newline at end of file diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 7c7bc4ca..050fa74e 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -29,6 +29,11 @@ flex 1 clip-long-text + a + color text-color + :hover + color link-hover-color + .anime-list-item-episodes horizontal justify-content flex-end diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index a42c422c..99cee268 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -73,24 +73,6 @@ component Settings(user *arn.User) Icon("circle-o") span Not connected - .widget.mountable - h3.widget-title - Icon("download") - span Import - - ImportLists(user) - - .widget.mountable - h3.widget-title - Icon("upload") - span Export - - .widget-section - label JSON: - a.button(href="/api/animelist/" + user.ID) - Icon("upload") - span Export anime list as JSON - .widget.mountable h3.widget-title Icon("puzzle-piece") @@ -113,6 +95,24 @@ component Settings(user *arn.User) a.button(href="https://www.youtube.com/watch?v=opyt4cw0ep8", target="_blank", rel="noopener") Icon("android") span Get the Android App + + .widget.mountable + h3.widget-title + Icon("download") + span Import + + ImportLists(user) + + .widget.mountable + h3.widget-title + Icon("upload") + span Export + + .widget-section + label JSON: + a.button(href="/api/animelist/" + user.ID) + Icon("upload") + span Export anime list as JSON .widget.mountable(data-api="/api/settings/" + user.ID) h3.widget-title @@ -138,6 +138,21 @@ component Settings(user *arn.User) .profile-image-container.avatar-preview img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") + .widget.mountable(data-api="/api/settings/" + user.ID) + h3.widget-title + Icon("font") + span Formatting + + .widget-section + label(for="TitleLanguage")= "Title language:" + select.widget-ui-element.action(id="TitleLanguage", data-field="TitleLanguage", value=user.Settings().TitleLanguage, title="Language of anime titles", data-action="save", data-trigger="change") + option(value="canonical") Canonical + option(value="english") English + option(value="romaji") Romaji + option(value="japanese") 日本語 + + InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") + .widget.mountable(data-api="/api/settings/" + user.ID) h3.widget-title Icon("star") @@ -158,22 +173,6 @@ component Settings(user *arn.User) a.button.ajax(href="/shop") Icon("star") span Go PRO - - .widget.mountable(data-api="/api/settings/" + user.ID) - h3.widget-title - Icon("font") - span Formatting - - .widget-section - label(for="TitleLanguage")= "Title language:" - select.widget-ui-element.action(id="TitleLanguage", data-field="TitleLanguage", value=user.Settings().TitleLanguage, title="Language of anime titles", data-action="save", data-trigger="change") - option(value="canonical") Canonical - option(value="english") English - option(value="romaji") Romaji - option(value="japanese") 日本語 - - InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") - //- .widget.mountable(data-api="/api/settings/" + user.ID) //- h3.widget-title diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 5f8d3be0..2f678be5 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -11,6 +11,7 @@ anime-alternative-title-color = hsla(0, 0%, 0%, 0.5) theme-white = bg-color theme-black = text-color link-hover-text-shadow = none +tab-active-text-shadow = none // UI ui-border-color = rgba(0, 0, 0, 0.1) @@ -30,6 +31,8 @@ input-focus-border-color = rgb(248, 165, 130) // Button button-hover-color = white button-hover-background = link-hover-color +tab-background = rgba(0, 0, 0, 0.02) +tab-hover-background = bg-color tab-active-color = white tab-active-background = hsl(216, 68%, 42%) // tab-active-background = rgb(46, 85, 160) diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index e02ea6ff..f45c0679 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -20,7 +20,9 @@ main-color = link-color link-active-color = link-hover-color button-hover-color = link-hover-color button-hover-background = hsla(0, 0%, 12%, 0.5) -tab-active-color = bg-color -tab-active-background = white +tab-background = hsla(0, 0%, 0%, 0.1) +tab-hover-background = hsla(0, 0%, 0%, 0.2) +tab-active-color = hsl(0, 0%, 95%) +tab-active-background = hsla(0, 0%, 2%, 0.5) loading-anim-color = link-color anime-alternative-title-color = hsla(0, 0%, 100%, 0.5) \ No newline at end of file diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index b64c0ea3..1e17a82d 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -1,21 +1,23 @@ .tab color text-color padding 0.5rem 1rem - background-color rgba(0, 0, 0, 0.02) + background-color tab-background border ui-border border-left none white-space nowrap :hover color text-color - background-color bg-color + background-color tab-hover-background + text-shadow none :active transform none &.active - background tab-active-background - color theme-white + background-color tab-active-background + color tab-active-color + text-shadow tab-active-text-shadow :first-child border-left ui-border diff --git a/styles/video.scarlet b/styles/video.scarlet index 51447e40..5db2e220 100644 --- a/styles/video.scarlet +++ b/styles/video.scarlet @@ -1,11 +1,10 @@ -// iframe -// min-height 200px +video-padding = 56.25% .video-container position relative width 100% height 0 - padding-bottom 56.25% + padding-bottom video-padding border-radius ui-element-border-radius overflow hidden From 7d748d0368fb35b8e3c17e1a5dbba8c34093291a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 4 Nov 2017 22:23:07 +0100 Subject: [PATCH 518/527] Icon update --- config.json | 19 +++++++++++-------- images/brand/128.png | Bin 0 -> 32655 bytes images/brand/128.webp | Bin 0 -> 8544 bytes images/brand/144.png | Bin 52172 -> 37083 bytes images/brand/144.webp | Bin 13538 -> 12292 bytes images/brand/220.png | Bin 0 -> 70715 bytes images/brand/220.webp | Bin 0 -> 23830 bytes images/brand/300.png | Bin 128420 -> 0 bytes images/brand/300.webp | Bin 38364 -> 0 bytes images/brand/600.png | Bin 457976 -> 0 bytes images/brand/600.webp | Bin 99612 -> 0 bytes images/brand/64.png | Bin 11111 -> 12055 bytes images/brand/64.webp | Bin 3888 -> 3604 bytes jobs/test/test.go | 2 +- layout/sidebar/sidebar.scarlet | 3 ++- pages/frontpage/frontpage.go | 2 +- pages/notifications/notifications.go | 2 +- pages/paypal/paypal.go | 2 +- styles/include/dark.scarlet | 2 +- styles/tabs.scarlet | 6 ++++++ 20 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 images/brand/128.png create mode 100644 images/brand/128.webp create mode 100644 images/brand/220.png create mode 100644 images/brand/220.webp delete mode 100644 images/brand/300.png delete mode 100644 images/brand/300.webp delete mode 100644 images/brand/600.png delete mode 100644 images/brand/600.webp diff --git a/config.json b/config.json index 8e3794a9..ff43a316 100644 --- a/config.json +++ b/config.json @@ -32,12 +32,18 @@ "manifest": { "short_name": "notify.moe", "gcm_sender_id": "941298467524", - "theme_color": "#f8a582", + "theme_color": "#f0c7bb", "background_color": "#ffffff", "icons": [ { "src": "images/brand/64.png", - "sizes": "64x64" + "sizes": "64x64", + "type": "image/png" + }, + { + "src": "images/brand/128.png", + "sizes": "128x128", + "type": "image/png" }, { "src": "images/brand/144.png", @@ -45,12 +51,9 @@ "type": "image/png" }, { - "src": "images/brand/300.png", - "sizes": "300x300" - }, - { - "src": "images/brand/600.png", - "sizes": "600x600" + "src": "images/brand/220.png", + "sizes": "220x220", + "type": "image/png" } ] }, diff --git a/images/brand/128.png b/images/brand/128.png new file mode 100644 index 0000000000000000000000000000000000000000..7b42c8c6c9276bdc87453cb2505542b169c56e3b GIT binary patch literal 32655 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 YowN<{db-J+fq{X+)78&qol`;+0J-bvGXMYp literal 0 HcmV?d00001 diff --git a/images/brand/128.webp b/images/brand/128.webp new file mode 100644 index 0000000000000000000000000000000000000000..c7874d0f98b61ee66b45105731a97d352d058eef GIT binary patch literal 8544 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

    c?=IC<8sY&~b{S*Ja**T~DYxs`?PoH17 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 z dLd^%>ha1)!t>L9 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

  • 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 a472)fe$NRJUd+J2z~JfX=d#Wzp$Py7Y}9@L diff --git a/images/brand/144.webp b/images/brand/144.webp index ed4dd62867d465442ae00efe97025370de3d35f5..707e2a05a4ec5fe576991a6c9314c09c470f09f0 100644 GIT binary patch literal 12292 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 literal 13538 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 GA*h`x%u?nwFj-rFK*|% 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 diff --git a/images/brand/220.png b/images/brand/220.png new file mode 100644 index 0000000000000000000000000000000000000000..64889a909b44c586341fb1dd08c26e45a6b4d0e4 GIT binary patch literal 70715 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 Q3=9kmp00i_>zopr0NP_BIRF3v literal 0 HcmV?d00001 diff --git a/images/brand/220.webp b/images/brand/220.webp new file mode 100644 index 0000000000000000000000000000000000000000..abd851256fd4879efeb55d9917871fd311da1026 GIT binary patch literal 23830 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 DIU8>1 literal 0 HcmV?d00001 diff --git a/images/brand/300.png b/images/brand/300.png deleted file mode 100644 index 13edc0191d416363f7efa7eae527f6a9df9fde2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128420 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

    9l`T*vl4~ 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%0DkZfsv%Lgl+N2hjUi$a=nAmk5Q;lvGxn)e%GVW3f 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≠DT)D1urcF_AsreZE|A4o@72ni%PdZ+v8!S%d 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 z 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 diff --git a/images/brand/300.webp b/images/brand/300.webp deleted file mode 100644 index 928a51a505f1dc69d3df0e4e82f2dde798b66fcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38364 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

    }p>9FVO9GbmXJYt# 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%0D+B#MAQfTRkU8}rP=Jy)anI0+6&NE$f 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 Q?5Rm=KKkBX^$iRR0O0JlX8-^I diff --git a/images/brand/64.png b/images/brand/64.png index 0309ab76852582d2b5c7d4bc6e5806581f42d5ab..4fc9b10dc429bc97779bac5379f835dc8d822971 100644 GIT binary patch literal 12055 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 I=akR{0MS7p)Bpeg literal 11111 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 Q*?4Jb*P%Kl9R>yl06^n2UH||9 literal 3888 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 diff --git a/jobs/test/test.go b/jobs/test/test.go index f506acd9..ace5bfac 100644 --- a/jobs/test/test.go +++ b/jobs/test/test.go @@ -59,7 +59,7 @@ func testPackage(pkg string) { Title: pkg, Message: "Test failed", Link: "https://" + pkg, - Icon: "https://notify.moe/images/brand/300.png", + Icon: "https://notify.moe/images/brand/220.png", }) return } diff --git a/layout/sidebar/sidebar.scarlet b/layout/sidebar/sidebar.scarlet index 08af9c02..95e0f34c 100644 --- a/layout/sidebar/sidebar.scarlet +++ b/layout/sidebar/sidebar.scarlet @@ -32,7 +32,8 @@ sidebar-spacing-y = 0.7rem pointer-events auto box-shadow none border-right ui-border - background rgba(0, 0, 0, 0.03) + // background rgba(0, 0, 0, 0.03) + background rgba(0, 0, 0, 0.2) .sidebar-visible transform translateX(0) !important diff --git a/pages/frontpage/frontpage.go b/pages/frontpage/frontpage.go index d8a69afb..32d66d4b 100644 --- a/pages/frontpage/frontpage.go +++ b/pages/frontpage/frontpage.go @@ -16,7 +16,7 @@ func Get(ctx *aero.Context) string { "og:description": description, "og:type": "website", "og:url": "https://" + ctx.App.Config.Domain, - "og:image": "https://" + ctx.App.Config.Domain + "/images/brand/600.png", + "og:image": "https://" + ctx.App.Config.Domain + "/images/brand/220.png", }, Meta: map[string]string{ "description": description, diff --git a/pages/notifications/notifications.go b/pages/notifications/notifications.go index 787c0716..8bf4a4ac 100644 --- a/pages/notifications/notifications.go +++ b/pages/notifications/notifications.go @@ -19,7 +19,7 @@ func Test(ctx *aero.Context) string { notification := &arn.Notification{ Title: "Anime Notifier", Message: "Yay, it works!", - Icon: "https://" + ctx.App.Config.Domain + "/images/brand/300.png", + Icon: "https://" + ctx.App.Config.Domain + "/images/brand/220.png", } user.SendNotification(notification) diff --git a/pages/paypal/paypal.go b/pages/paypal/paypal.go index 2c5d188f..af1813f6 100644 --- a/pages/paypal/paypal.go +++ b/pages/paypal/paypal.go @@ -49,7 +49,7 @@ func CreatePayment(ctx *aero.Context) string { // Name: "Anime Notifier", // Presentation: paypalsdk.Presentation{ // BrandName: "Anime Notifier", - // LogoImage: "https://notify.moe/brand/300", + // LogoImage: "https://notify.moe/brand/220", // LocaleCode: "US", // }, diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index f45c0679..a960135d 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -9,7 +9,7 @@ text-color = hsl(0, 0%, 90%) bg-color = hsl(0, 0%, 24%) link-color = hsl(hue, saturation, 66%) link-hover-color = hsl(hue, saturation, 76%) -ui-background = hsla(0, 0%, 8%, 0.5) +ui-background = hsla(0, 0%, 8%, 0.3) theme-white = bg-color theme-black = text-color diff --git a/styles/tabs.scarlet b/styles/tabs.scarlet index 1e17a82d..63f496d8 100644 --- a/styles/tabs.scarlet +++ b/styles/tabs.scarlet @@ -21,6 +21,12 @@ :first-child border-left ui-border + border-top-left-radius ui-element-border-radius + border-bottom-left-radius ui-element-border-radius + + :last-child + border-top-right-radius ui-element-border-radius + border-bottom-right-radius ui-element-border-radius // color text-color !important // :hover From ce03cadc985dac6f42f29171e2b8315444006915 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 4 Nov 2017 22:50:05 +0100 Subject: [PATCH 519/527] Minor changes --- styles/forum.scarlet | 15 ++++++--------- styles/include/config.scarlet | 5 +++++ styles/include/dark.scarlet | 13 +++++++++---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/styles/forum.scarlet b/styles/forum.scarlet index ad97aec8..7dbf2d9c 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -98,17 +98,14 @@ post-content-padding-y = 0.75rem &:before content "+" -.post-permalink - color blue !important +// .post-permalink +// color post-permalink-color !important -.post-delete - color rgb(255, 32, 12) !important +// .post-like +// color post-like-color !important -.post-like - color green !important - -.post-unlike - color rgb(255, 32, 12) !important +// .post-unlike +// color post-unlike-color !important .post-save // diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 2f678be5..186aac38 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -53,6 +53,11 @@ nav-link-hover-slide-color = main-color // nav-link-color = rgb(160, 160, 160) // nav-link-hover-color = rgb(80, 80, 80) +// Forum +post-like-color = green +post-unlike-color = rgb(255, 32, 12) +post-permalink-color = blue + // Tables table-width-normal = 900px diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index a960135d..e590a7cd 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -4,18 +4,18 @@ hue = 45 saturation = 100% -// Derived colors +// Main text-color = hsl(0, 0%, 90%) bg-color = hsl(0, 0%, 24%) link-color = hsl(hue, saturation, 66%) link-hover-color = hsl(hue, saturation, 76%) +link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 66%, 0.5) ui-background = hsla(0, 0%, 8%, 0.3) +// White/black theme-white = bg-color theme-black = text-color -link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 66%, 0.5) - main-color = link-color link-active-color = link-hover-color button-hover-color = link-hover-color @@ -25,4 +25,9 @@ tab-hover-background = hsla(0, 0%, 0%, 0.2) tab-active-color = hsl(0, 0%, 95%) tab-active-background = hsla(0, 0%, 2%, 0.5) loading-anim-color = link-color -anime-alternative-title-color = hsla(0, 0%, 100%, 0.5) \ No newline at end of file +anime-alternative-title-color = hsla(0, 0%, 100%, 0.5) + +// Forum +post-like-color = green +post-unlike-color = rgb(255, 32, 12) +post-permalink-color = blue \ No newline at end of file From 239ef24b5b661a30e5a040d334d74c4c6361d2b5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 06:56:09 +0100 Subject: [PATCH 520/527] Redesign --- layout/sidebar/sidebar.scarlet | 5 ++--- pages/animelist/animelist.scarlet | 2 +- pages/shop/history.pixy | 27 +++++++++++++++------------ styles/forum.scarlet | 12 ++++++------ styles/include/config.scarlet | 12 +++++++++--- styles/include/dark.scarlet | 10 +++++++--- styles/input.scarlet | 4 ++-- styles/table.scarlet | 5 ++++- styles/widgets.scarlet | 1 - 9 files changed, 46 insertions(+), 32 deletions(-) diff --git a/layout/sidebar/sidebar.scarlet b/layout/sidebar/sidebar.scarlet index 95e0f34c..0edd7907 100644 --- a/layout/sidebar/sidebar.scarlet +++ b/layout/sidebar/sidebar.scarlet @@ -8,7 +8,7 @@ sidebar-spacing-y = 0.7rem z-index 10 min-width 200px height 100% - background ui-background + background sidebar-opaque-background transform translateX(-100%) overflow-x hidden overflow-y auto @@ -32,8 +32,7 @@ sidebar-spacing-y = 0.7rem pointer-events auto box-shadow none border-right ui-border - // background rgba(0, 0, 0, 0.03) - background rgba(0, 0, 0, 0.2) + background sidebar-background .sidebar-visible transform translateX(0) !important diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index 050fa74e..2934ffcb 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -30,7 +30,7 @@ clip-long-text a - color text-color + color anime-list-item-name-color :hover color link-hover-color diff --git a/pages/shop/history.pixy b/pages/shop/history.pixy index f11e42e2..0992e59f 100644 --- a/pages/shop/history.pixy +++ b/pages/shop/history.pixy @@ -3,18 +3,21 @@ component PurchaseHistory(purchases []*arn.Purchase, user *arn.User) h1.page-title Purchase History - table - thead - tr.mountable - th Icon - th Item - th.history-quantity Quantity - th.history-price Price - th.history-date Date - tbody - each purchase in purchases - tr.shop-item.mountable(data-item-id=purchase.ItemID) - PurchaseInfo(purchase) + if len(purchases) == 0 + p.text-center.mountable You haven't bought anything yet. + else + table + thead + tr.mountable + th Icon + th Item + th.history-quantity Quantity + th.history-price Price + th.history-date Date + tbody + each purchase in purchases + tr.shop-item.mountable(data-item-id=purchase.ItemID) + PurchaseInfo(purchase) component PurchaseInfo(purchase *arn.Purchase) td.item-icon diff --git a/styles/forum.scarlet b/styles/forum.scarlet index 7dbf2d9c..1ad0affd 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -98,14 +98,14 @@ post-content-padding-y = 0.75rem &:before content "+" -// .post-permalink -// color post-permalink-color !important +.post-permalink + color post-permalink-color -// .post-like -// color post-like-color !important +.post-like + color post-like-color -// .post-unlike -// color post-unlike-color !important +.post-unlike + color post-unlike-color .post-save // diff --git a/styles/include/config.scarlet b/styles/include/config.scarlet index 186aac38..8a74749f 100644 --- a/styles/include/config.scarlet +++ b/styles/include/config.scarlet @@ -37,6 +37,9 @@ tab-active-color = white tab-active-background = hsl(216, 68%, 42%) // tab-active-background = rgb(46, 85, 160) +sidebar-background = rgba(0, 0, 0, 0.03) +sidebar-opaque-background = ui-background + // Forum forum-width = 830px post-highlight-color = rgba(248, 165, 130, 0.7) @@ -54,9 +57,12 @@ nav-link-hover-slide-color = main-color // nav-link-hover-color = rgb(80, 80, 80) // Forum -post-like-color = green -post-unlike-color = rgb(255, 32, 12) -post-permalink-color = blue +post-like-color = green !important +post-unlike-color = rgb(255, 32, 12) !important +post-permalink-color = blue !important + +table-row-hover-background = hsla(0, 0%, 0%, 0.01) +anime-list-item-name-color = link-color // Tables table-width-normal = 900px diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index e590a7cd..b47a9495 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -11,6 +11,9 @@ link-color = hsl(hue, saturation, 66%) link-hover-color = hsl(hue, saturation, 76%) link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 66%, 0.5) ui-background = hsla(0, 0%, 8%, 0.3) +sidebar-background = rgba(0, 0, 0, 0.2) +sidebar-opaque-background = ui-background +table-row-hover-background = hsla(0, 0%, 100%, 0.01) // White/black theme-white = bg-color @@ -26,8 +29,9 @@ tab-active-color = hsl(0, 0%, 95%) tab-active-background = hsla(0, 0%, 2%, 0.5) loading-anim-color = link-color anime-alternative-title-color = hsla(0, 0%, 100%, 0.5) +anime-list-item-name-color = text-color // Forum -post-like-color = green -post-unlike-color = rgb(255, 32, 12) -post-permalink-color = blue \ No newline at end of file +post-like-color = link-color +post-unlike-color = link-color +post-permalink-color = link-color \ No newline at end of file diff --git a/styles/input.scarlet b/styles/input.scarlet index c3bcff50..601f6a0b 100644 --- a/styles/input.scarlet +++ b/styles/input.scarlet @@ -1,8 +1,8 @@ mixin input-focus :focus - border 1px solid input-focus-border-color + border 1px solid input-focus-border-color !important // 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) !important input, textarea, button, .button, select ui-element diff --git a/styles/table.scarlet b/styles/table.scarlet index 66593695..51a79bc5 100644 --- a/styles/table.scarlet +++ b/styles/table.scarlet @@ -21,4 +21,7 @@ th tbody tr - bg-light-up \ No newline at end of file + background-color transparent + + :hover + background-color table-row-hover-background \ No newline at end of file diff --git a/styles/widgets.scarlet b/styles/widgets.scarlet index 0b5bcaba..95de3cee 100644 --- a/styles/widgets.scarlet +++ b/styles/widgets.scarlet @@ -22,7 +22,6 @@ .widget vertical width 100% - max-width 300px margin calc(content-padding / 2) overflow hidden From b0b819306bb5a66050ccd8a2cbad8f6064f69486 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 07:29:13 +0100 Subject: [PATCH 521/527] Redesign --- pages/settings/settings.pixy | 4 ++ pages/settings/settings.scarlet | 5 +- pages/shop/history.pixy | 2 +- pages/shop/shop.pixy | 6 +-- pages/shop/shop.scarlet | 16 +++++- patches/add-shop-items/add-shop-items.go | 14 +++--- styles/include/dark.scarlet | 64 ++++++++++++------------ 7 files changed, 66 insertions(+), 45 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 99cee268..2a8dfa0a 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -1,3 +1,7 @@ +component SettingsTabs + .tabs + Tab("Personal", "user", "/settings/personal") + component Settings(user *arn.User) h1.page-title Settings diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet index 5425717f..1bd383ff 100644 --- a/pages/settings/settings.scarlet +++ b/pages/settings/settings.scarlet @@ -1,5 +1,8 @@ .settings - horizontal-wrap-center + vertical + margin 0 auto + width 100% + max-width 400px .widget-section > button, .widget-section > .button diff --git a/pages/shop/history.pixy b/pages/shop/history.pixy index 0992e59f..914ed05b 100644 --- a/pages/shop/history.pixy +++ b/pages/shop/history.pixy @@ -4,7 +4,7 @@ component PurchaseHistory(purchases []*arn.Purchase, user *arn.User) h1.page-title Purchase History if len(purchases) == 0 - p.text-center.mountable You haven't bought anything yet. + p.text-center.mountable You haven't bought any items yet. else table thead diff --git a/pages/shop/shop.pixy b/pages/shop/shop.pixy index db8edda8..d0e97467 100644 --- a/pages/shop/shop.pixy +++ b/pages/shop/shop.pixy @@ -15,8 +15,8 @@ component ShopTabs(user *arn.User) Tab(strconv.Itoa(user.Balance), "diamond", "/charge") component ShopItem(item *arn.Item) - .widget.shop-item.mountable(data-item-id=item.ID) - h3.widget-title.shop-item-name + .shop-item.mountable(data-item-id=item.ID) + h3.shop-item-name .item-icon Icon(item.Icon) span= item.Name @@ -24,5 +24,5 @@ component ShopItem(item *arn.Item) .shop-item-description!= markdown.Render(item.Description) .buttons.shop-buttons button.shop-button-buy.action(data-item-id=item.ID, data-item-name=item.Name, data-price=item.Price, data-trigger="click", data-action="buyItem") - span.shop-item-price= item.Price + span.shop-item-price= "Buy for " + toString(item.Price) Icon("diamond") \ No newline at end of file diff --git a/pages/shop/shop.scarlet b/pages/shop/shop.scarlet index a99e56e2..1a70c6df 100644 --- a/pages/shop/shop.scarlet +++ b/pages/shop/shop.scarlet @@ -2,7 +2,21 @@ item-color-pro-account = hsl(0, 100%, 71%) item-color-anime-support-ticket = hsl(217, 64%, 50%) .shop-items - // ... + horizontal-wrap + justify-content space-around + +.shop-item + ui-element + flex 1 + flex-basis 380px + margin calc(content-padding / 2) + padding 0.5rem 1rem + +.shop-item-name + font-size 1.7rem + text-align center + padding 0.75rem 0 + // border-bottom 1px solid rgba(0, 0, 0, 0.1) .item-icon display inline-block diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index 8c44fd5d..4d79929b 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -7,7 +7,7 @@ var items = []*arn.Item{ ID: "pro-account-3", Name: "PRO Account (1 season)", Price: 900, - Description: `PRO account for 1 anime season (3 months). + Description: `PRO status for 1 anime season (3 months). 1 month equals 300 gems. @@ -28,9 +28,9 @@ Includes: ID: "pro-account-6", Name: "PRO Account (2 seasons)", Price: 1600, - Description: `PRO account for 2 anime seasons (6 months). + Description: `PRO status for 2 anime seasons (6 months). -11% less monthly costs compared to standard. +11% less monthly costs compared to 1 season. Includes: @@ -49,9 +49,9 @@ Includes: ID: "pro-account-12", Name: "PRO Account (4 seasons)", Price: 3000, - Description: `PRO account for 4 anime seasons (12 months). + Description: `PRO status for 4 anime seasons (12 months). -16% less monthly costs compared to standard. +16% less monthly costs compared to 1 season. Includes: @@ -70,9 +70,9 @@ Includes: ID: "pro-account-24", Name: "PRO Account (8 seasons)", Price: 5900, - Description: `PRO account for 8 anime seasons (24 months). + Description: `PRO status for 8 anime seasons (24 months). -18% less monthly costs compared to standard. +18% less monthly costs compared to 1 season. Includes: diff --git a/styles/include/dark.scarlet b/styles/include/dark.scarlet index b47a9495..b55c961d 100644 --- a/styles/include/dark.scarlet +++ b/styles/include/dark.scarlet @@ -1,37 +1,37 @@ -// Dark theme +// // Dark theme -// Main color -hue = 45 -saturation = 100% +// // Main color +// hue = 45 +// saturation = 100% -// Main -text-color = hsl(0, 0%, 90%) -bg-color = hsl(0, 0%, 24%) -link-color = hsl(hue, saturation, 66%) -link-hover-color = hsl(hue, saturation, 76%) -link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 66%, 0.5) -ui-background = hsla(0, 0%, 8%, 0.3) -sidebar-background = rgba(0, 0, 0, 0.2) -sidebar-opaque-background = ui-background -table-row-hover-background = hsla(0, 0%, 100%, 0.01) +// // Main +// text-color = hsl(0, 0%, 90%) +// bg-color = hsl(0, 0%, 24%) +// link-color = hsl(hue, saturation, 66%) +// link-hover-color = hsl(hue, saturation, 76%) +// link-hover-text-shadow = 0 0 8px hsla(hue, saturation, 66%, 0.5) +// ui-background = hsla(0, 0%, 8%, 0.3) +// sidebar-background = rgba(0, 0, 0, 0.2) +// sidebar-opaque-background = ui-background +// table-row-hover-background = hsla(0, 0%, 100%, 0.01) -// White/black -theme-white = bg-color -theme-black = text-color +// // White/black +// theme-white = bg-color +// theme-black = text-color -main-color = link-color -link-active-color = link-hover-color -button-hover-color = link-hover-color -button-hover-background = hsla(0, 0%, 12%, 0.5) -tab-background = hsla(0, 0%, 0%, 0.1) -tab-hover-background = hsla(0, 0%, 0%, 0.2) -tab-active-color = hsl(0, 0%, 95%) -tab-active-background = hsla(0, 0%, 2%, 0.5) -loading-anim-color = link-color -anime-alternative-title-color = hsla(0, 0%, 100%, 0.5) -anime-list-item-name-color = text-color +// main-color = link-color +// link-active-color = link-hover-color +// button-hover-color = link-hover-color +// button-hover-background = hsla(0, 0%, 12%, 0.5) +// tab-background = hsla(0, 0%, 0%, 0.1) +// tab-hover-background = hsla(0, 0%, 0%, 0.2) +// tab-active-color = hsl(0, 0%, 95%) +// tab-active-background = hsla(0, 0%, 2%, 0.5) +// loading-anim-color = link-color +// anime-alternative-title-color = hsla(0, 0%, 100%, 0.5) +// anime-list-item-name-color = text-color -// Forum -post-like-color = link-color -post-unlike-color = link-color -post-permalink-color = link-color \ No newline at end of file +// // Forum +// post-like-color = link-color +// post-unlike-color = link-color +// post-permalink-color = link-color \ No newline at end of file From 7cf3c2564bc4d6fb7830e784ad47acc35c0bb4ca Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 08:16:20 +0100 Subject: [PATCH 522/527] New settings page --- main.go | 11 +- pages/settings/settings.go | 18 +- pages/settings/settings.pixy | 243 +++++++++++++---------- pages/settings/settings.scarlet | 12 +- pages/shop/shop.scarlet | 2 +- patches/add-shop-items/add-shop-items.go | 8 +- tests.go | 6 + 7 files changed, 179 insertions(+), 121 deletions(-) diff --git a/main.go b/main.go index 32eb06f3..84301e33 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "github.com/aerogo/session-store-nano" "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" @@ -94,7 +95,6 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/post/:id", posts.Get) app.Ajax("/character/:id", character.Get) app.Ajax("/new/thread", newthread.Get) - app.Ajax("/settings", settings.Get) app.Ajax("/artworks", artworks.Get) app.Ajax("/amvs", amvs.Get) app.Ajax("/users", users.Active) @@ -104,6 +104,15 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/statistics/anime", statistics.Anime) app.Ajax("/login", login.Get) + // Settings + app.Ajax("/settings", settings.Get(components.SettingsPersonal)) + app.Ajax("/settings/accounts", settings.Get(components.SettingsAccounts)) + app.Ajax("/settings/notifications", settings.Get(components.SettingsNotifications)) + app.Ajax("/settings/apps", settings.Get(components.SettingsApps)) + app.Ajax("/settings/avatar", settings.Get(components.SettingsAvatar)) + app.Ajax("/settings/formatting", settings.Get(components.SettingsFormatting)) + app.Ajax("/settings/pro", settings.Get(components.SettingsPro)) + // Soundtracks app.Ajax("/soundtracks", soundtracks.Get) app.Ajax("/soundtracks/from/:index", soundtracks.From) diff --git a/pages/settings/settings.go b/pages/settings/settings.go index a7a03e0a..87c47159 100644 --- a/pages/settings/settings.go +++ b/pages/settings/settings.go @@ -4,17 +4,19 @@ import ( "net/http" "github.com/aerogo/aero" - "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/utils" ) -// Get user settings page. -func Get(ctx *aero.Context) string { - user := utils.GetUser(ctx) +// Get settings. +func Get(component func(*arn.User) string) func(*aero.Context) string { + return func(ctx *aero.Context) string { + user := utils.GetUser(ctx) - if user == nil { - return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + return ctx.HTML(component(user)) } - - return utils.AllowEmbed(ctx, ctx.HTML(components.Settings(user))) } diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 2a8dfa0a..a42106e2 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -1,9 +1,17 @@ component SettingsTabs .tabs - Tab("Personal", "user", "/settings/personal") + Tab("Personal", "user", "/settings") + Tab("Accounts", "cubes", "/settings/accounts") + Tab("Notifications", "bell", "/settings/notifications") + Tab("Apps", "puzzle-piece", "/settings/apps") + Tab("Avatar", "picture-o", "/settings/avatar") + Tab("Formatting", "font", "/settings/formatting") + Tab("PRO", "star", "/settings/pro") -component Settings(user *arn.User) - h1.page-title Settings +component SettingsPersonal(user *arn.User) + SettingsTabs + + h1.page-title Personal settings .settings .widget.mountable(data-api="/api/user/" + user.ID) @@ -15,17 +23,12 @@ component Settings(user *arn.User) InputText("Tagline", user.Tagline, "Tagline", "Text that appears below your username") InputText("Website", user.Website, "Website", "Your homepage") - .widget.mountable(data-api="/api/user/" + user.ID) - h3.widget-title - Icon("cubes") - span Accounts +component SettingsNotifications(user *arn.User) + SettingsTabs - InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co") - InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net") - InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") - InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh") - //- InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") + h1.page-title Notification settings + .settings .widget.mountable h3.widget-title Icon("bell") @@ -49,6 +52,130 @@ component Settings(user *arn.User) Icon("paper-plane") span Send test notification +component SettingsApps(user *arn.User) + SettingsTabs + + h1.page-title App settings + + .settings + .widget.mountable + h3.widget-title + Icon("puzzle-piece") + span Apps + + .widget-section + label Chrome Extension: + button.action(data-action="installExtension", data-trigger="click") + Icon("chrome") + span Get the Chrome Extension + + .widget-section + label Desktop App: + button.action(data-action="installApp", data-trigger="click") + Icon("desktop") + span Get the Desktop App + + .widget-section + label Android App: + a.button(href="https://www.youtube.com/watch?v=opyt4cw0ep8", target="_blank", rel="noopener") + Icon("android") + span Get the Android App + +component SettingsAvatar(user *arn.User) + SettingsTabs + + h1.page-title Avatar settings + + .settings + .widget.mountable(data-api="/api/settings/" + user.ID) + h3.widget-title + Icon("picture-o") + span Avatar + + .widget-section + label(for="Avatar.Source") Source: + select.widget-ui-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change") + option(value="") Automatic + option(value="Gravatar") Gravatar + option(value="URL") Link + //- option(value="FileSystem") Upload + + if user.Settings().Avatar.Source == "URL" + InputText("Avatar.SourceURL", user.Settings().Avatar.SourceURL, "Link", "Post the link to the image here") + + if user.Settings().Avatar.Source == "Gravatar" || (user.Settings().Avatar.Source == "" && user.Avatar.Source == "Gravatar") + .profile-image-container.avatar-preview + img.profile-image.mountable(src=user.Gravatar(), alt="Gravatar") + + if user.Settings().Avatar.Source == "URL" && user.Settings().Avatar.SourceURL != "" + .profile-image-container.avatar-preview + img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") + +component SettingsFormatting(user *arn.User) + SettingsTabs + + h1.page-title Formatting settings + + .settings + .widget.mountable(data-api="/api/settings/" + user.ID) + h3.widget-title + Icon("font") + span Formatting + + .widget-section + label(for="TitleLanguage")= "Title language:" + select.widget-ui-element.action(id="TitleLanguage", data-field="TitleLanguage", value=user.Settings().TitleLanguage, title="Language of anime titles", data-action="save", data-trigger="change") + option(value="canonical") Canonical + option(value="english") English + option(value="romaji") Romaji + option(value="japanese") 日本語 + + InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") + +component SettingsPro(user *arn.User) + SettingsTabs + + h1.page-title PRO settings + + .settings + .widget.mountable(data-api="/api/settings/" + user.ID) + h3.widget-title + Icon("star") + span PRO + + if user.IsPro() + .widget-section + label + span Your PRO account expires in + span.utc-date(data-date=user.ProExpires) + span . + a.button.ajax(href="/shop") + Icon("star") + span Extend PRO account duration + else + .widget-section + label Would you like to support the site development? + a.button.ajax(href="/shop") + Icon("star") + span Go PRO + +component SettingsAccounts(user *arn.User) + SettingsTabs + + h1.page-title Accounts settings + + .settings + .widget.mountable(data-api="/api/user/" + user.ID) + h3.widget-title + Icon("cubes") + span Accounts + + InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co") + InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net") + InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") + InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh") + //- InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") + .widget.mountable h3.widget-title Icon("user-plus") @@ -76,29 +203,6 @@ component Settings(user *arn.User) Icon("circle-o") span Not connected - - .widget.mountable - h3.widget-title - Icon("puzzle-piece") - span Apps - - .widget-section - label Chrome Extension: - button.action(data-action="installExtension", data-trigger="click") - Icon("chrome") - span Get the Chrome Extension - - .widget-section - label Desktop App: - button.action(data-action="installApp", data-trigger="click") - Icon("desktop") - span Get the Desktop App - - .widget-section - label Android App: - a.button(href="https://www.youtube.com/watch?v=opyt4cw0ep8", target="_blank", rel="noopener") - Icon("android") - span Get the Android App .widget.mountable h3.widget-title @@ -116,71 +220,4 @@ component Settings(user *arn.User) label JSON: a.button(href="/api/animelist/" + user.ID) Icon("upload") - span Export anime list as JSON - - .widget.mountable(data-api="/api/settings/" + user.ID) - h3.widget-title - Icon("picture-o") - span Avatar - - .widget-section - label(for="Avatar.Source") Source: - select.widget-ui-element.action(id="Avatar.Source", data-field="Avatar.Source", value=user.Settings().Avatar.Source, data-action="save", data-trigger="change") - option(value="") Automatic - option(value="Gravatar") Gravatar - option(value="URL") Link - //- option(value="FileSystem") Upload - - if user.Settings().Avatar.Source == "URL" - InputText("Avatar.SourceURL", user.Settings().Avatar.SourceURL, "Link", "Post the link to the image here") - - if user.Settings().Avatar.Source == "Gravatar" || (user.Settings().Avatar.Source == "" && user.Avatar.Source == "Gravatar") - .profile-image-container.avatar-preview - img.profile-image.mountable(src=user.Gravatar(), alt="Gravatar") - - if user.Settings().Avatar.Source == "URL" && user.Settings().Avatar.SourceURL != "" - .profile-image-container.avatar-preview - img.profile-image.mountable(src=strings.Replace(user.Settings().Avatar.SourceURL, "http://", "https://", 1), alt="Avatar preview") - - .widget.mountable(data-api="/api/settings/" + user.ID) - h3.widget-title - Icon("font") - span Formatting - - .widget-section - label(for="TitleLanguage")= "Title language:" - select.widget-ui-element.action(id="TitleLanguage", data-field="TitleLanguage", value=user.Settings().TitleLanguage, title="Language of anime titles", data-action="save", data-trigger="change") - option(value="canonical") Canonical - option(value="english") English - option(value="romaji") Romaji - option(value="japanese") 日本語 - - InputNumber("Format.RatingsPrecision", float64(user.Settings().Format.RatingsPrecision), "Ratings precision", "How many decimals after the comma would you like to display in ratings on anime pages?", "0", "2", "1") - - .widget.mountable(data-api="/api/settings/" + user.ID) - h3.widget-title - Icon("star") - span PRO - - if user.IsPro() - .widget-section - label - span Your PRO account expires in - span.utc-date(data-date=user.ProExpires) - span . - a.button.ajax(href="/shop") - Icon("star") - span Extend PRO account duration - else - .widget-section - label Would you like to support the site development? - a.button.ajax(href="/shop") - Icon("star") - span Go PRO - - //- .widget.mountable(data-api="/api/settings/" + user.ID) - //- h3.widget-title - //- Icon("cogs") - //- span Settings - - //- InputText("TitleLanguage", user.Settings().TitleLanguage, "Title language", "Language of anime titles") \ No newline at end of file + span Export anime list as JSON \ No newline at end of file diff --git a/pages/settings/settings.scarlet b/pages/settings/settings.scarlet index 1bd383ff..23a00d75 100644 --- a/pages/settings/settings.scarlet +++ b/pages/settings/settings.scarlet @@ -1,8 +1,12 @@ .settings - vertical - margin 0 auto - width 100% - max-width 400px + horizontal-wrap-center + // vertical + // margin 0 auto + // width 100% + // max-width 400px + + .widget + max-width 400px .widget-section > button, .widget-section > .button diff --git a/pages/shop/shop.scarlet b/pages/shop/shop.scarlet index 1a70c6df..8c4c25be 100644 --- a/pages/shop/shop.scarlet +++ b/pages/shop/shop.scarlet @@ -13,7 +13,7 @@ item-color-anime-support-ticket = hsl(217, 64%, 50%) padding 0.5rem 1rem .shop-item-name - font-size 1.7rem + font-size 1.6rem text-align center padding 0.75rem 0 // border-bottom 1px solid rgba(0, 0, 0, 0.1) diff --git a/patches/add-shop-items/add-shop-items.go b/patches/add-shop-items/add-shop-items.go index 4d79929b..89dd101e 100644 --- a/patches/add-shop-items/add-shop-items.go +++ b/patches/add-shop-items/add-shop-items.go @@ -13,8 +13,8 @@ var items = []*arn.Item{ Includes: -* Special highlight on the forums * Chrome extension for quick list access +* Special highlight on the forums * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions @@ -34,8 +34,8 @@ Includes: Includes: -* Special highlight on the forums * Chrome extension for quick list access +* Special highlight on the forums * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions @@ -55,8 +55,8 @@ Includes: Includes: -* Special highlight on the forums * Chrome extension for quick list access +* Special highlight on the forums * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions @@ -76,8 +76,8 @@ Includes: Includes: -* Special highlight on the forums * Chrome extension for quick list access +* Special highlight on the forums * Access to the VIP channel on Discord * PRO star on your profile * High priority for your personal suggestions diff --git a/tests.go b/tests.go index 00c691be..ff45fb35 100644 --- a/tests.go +++ b/tests.go @@ -251,6 +251,12 @@ var routeTests = map[string][]string{ "/dark-flame-master": nil, "/user": nil, "/settings": nil, + "/settings/accounts": nil, + "/settings/notifications": nil, + "/settings/apps": nil, + "/settings/avatar": nil, + "/settings/formatting": nil, + "/settings/pro": nil, "/shop": nil, "/shop/history": nil, "/charge": nil, From 5801b3f4e11a657791953b78d531b3c6ef6dbf71 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 09:32:46 +0100 Subject: [PATCH 523/527] Redesign --- pages/admin/purchases.pixy | 2 +- pages/anime/anime.go | 21 ++++++++------- pages/anime/anime.pixy | 7 ++--- pages/anime/episodes.go | 2 +- pages/anime/episodes.pixy | 53 +++++++++++++++++++------------------- pages/search/search.go | 4 +-- pages/search/search.pixy | 32 +++++++++++------------ pages/shop/history.pixy | 2 +- pages/shop/shop.scarlet | 2 +- scripts/AnimeNotifier.ts | 2 +- styles/navigation.scarlet | 4 +-- 11 files changed, 66 insertions(+), 65 deletions(-) diff --git a/pages/admin/purchases.pixy b/pages/admin/purchases.pixy index fff793d6..e66e02b6 100644 --- a/pages/admin/purchases.pixy +++ b/pages/admin/purchases.pixy @@ -14,7 +14,7 @@ component GlobalPurchaseHistory(purchases []*arn.Purchase) th.history-date Date tbody each purchase in purchases - tr.shop-item.mountable(data-item-id=purchase.ItemID) + tr.shop-history-item.mountable(data-item-id=purchase.ItemID) td a.ajax(href=purchase.User().Link())= purchase.User().Nick PurchaseInfo(purchase) \ No newline at end of file diff --git a/pages/anime/anime.go b/pages/anime/anime.go index 71ea06d4..5ee3d120 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -10,8 +10,8 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -// const maxEpisodes = 26 -// const maxEpisodesLongSeries = 5 +const maxEpisodes = 26 +const maxEpisodesLongSeries = 10 const maxDescriptionLength = 170 // Get anime page. @@ -24,16 +24,17 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime not found", err) } + episodes := anime.Episodes().Items // episodesReversed := false - // if len(anime.Episodes().Items) > maxEpisodes { - // episodesReversed = true - // anime.Episodes().Items = anime.Episodes().Items[len(anime.Episodes().Items)-maxEpisodesLongSeries:] + if len(episodes) > maxEpisodes { + // episodesReversed = true + episodes = episodes[len(episodes)-maxEpisodesLongSeries:] - // for i, j := 0, len(anime.Episodes().Items)-1; i < j; i, j = i+1, j-1 { - // anime.Episodes().Items[i], anime.Episodes().Items[j] = anime.Episodes().Items[j], anime.Episodes().Items[i] - // } - // } + for i, j := 0, len(episodes)-1; i < j; i, j = i+1, j-1 { + episodes[i], episodes[j] = episodes[j], episodes[i] + } + } // Friends watching var friends []*arn.User @@ -113,5 +114,5 @@ func Get(ctx *aero.Context) string { ctx.Data = openGraph - return ctx.HTML(components.Anime(anime, tracks, friends, friendsAnimeListItems, user)) + return ctx.HTML(components.Anime(anime, tracks, episodes, friends, friendsAnimeListItems, user)) } diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 78134249..8d4460cb 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,11 +1,11 @@ -component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) +component Anime(anime *arn.Anime, tracks []*arn.SoundTrack, episodes []*arn.AnimeEpisode, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) .anime .anime-main-column - AnimeMainColumn(anime, tracks, user) + AnimeMainColumn(anime, tracks, episodes, user) .anime-side-column AnimeSideColumn(anime, friends, listItems, user) -component AnimeMainColumn(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn.User) +component AnimeMainColumn(anime *arn.Anime, tracks []*arn.SoundTrack, episodes []*arn.AnimeEpisode, user *arn.User) .anime-header(data-id=anime.ID) if anime.Image.Large != "" .anime-image-container.mountable @@ -50,6 +50,7 @@ component AnimeMainColumn(anime *arn.Anime, tracks []*arn.SoundTrack, user *arn. AnimeCharacters(anime) AnimeRelations(anime, user) AnimeTracks(anime, tracks) + AnimeEpisodes(episodes) //- //- h3.anime-section-name Reviews //- //- p Coming soon. diff --git a/pages/anime/episodes.go b/pages/anime/episodes.go index fe2b097d..901d3b75 100644 --- a/pages/anime/episodes.go +++ b/pages/anime/episodes.go @@ -19,5 +19,5 @@ func Episodes(ctx *aero.Context) string { return ctx.Error(http.StatusNotFound, "Anime not found", err) } - return ctx.HTML(components.AnimeEpisodes(anime)) + return ctx.HTML(components.AnimeEpisodes(anime.Episodes().Items)) } diff --git a/pages/anime/episodes.pixy b/pages/anime/episodes.pixy index bc8fe58b..46223df3 100644 --- a/pages/anime/episodes.pixy +++ b/pages/anime/episodes.pixy @@ -1,27 +1,26 @@ -component AnimeEpisodes(anime *arn.Anime) - AnimeTabs(anime) - - h3.anime-section-name Episodes - - table.episodes - tbody - each episode in anime.Episodes().Items - tr.episode.mountable - td.episode-number - if episode.Number != -1 - span= episode.Number - td.episode-title - if episode.Title.Japanese != "" - Japanese(episode.Title.Japanese) - else - span - - td.episode-actions - for name, link := range episode.Links - a(href=link, target="_blank", rel="noopener", title="Watch episode " + toString(episode.Number) + " on " + name) - RawIcon("eye") - //- a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") - //- RawIcon("google") - if validator.IsValidDate(episode.AiringDate.Start) - td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() - else - td.episode-airing-date-start \ No newline at end of file +component AnimeEpisodes(episodes []*arn.AnimeEpisode) + if len(episodes) > 0 + .anime-section + h3.anime-section-name Episodes + table.episodes + tbody + each episode in episodes + tr.episode.mountable + td.episode-number + if episode.Number != -1 + span= episode.Number + td.episode-title + if episode.Title.Japanese != "" + Japanese(episode.Title.Japanese) + else + span - + td.episode-actions + for name, link := range episode.Links + a(href=link, target="_blank", rel="noopener", title="Watch episode " + toString(episode.Number) + " on " + name) + RawIcon("eye") + //- a(href="https://translate.google.com/#ja/en/" + episode.Title.Japanese, target="_blank", rel="noopener") + //- RawIcon("google") + if validator.IsValidDate(episode.AiringDate.Start) + td.episode-airing-date-start.utc-airing-date(data-start-date=episode.AiringDate.Start, data-end-date=episode.AiringDate.End, data-episode-number=episode.Number)= episode.AiringDate.StartDateHuman() + else + td.episode-airing-date-start \ No newline at end of file diff --git a/pages/search/search.go b/pages/search/search.go index 67298913..d454f76b 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -6,8 +6,8 @@ import ( "github.com/animenotifier/notify.moe/components" ) -const maxUsers = 6 * 6 -const maxAnime = 5 * 6 +const maxUsers = 36 +const maxAnime = 26 const maxPosts = 3 const maxThreads = 3 diff --git a/pages/search/search.pixy b/pages/search/search.pixy index caae73aa..ad630b01 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -2,20 +2,6 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim h1.page-title= "Search: " + term .search - .widget - h3.widget-title - Icon("user") - span Users - - .user-avatars.user-search - if len(users) == 0 - p.no-search-results.mountable No users found. - else - each user in users - .mountable(data-mountable-type="user") - Avatar(user) - //- a.ajax(href=user.Link())= user.Nick - .widget h3.widget-title Icon("tv") @@ -28,7 +14,7 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim each anime in animeResults a.profile-watching-list-item.mountable.ajax(href=anime.Link(), title=anime.Title.Canonical, data-mountable-type="anime") img.anime-cover-image.anime-search-result(src=anime.Image.Tiny, alt=anime.Title.Canonical) - + .widget h3.widget-title Icon("comment") @@ -58,4 +44,18 @@ component SearchResults(term string, users []*arn.User, animeResults []*arn.Anim Icon("music") span Soundtracks - p.no-search-results.mountable Soundtrack search coming soon. \ No newline at end of file + p.no-search-results.mountable Soundtrack search coming soon. + + .widget + h3.widget-title + Icon("user") + span Users + + .user-avatars.user-search + if len(users) == 0 + p.no-search-results.mountable No users found. + else + each user in users + .mountable(data-mountable-type="user") + Avatar(user) + //- a.ajax(href=user.Link())= user.Nick \ No newline at end of file diff --git a/pages/shop/history.pixy b/pages/shop/history.pixy index 914ed05b..31288ae6 100644 --- a/pages/shop/history.pixy +++ b/pages/shop/history.pixy @@ -16,7 +16,7 @@ component PurchaseHistory(purchases []*arn.Purchase, user *arn.User) th.history-date Date tbody each purchase in purchases - tr.shop-item.mountable(data-item-id=purchase.ItemID) + tr.shop-history-item.mountable(data-item-id=purchase.ItemID) PurchaseInfo(purchase) component PurchaseInfo(purchase *arn.Purchase) diff --git a/pages/shop/shop.scarlet b/pages/shop/shop.scarlet index 8c4c25be..214f4cab 100644 --- a/pages/shop/shop.scarlet +++ b/pages/shop/shop.scarlet @@ -22,7 +22,7 @@ item-color-anime-support-ticket = hsl(217, 64%, 50%) display inline-block // Colors -.shop-item, .inventory-slot +.shop-item, .inventory-slot, .shop-history-item [data-item-id="pro-account-3"] .item-icon color item-color-pro-account diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index f669dd98..79b42b42 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -269,7 +269,7 @@ export class AnimeNotifier { } async updatePushUI() { - if(!this.pushManager.pushSupported || !this.app.currentPath.includes("/settings")) { + if(!this.pushManager.pushSupported || !this.app.currentPath.includes("/settings/notifications")) { return } diff --git a/styles/navigation.scarlet b/styles/navigation.scarlet index 4bf731d1..992962d5 100644 --- a/styles/navigation.scarlet +++ b/styles/navigation.scarlet @@ -43,8 +43,8 @@ #search background transparent - border none - box-shadow none + border none !important + box-shadow none !important font-size 1em padding 0 width 0 From 801e36d039f33da3c75ccc999140af3a3f16c2c5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 09:55:42 +0100 Subject: [PATCH 524/527] Improved search results --- pages/anime/anime.pixy | 51 ++++++++++++++++++++----------------- pages/anime/anime.scarlet | 19 +++++++------- pages/search/search.scarlet | 3 +++ 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 8d4460cb..eb1b914e 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -29,24 +29,10 @@ component AnimeMainColumn(anime *arn.Anime, tracks []*arn.SoundTrack, episodes [ //- h3.anime-section-name.anime-summary-header Summary p.anime-summary.mountable= anime.Summary + //- AnimeActions(anime, user) + //- AnimeTabs(anime) - if user != nil - .buttons.anime-actions - if user.Role == "editor" || user.Role == "admin" - a.button.ajax(href=anime.Link() + "/edit") - Icon("pencil-square-o") - span Edit anime - - if user.AnimeList().Contains(anime.ID) - a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) - Icon("pencil") - span Edit in collection - else - button.action(data-api="/api/animelist/" + user.ID, data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID) - Icon("plus") - span Add to collection - AnimeCharacters(anime) AnimeRelations(anime, user) AnimeTracks(anime, tracks) @@ -58,6 +44,31 @@ component AnimeMainColumn(anime *arn.Anime, tracks []*arn.SoundTrack, episodes [ //- .footer //- span Powered by Kitsu. +component AnimeSideColumn(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) + AnimeTrailer(anime) + AnimeInformation(anime) + AnimeRatings(anime, user) + AnimePopularity(anime) + AnimeFriends(friends, listItems) + AnimeLinks(anime) + +component AnimeActions(anime *arn.Anime, user *arn.User) + if user != nil + .buttons.anime-actions + //- if user.Role == "editor" || user.Role == "admin" + //- a.button.ajax(href=anime.Link() + "/edit") + //- Icon("pencil-square-o") + //- span Edit anime + + if user.AnimeList().Contains(anime.ID) + a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) + Icon("pencil") + span Edit in collection + else + button.action(data-api="/api/animelist/" + user.ID, data-action="addAnimeToCollection", data-trigger="click", data-anime-id=anime.ID) + Icon("plus") + span Add to collection + component AnimeRatings(anime *arn.Anime, user *arn.User) section.anime-section.mountable h3.anime-section-name Ratings @@ -134,14 +145,6 @@ component AnimeLinks(anime *arn.Anime) Icon("external-link") span= mapping.Name() -component AnimeSideColumn(anime *arn.Anime, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, user *arn.User) - AnimeTrailer(anime) - AnimeInformation(anime) - AnimeRatings(anime, user) - AnimePopularity(anime) - AnimeFriends(friends, listItems) - AnimeLinks(anime) - component AnimeRelations(anime *arn.Anime, user *arn.User) if anime.Relations() != nil && len(anime.Relations().Items) > 0 section.anime-section.mountable diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 17668ebe..491ca5ea 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -125,21 +125,20 @@ color anime-alternative-title-color !important .anime-actions - display none !important - // horizontal - // justify-content center + horizontal + justify-content flex-end - // // Action button margin + // Action button margin // margin calc(content-padding - 0.5rem) -0.5rem - // // Setting z-index requires setting a background as well + // Setting z-index requires setting a background as well // z-index 10 -> 1450px - .anime-actions - position absolute - bottom 0 - right content-padding +// > 1450px +// .anime-actions +// position fixed +// bottom 0 +// right content-padding // .anime-rating-categories // horizontal diff --git a/pages/search/search.scarlet b/pages/search/search.scarlet index 9a609128..216d8a7c 100644 --- a/pages/search/search.scarlet +++ b/pages/search/search.scarlet @@ -1,3 +1,6 @@ +.anime-search + justify-content flex-start + .anime-search-result width 55px !important height 78px !important From ed9031d68813daf5a6d9b0c14eabc8c4b651c20e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 11:02:31 +0100 Subject: [PATCH 525/527] Redesign --- pages/anime/anime.pixy | 16 +++++++--------- pages/anime/anime.scarlet | 14 +++++++++++++- pages/anime/episodes.pixy | 4 ++-- pages/search/search.scarlet | 3 ++- pages/soundtracks/soundtracks.scarlet | 4 ++++ 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index eb1b914e..9429f016 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -28,10 +28,8 @@ component AnimeMainColumn(anime *arn.Anime, tracks []*arn.SoundTrack, episodes [ //- h3.anime-section-name.anime-summary-header Summary p.anime-summary.mountable= anime.Summary - - //- AnimeActions(anime, user) - - //- AnimeTabs(anime) + + AnimeActions(anime, user) AnimeCharacters(anime) AnimeRelations(anime, user) @@ -54,11 +52,11 @@ component AnimeSideColumn(anime *arn.Anime, friends []*arn.User, listItems map[* component AnimeActions(anime *arn.Anime, user *arn.User) if user != nil - .buttons.anime-actions - //- if user.Role == "editor" || user.Role == "admin" - //- a.button.ajax(href=anime.Link() + "/edit") - //- Icon("pencil-square-o") - //- span Edit anime + .buttons.anime-actions.mountable + if user.Role == "editor" || user.Role == "admin" + a.button.ajax(href=anime.Link() + "/edit") + Icon("pencil-square-o") + span Edit anime if user.AnimeList().Contains(anime.ID) a.button.ajax(href="/+" + user.Nick + "/animelist/anime/" + anime.ID) diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 491ca5ea..be76338c 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -29,6 +29,8 @@ > 800px .anime-header horizontal + padding-bottom content-padding + border-bottom 1px solid rgba(0, 0, 0, 0.05) .anime-title text-align left @@ -36,6 +38,16 @@ .anime-alternative-title text-align left + .anime-actions + flex 1 + justify-content flex-end + align-items flex-end + + button, + .button + margin-right 0 + margin-bottom 0 + .anime-info-table margin 0 // width 100% @@ -126,7 +138,7 @@ .anime-actions horizontal - justify-content flex-end + justify-content center // Action button margin // margin calc(content-padding - 0.5rem) -0.5rem diff --git a/pages/anime/episodes.pixy b/pages/anime/episodes.pixy index 46223df3..5b149e8b 100644 --- a/pages/anime/episodes.pixy +++ b/pages/anime/episodes.pixy @@ -1,11 +1,11 @@ component AnimeEpisodes(episodes []*arn.AnimeEpisode) if len(episodes) > 0 - .anime-section + .anime-section.mountable h3.anime-section-name Episodes table.episodes tbody each episode in episodes - tr.episode.mountable + tr.episode.mountable(data-mountable-type="episode") td.episode-number if episode.Number != -1 span= episode.Number diff --git a/pages/search/search.scarlet b/pages/search/search.scarlet index 216d8a7c..89befb2f 100644 --- a/pages/search/search.scarlet +++ b/pages/search/search.scarlet @@ -1,4 +1,5 @@ -.anime-search +.anime-search, +.user-search justify-content flex-start .anime-search-result diff --git a/pages/soundtracks/soundtracks.scarlet b/pages/soundtracks/soundtracks.scarlet index e9cab0eb..d61517ee 100644 --- a/pages/soundtracks/soundtracks.scarlet +++ b/pages/soundtracks/soundtracks.scarlet @@ -35,6 +35,10 @@ .sound-track-anime-image max-width 142px +.music-buttons + display flex + justify-content center + > 600px .music-buttons position absolute From 7cce904a32dda625367d51ccfda18a5cc6e65b48 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 13:47:51 +0100 Subject: [PATCH 526/527] Changed sidebar min res to 1350px --- pages/anime/anime.scarlet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index be76338c..82d38b6d 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -11,7 +11,7 @@ margin-top 1rem flex-basis 300px -> 1400px +> 1350px .anime horizontal From 331da5044e8f56f41cee676e58ca7415c61b464a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 5 Nov 2017 14:07:04 +0100 Subject: [PATCH 527/527] Fixed statistics pages --- pages/profile/stats.scarlet | 6 ++++++ pages/statistics/statistics.scarlet | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 pages/profile/stats.scarlet diff --git a/pages/profile/stats.scarlet b/pages/profile/stats.scarlet new file mode 100644 index 00000000..374457bf --- /dev/null +++ b/pages/profile/stats.scarlet @@ -0,0 +1,6 @@ +.stats + horizontal-wrap + justify-content space-around + + .widget + max-width 300px \ No newline at end of file diff --git a/pages/statistics/statistics.scarlet b/pages/statistics/statistics.scarlet index 9416327d..c14c60f6 100644 --- a/pages/statistics/statistics.scarlet +++ b/pages/statistics/statistics.scarlet @@ -1,7 +1,11 @@ .statistics - horizontal-wrap-center + horizontal-wrap + justify-content space-around text-align center + .widget + max-width 300px + .pie-chart transform rotate(-90deg)

    O|^%f>~& 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%0D)Y=t#&(@und#h1e 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 diff --git a/images/brand/600.webp b/images/brand/600.webp deleted file mode 100644 index a53a1dd01fbe8b63db2ed5c72f6d03a856327941..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99612 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