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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 z zW#2IZ$SF~O0ZQQ``_I5jt%Kh7a-UPyQcLQvNqNPW~hQ^?zzn`TxJcgkRVC^OdvbovQY%4ksetA?UC?UJWJtEOiAfL+#vGi*X{FTFjPP;TPt5i0JW;41&13A zcHZ(P5e*dm&74>bv-aHQ;_R31u70R>9f6s0sJ7$jRP##8ssd+yM(8q6FO-CzKrr|( z75*otT95fN zRb(VGB|(EXB8Q=kza**DqBmx>n`uq%gQn4SH*UuzMU9tkPrl1V8)dcCeidJM0-Z}L zbw98UlyCHJkCX`}=gZqRt)s)QrpmQJJeOH&-I-^=D|RA_^=I7UGjw~yYuxlnn&&-H zdXVR@2pu5=JIRqdHH)}uU9e*2drqdloZB$LO55og4aV%i$E;#3-H)NUv-_><;qt7z z_G2w9hR3=7pR?s6pT-+5nYq^moA0K{_A>4ijB*t;2eX@&q6V&0Jp!t;JI5?%;33Mt zM4a*3P4L3LKvmwOgg!AHBziA(#S;teCm>kVvf34nwd$i*cU5BoN5f9Zfx0B*p@n;V zm`Ar7yG};6Ok}a2kjO7V|DFZGb?`Wq!;{_v%ZsQG9^w8O#-VzCawL`N?S9Su!S%?5 z%r%wyf!G{>Aa{CbOaXURUoezV)r!`3+Ce>2bA9&UVZ_0{m7;n#DuBH20r)LXK{o5i z!cI<7hA`A0T)i_KKW)m9+2;`C5P-_beFZ`C z0s$W5Pv{NUF$DKx2R=J@A;L|QZ0>3VrqSrWXJP2OarK`^M@2obiBq&2FOeC3Dd!!e z@HoY7DV~`^p}I8@k~&K}W2*ZGt{U+6;_R4Ml+jN)fmiM#dKp!pzZ#a-Z^gjG2+1TQ zfEkm$HZHe(e>wHBRSMoT4pZ$1j5;tGOx7SGpAfXavEKI0%7!RpO&h86`R$NHz0g^W z&&#W}Nyj1NI^!g5NSHCL&DR_^-1%UjYxJiIqYL`u@&J~<*_=^1yNcPJ&4$q~kOCIo zJwm`sNKX?Ulxd(C6R5SLtC^DhHVtd>)eAX4!*GX}rirA7taW?nZUF1cy9X9)QCh*z8n7C?AI9ko3m;$vkp=n)foE|ed12-P~-WrTxMI(eCBz$>w^X8HI92= zH3gZ|AQhzozdN^DcG^JSQ2M+Bs@{;qlz9OD=IK9;8I)urc8YoBp`rkMT&A9@34Cxc4V$bB_4hAi=eBjtayq2ZsWu4Lz{_%rih z_^(sJYdBQYbNC!mQ)+*K4@J%*+4) literal 50821 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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$ytP{I8)hHpgslr(N6MGj_f{+rTpFFkM#2xM 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%0AK>f#eU~!z6f)PttAuT0oK|7|T>?HoC~l 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�U0Q5?ekW?XzQqGt_q(q03Mtbs(*$h`a! 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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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

W@O!HN#qYeMgiFlvGnbW4{b&x?mpMf zg&7jzkPR{fZ~y!T=Lj1|dceGYCL(SlS;U09xudIDdohLLB*Qu~!LU#+v`C~e&;

PYb%``}?W zcA_TyCREANUzadq+mAEoL`Y%IkSAKmzT5LxqD5m)9kZyzTK% z^Z))K2{DrsZ?R(4I^wO$k}BqlgMQ)392)g}*sS=>^5SNOhEsBFBGa5qOMnr1edVRd z04Q)aN${|S0cg*>FDToCj~t<;eQ44K+x=s|n5H`54;Vyk>%?qZ+F%q$n&@g1ZS&l< z5Ui}Mu0xB`yr%OwjQ+R`ON;NdN(vg9saeQo>|A}GR37_(*^$GmmF9@8R-OWyqTuKo zX9XS&5U|jXYKKV0q147zm)khb zReZ$@uBQ(c3V9AAUdpYgQXJ<}U537LRkVPt*zV*Jm4`(nmR%;!91^81itQ6C19je* zUf8S9Z~KB>+$kZReE^n(Yp1kb<3QCrNM1ZAnCk0V3|t%BV~^~Ql|s|*zROjKTkZ$A z0Cv%i_k0wODjN{64T;b7J(8^}4uw<9p%;OB7uX;#WQ^bQ^5eQ9jHg$|o4%cc?1s(g zJeC}ynG}gMk^?F+3Lw;>z30j?VUJ8DK!Bz#S?Wo~$*|obQsCM8jE_25X+}xL%U|V$ z7=U)o7rfEK;{$)q_^dB6kbB&=(y$q9tb=*Tbm@)+&x7k1n#)2C8rm`tnTZv62JzRp z56#5%0O9`M6dRa}b+)41=)qYH)Tp4Us;>F8ou-Lp7c2I}x|(6DH+Jb+Pr5r7eg$eI z5=^p1k0m}xk>A=rzc^>rP!xEkP%yGTl&PQm43w@Bc`2N%3a33pwE82Yd_|_KR(;5G z%&3mXqrEzUm0fZTo;FDa$K}@n$EY;Q8c0zWcfSp>d^^kqJYn4fKloF0pL{SFeBmbk zG2WxV=t+O44pwuT&&9Qom0Nh^WM+|Z&g#~aFk|d&V4=Ng)$fkN&mhtc6G`k2x!j(> zP_sIA$z=N_96V{YANpxzl^@M+AGzcSL4-36W$lE;0xPvbFy)=`FnDCWF!?YUBcDEv KsMlWDfB*n|QbbGu 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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�Lox+uRFJ`X8H5yMq=ifdJRd6^$OPD$ zMoKq*Nku@SG35#~Gjkk1aDX+NHbPCOA6K=odRsu3aNK`_)Dv{K2ae)uTJm=8}4SvpLyd1G(Q$I3r zkhX@2lREG+byyJB@T9_RfSLqVXk^(n+n6i~N)yva<)L*Ixmcm*x@0?-GdMW(z(PaQ z>w_JQR4`l^ou{{}z~JBzyB~h^;(a%LES{J?$CjIRSC(IS_1!v34t1L`WKC_Zh-5qw zNq}$q&+k9D@50E5=H{j>XJKTq=Z8JE-nd$%RQV96asoJU!_FT+-*sunjc1P!Km5$^hI&?XHL?1#4wfz}&=9)h ztj3m7I*1h@U4>C;C8=64%ER@uWc(1}IMk$rCmpKeQ#||hpXgrEOGi@yU5>DxlcsMW zwKP&!VTu1*9n%|R5jFv>@qLfVLYdIZ;uvL8*rU~`dQYJem&t`8CMuIDG*g|tz>crb63sy;*!ef7-3u`i6dH?8tH5;(il1j z6XUv;0Em(@I3*^_Wh{cwEl{aOyz=rOp@pq0)?&|3K|Y(N9h`LhW7UUSh9(UmN`WEW pXcX4qXvj!~sm3(=KDDIAe*l_}+4pQz6QckC002ovPDHLkV1fs1)P4W} literal 49182 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 cNo*y4P9S5PJaQxFS)$E|EVAIdq-oOT_kTqH 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

W@O!HN#qYeMgiFlvGnbW4{b&x?mpMf zg&7jzkPR{fZ~y!T=Lj1|dceGYCL(SlS;U09xudIDdohLLB*Qu~!LU#+v`C~e&;

PYb%``}?W zcA_TyCREANUzadq+mAEoL`Y%IkSAKmzT5LxqD5m)9kZyzTK% z^Z))K2{DrsZ?R(4I^wO$k}BqlgMQ)392)g}*sS=>^5SNOhEsBFBGa5qOMnr1edVRd z04Q)aN${|S0cg*>FDToCj~t<;eQ44K+x=s|n5H`54;Vyk>%?qZ+F%q$n&@g1ZS&l< z5Ui}Mu0xB`yr%OwjQ+R`ON;NdN(vg9saeQo>|A}GR37_(*^$GmmF9@8R-OWyqTuKo zX9XS&5U|jXYKKV0q147zm)khb zReZ$@uBQ(c3V9AAUdpYgQXJ<}U537LRkVPt*zV*Jm4`(nmR%;!91^81itQ6C19je* zUf8S9Z~KB>+$kZReE^n(Yp1kb<3QCrNM1ZAnCk0V3|t%BV~^~Ql|s|*zROjKTkZ$A z0Cv%i_k0wODjN{64T;b7J(8^}4uw<9p%;OB7uX;#WQ^bQ^5eQ9jHg$|o4%cc?1s(g zJeC}ynG}gMk^?F+3Lw;>z30j?VUJ8DK!Bz#S?Wo~$*|obQsCM8jE_25X+}xL%U|V$ z7=U)o7rfEK;{$)q_^dB6kbB&=(y$q9tb=*Tbm@)+&x7k1n#)2C8rm`tnTZv62JzRp z56#5%0O9`M6dRa}b+)41=)qYH)Tp4Us;>F8ou-Lp7c2I}x|(6DH+Jb+Pr5r7eg$eI z5=^p1k0m}xk>A=rzc^>rP!xEkP%yGTl&PQm43w@Bc`2N%3a33pwE82Yd_|_KR(;5G z%&3mXqrEzUm0fZTo;FDa$K}@n$EY;Q8c0zWcfSp>d^^kqJYn4fKloF0pL{SFeBmbk zG2WxV=t+O44pwuT&&9Qom0Nh^WM+|Z&g#~aFk|d&V4=Ng)$fkN&mhtc6G`k2x!j(> zP_sIA$z=N_96V{YANpxzl^@M+AGzcSL4-36W$lE;0xPvbFy)=`FnDCWF!?YUBcDEv KsMlWDfB*n|QbbGu 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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 rRhp3fP(qu5 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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 rRhp3fP(qu5 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 zcmV)bK&iipP)uJ@VVD_UC<6{NG_fI~0ue<-1QkJoA_k0xBC#Thg@9ne9*`iQ#9$Or zQF$}6R&?d%y_c8YA7_1QpS|}zXYYO1x&V;8{kgn!SPFnNo`4_X6{c}T{8k*B#$jdxfFg<9uYy1K45IaYvHg`_dOZM)Sy63ve6hvv z1)yUy0P^?0*fb9UASvow`@mQCp^4`uNg&9uGcn1|&Nk+9SjOUl{-OWr@Hh0;_l(8q z{wNRKos+;6rV8ldy0Owz(}jF`W(JeRp&R{qi2rfmU!TJ;gp(Kmm5I1s5m_f-n#TRsj}B0%?E`vOzxB2#P=n*a3EfYETOrKoe*ICqM@{4K9Go;5xVgZi5G4 z1dM~{UdP6d+Yd3o?MrAqM0Kc|iV92owdyL5UC#5<>aVCa44|hpM4E zs0sQWIt5*Tu0n&*J!lk~f_{hI!w5`*sjxDv4V%CW*ah~3!{C*0BD@;TgA3v9a1~q+ zAA{TB3-ERLHar49hi4Ih5D^-ph8Q6X#0?2VqLBoIkE}zAkxHZUgRb+f=nat zP#6>iMMoK->`~sRLq)(kHo*Vn{;LcG6+edD1=7D>9j^O?D{Qg|tCDK{ym)H7&wDr6*;uGTJg8GHjVbnL{!cWyUB7MT6o-VNo_w8Yq`2<5Ub)hw4L3rj}5@qxMs0 zWMyP6Wy582WNT#4$d1qunl{acmP#w5ouJ*Jy_Zv#bCKi7ZIf$}8d zZdVy&)LYdbX%I9R8VMQ|8r>Q*nyQ)sn)#Z|n)kKvS`4iu ztvy=3T65Yu+7a4Yv^%sXb>ww?bn(=Yu(!=O6^iuTp>)p_Y^{w=i z^lS773}6Fm1Fpe-gF!>Ip{*g$u-szvGhed;vo5pW&GpS$<~8QGEXWp~7V9lKEnZq0SaK{6Sl+dwSOr*Z zvFf(^Xl-N7w{EeXveC4Ov)N}e%%C!Y7^RFWwrE>d+x51mZQt2h+X?JW*!^a2WS?Sx z)P8cQ&Qi|OhNWW;>JChYI)@QQx?`Nj^#uJBl~d&PK+RZLOLos~K(b5>qmrMN0})tOkySZ3_W zICNY@+|jrX%s^&6b2i>5eqa0y%Z;^%^_=a@u3%4b9605ii3Ep)@`TAmhs0fpQ%O!q zl}XcFH*PieWwLj2ZSq`7V9Mc?h17`D)-+sNT-qs~3@?S(ldh7UlRlVXkWrK|vf6I- z?$tAVKYn8-l({mqQ$Q8{O!WzMg`0(=S&msXS#Pt$vrpzo=kRj+a`kh!z=6$;c zwT88(J6|n-WB%w`m$h~4pmp)YIh_ z3ETV2tjiAU!0h1dxU-n=E9e!)6|Z;4?!H=SSy{V>ut&IOq{_dl zbFb#!9eY1iCsp6Bajj|Hr?hX|zPbJE{X++w546-O*Ot`2Kgd0Jx6Z4syT zu9enWavU5N9)I?I-1m1*_?_rJ$vD~agVqoG+9++s?NEDe`%Fht$4F;X=in*dQ{7$m zU2Q)a|9JSc+Uc4zvS-T963!N$T{xF_ZuWe}`RNOZ7sk3{yB}PPym+f8xTpV;-=!;; zJuhGEb?H5K#o@~7t9DmUU1MD9xNd#Dz0azz?I)|B+WM{g+Xrk0I&awC=o(x)cy`EX z=)z6+o0o6-+`4{y+3mqQ%kSJBju{@g%f35#FZJHb`&swrA8dGtepviS>QUumrN{L@ z>;2q1Vm)$Z)P1z?N$8UYW2~{~zhwUMVZ87u`Dx{Z>O|9|`Q+&->FRy-Sjp7DHs zy69KwU-!MxeeuI@&cF4|M9z%AfP?@5 z`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u00v@9M??Vs0RI60puMM)00009a7bBm z000XU000XU0RWnu7ytkO2XskIMF-&v1QQ@9$B;-N001BWNklFbI$k!DKENFqrFWjIYzXw&6atQ6<)P72Vc zEP&yc@nwu5HI@gz`y{V^$5qrS4WxAIVFd{Li@kpHHRLaPqZSP)X9U6k*_~O5wH9N{ zTx0P)&?$YJt|8afOUKHM1jP~}^eC^n5W+mAWoyw|BjmX~rIp}$9*Ito%oyYg8Et9} zZEbCgjZ9&Ed5*TG`Ty%n2tlbB&^6G({%uE5zSnx#Uv%?f#(;id18^&+XGH_&VZu72 z(Ac@2ni~9$PE@H4s|{9IL=b@QA%MUONhZf%+QS%7p1^8@wlfQO>VnSku*A@_u!G}= zP7-+PMU7^Tw+l0K{SpnJ-blFc%GEsk_->>xFvhf^-OKeQU%p#*iztM^7y;5Pj!X@x znZ-9YMdiwXL<);A786D2T8*h)&oaGl8&kWtpp?QIaqc!K1yMDoX#uk%Pt*A3dVnFH z57AmvkK(f%%P(=0Uvx!&${r+ z3Lrrki#A{yRnp27$`7a?I>h+i?MUIB^ZDO$NFhiPLy{yfc>XsHMxaPjL;qkGC-$Ah z^JMFHt$UvRBH4diwEMl4ztqjQ*|sE&l+_!TvhVq$SfN`^_OeER*)M1WoQvZ%|IO_0 z&4o+TnN2rZ&sjt(71884X<9}39@B^Sx(7=QD2bB(84c%LzWlsLtm@y;5@>B{Z!a-EJcUvME6s}@ z#?zi9=} zJh+{pBV` zXe%g{15O?ppK~0Y$@^MhKKr|3XP&syAcCZ6;VdlKP6`+MZ?S0J42otnP zSg^dG@e`B8)wl(aXWz$5H7+3pk}$+8bWmKh2$Q6se*O)qRT`W;Hi6c9&e@r!=@~br z`6uS*NNi_6tT14;B*^(R#wt`Mt4MhcULp(%Ns_?evUwcXaU9=wfOa;Hu&3nzP0qt5 zx!OUgvtfl_FhtLeu2DTD?uk8wc{z6@&CmC~(l)A~AQ_)<9;(Gl1~@_pq=Jc& z3RC0LL{WkeVuk}qW4B4ChI-Sy3GqVDer{)})F?lss1(EdPN0-}L649YtdK4nNn(Z; z_j2^m2vUY_Q>{ekxuDP36k%p%0nJOxIT1q2m%;T<-2*GpS~G8HFXP81F~(qwX*t$Q zjW*4HC&!+Al+h<2rhe?`%!0kd6vtYN@B54%7{)YG>JwF@nt7w7-ZOj(XinLNe%F{v8OL;M&JPzzV4pK5z zse=?qDX~Hj)#B5LnNv5s`CXXPBf>d{Gf&~Qg`{IOs?*bR2&m?!pZ@+1u*Ol!vhx-gTk-b+vnFd7Wmc`pTc64Bg(9k3c}E*QK=%7*TPLNN3rBo%GbszUHu(2jKPdf(`dwV7gsi*S*ErwO=~WkkQS{q zi&icqX{0=L?>54MLKt@(2QF6wk}br^0~oXRttKiOrDikiAd6TH&U5T*aOL z^dq7ub;dEvV}sDk_zerj%=zBDm<0qe1{0@PZIFl;2W43XAq{iyE&G|Z7K|mSN1S)@ z3bsDEhamU!6GJcA08Tx>G!{<^Mh=d$?z|NUPvUzXjcUUkBwmOR9%%$gqF=r$>KVd_ zl>*O~)U~FktHhzrd+`0xQK!vtamr<67UqE@t&tkblGXF+TG+$4{^3XD3c(EZY8?*n za={#rZ?22B7Gcj2719(PCGKx$fxu?yez9}^Y}rG(Jx8t(FnnYJqC2(~YyTbT3P;31)2QJz5h3avGz_5xGG6R6;2tY*#59cPgny$ic}{Ha4MpWnrf zN1i8_^UuiUH9?icZK?rf$E?0!5d({QxcjRQaNzmlsGMwVbTgC(g&aCb!B|YH zoxml#Ft&ABodG(nN$L@a)n~*4XUh)Gc4Sy3=;$hQTmQpft za*FP9J7HTu->Por4fZlNGL2FSll{9=Ib?Ed3Mqd+>i1jvzz=g=cGGz@8V$bn*}EAZskIEOc}Xdeg%BGjFP_R} z7(iHeew#RbK}Tq<@2pFm5;aH-Q6=WW%U1BzL)!?$a1IgmvKYXeC~FdoN#+6*Uom!c zl>WYM%0p!+h77J4Af2qEB}fcH`FNfnttF&s`m*hN(~zuN9jvvaX-elnJ7GD`-4E?z zWq%tx9@|S0dZ&leCUBOvRpRyYJ|c-}Oii=l%C!tF?4v$a=S%3%o;(iR6qtHBq_qUXK7+3mkyhjQOr&ilPDsJqB({sXCDct z8vLwiFcT@Pivm}!;;9FoCzlH_^4tqU@`XLD$q5NeqA@1L&MNdZK}S@H=w8$bo^&Zu zf03TP4kpK@9T4uqn$im(n%Qyp#cP69I8IbcNHq}UJDRvjR z_Nw!_@8SI{Y!BG_&`$iK-%6u|wqRYJKRcr_5~Sy2;)Ln(X)bxi1`Z!S%7;IA8;x4T z=l}E@sL*$yP&34htNN*7NCs9G7}w|1|qdZ8L*9*xKcxlCcvC~1Q;MSruB1^ zU8hEd?(R;kwVXUYHK%^j{C+lrIA;)OlGHtn)h!aHWvLBI&$4b;+m`|o&B}|GFnr)7 zDv+Su8QtCyGIe6YY5wNa*wwj}QD-wz+{>YmMl*hk*@;iiKNIGjP zwAKXW&^e7U?gBI^M!U0LW9QTiT6A%uiKnV0Npi+$a3CtJS#h@-;km|2A^@ zENf|2{G>c9-jK7anzOIdIBe~#P@!5O7YtDp5f(gDPsu&BkdUSYcFK`&VArX-uGK?WL~+G|NMu$2!a5u9ilfv zB2tfhxr7y}wZYOVWGfn^NHdoxYI>J#&^4ZgbOHQ6-b$_w<$=7C18D zqVXvJ`HVQ@4BW`+rD*GDQj8(Z6u+(ntEfyi=v&tWZP- zIy#G-I5ON);huY{mz;KEMl#2Pg*8Z}Fw+rT^U7$QP}7Dte)xJ!u7iCKY$uH}&(n5} zwa*q*NCPU10`XLxQfGnkP??b<)IFbIO|cLt>t)v*fw^LrrCPE?u3B<=lRHQoJPOpHvsbjkNHNlM>9 z8z=S;<0}tq&lH-}IT1c*6*wqJqnLa?pwL~WGEqh61@kT#q^>QSzyBnDA?M1vnc*}g zWh(O|jKRx=SS9eKVCu*i7r*j6p5L*LXt+kMzrZzbxtJ?1y^y=U@UyG|rs3QJ! zqz#J@W|pUB3vR4C*G-Zg2dN{GgmW^f6Y3Kc($s(_GXh9tl~vKY-zFdwHf=?jj;<2Q zmmJ?eK2s%h;)c_+38Bw!0COr`TF+tqnkSSe1(jMtdr!fU9#R4de_%1j`Yc_ykfR4i z@H~kOe1x?WN;#rRL>$K$QOAh*lnvH1VMr!tr+u5CQOqS(oE*(!w5EGr8#;@EgA0yOjVj-DJR4xhyH@_?UJxYAZtt|`5qecqoM4FqmMZH7r& zT}0BOQf-h_V&*L!V5%0g`hulUkLemH^X`wojk~^h4~1fg(GK~$=Mi8$Fw%LIhMVyn&`?#X~$vpyj$#&`tEBdx_m^#*hBN#b>i6$xNkb$*jQqnFPktYukL~wdf>e^+k(0F*3=HXAilbYn>^InyjL3UB;vsB4w0Q3(xBPYhte` zNeROM{1B860w{KZ)mRflxsS!`mvUs!QG&qB^!h1B5BzWo`J9I_hDI&sy4PLK zp+jRl^YA`$1;-T#qcio8>z8oJfW%sn8A7vuV6i$zS*TA{h#PZL#WOXGT4BfqKD`S% zdH$J$_=*`EaS9leXA@_%lsQso?OpDyRZ6)itSt3PM83TUGIcbXkf@-9H3osgCWh75 zUe2Mt#}K{;o>S!5RMXSj$+3OMk-igTv^s7$#|B7~Nw=lTN>bcHsGMZ<*feP^!j}du z4P>|gV?imB@j5sE!AE#*`*DZdbynYR8lJH;yqopOm5Yd2vUKGD|M98sap4uqdElO> zp_m7o89@+0rOs8iUBQJnT)+>%^(YE~Cp;>X6>fd!)f_l5%3}}hBo}%useCj0kmy#W zujQ8}lhkQv6cSBZuTiU1&srjGMvq1#=AuhhvTgGLFkq9^MYz;S$OIl_De4(>|JD&K z1bSApr|TWGNa<0Vs?ynA&IllhQ2~TumSel~ESR^5!oYmSkB+1Ajt|g@Wx>)8_U<@> z3M58@m5CE#%H$k%;aRS%XBI#j5O^d;b70>nLZ=kUA!8@2E?i>)fiHlD<>rrmfQRqf zN-pne{x%E0O|&hHJLkhfo<^Lq;ew?cIdqckkL_k^yh7#pNl+>yegx!lP>or7!E)a8 zo44?jA3n?Y@HqK=$mGciZ~OHd*}3%uPdu`lFz=&d?ONvmjG9p&Yv-*(TZGLvPKzu` zfrwM;6I0HJP_0d8Hdf0DU8^??@$|zx2?7tR<1DHqtw8^1!P+3 zp+QPXZ91Z}yE6lT0udA(4lo9!k0|7^NtH|9a4mbc>?dE!VQ~7oTrP)+8q_DF?2TMU zuuauDD@K|rT(kILt-!>FLMg|VM|UF9l+NxV$M&Cq(07|91(*nQopMJTo8JCLp7_~w zS;(ff@hu)7uvQY33-}&faM4l@?;azrRk-iYED8wW7EPy+4ZtXN6nO8)-_5?G(`Q{VhHKmF{N*m3XU96mILNi`^mL=Z=o#mffRy=6Z^KFo?iry%+lkxm<} zGI5G*^&JYU$>;Oz*?o-h6O(lJb#Y+dQ3!nJr7{DujT)p0%Pv^Uz%^I1`5{+GX)$|E zwoV&t8WV&e4Xs$SW{9D|9^y!FVB1~}ZrulA-o36++413l453ly){osr|GK68_sa3&FJ7#)dZ^>89)@+B&Iew1=?f` zt<4!?q`OlP{M@jYg*_0}H!2 zxZ?=enA94FGvt~1?lbY=3WLssmYNx_&9~)9V$0Si_tRPSIXUVU2r1mXdLFhm2{OP` zYixY=wdB`t;K}>9kjn+w)FE%7X8@Mbny^%$f1s1m(P6H+>LR)p^z;4C-3`8+d3`Ce z6icFuU2t-YO}}y}ul>kv{Pe!(7(H-;!G+zt`nD@5=F2?#$N_$M$75g&z9%3`Gw(k| z8m2v`X*6#AWnx+~b)}&?IYnG`G_sg2HPg`9(@CKavUktXR#w+E0(n-nturJnVS22F zloo+Pcow9dNtN2$(O4e*?o&MYwI4BmNe?&u+O4d<@;rhtghqlLonpnss~H)sAbf!_ zNd_h@`M~GM?$H@#tF|xPnVZ$^o15)53pl2!PD%;Gkfkez*tPvIX)PrHNB$`1{iqx= zjldeD@DY2Z?F>HFn5+aHSSM$j8XG4~OzVtS*3hWNTzbuFwmiNY zB@8CDEhA9B{q{F~qIK?D9El^z#Ek}D!O!k^oW-l=JKbC<1Oy~0Q&SN(j(O;=&1|~i z0@mKJkwV^wMwHc(9qac)N9!P`rl-4`SKN3xjfUXC`?p}w z^bfTokXW5n^OSLGkR>QS0%I-02*)8{JkN$Pb{ZL*Ov7J)nWJ37m3-MpV=D+Wjs z?P#=*zU_^l$j}EO!^zC9lxf3VU%8*v8Ul zFsi2NRs$;YxK{cDSYuJrXJEd+k=Iu)*^KqWU<8k4!Ume1cS`eDB}yX5Z%hY`%9h zkKOe+kADAAo_k;$d!Bxl(c`0}Mxna8$yrU;viV&2s!MtJp66%}1cweB!3*;&TQSJW zKo?m(!NzIh!8snN-dXhneAaBHzN`w%21OD8>{u!*HsYXAYtlUN(m*3-eR zZHFlo^5jc}tk=c14p($$0ZlMrrAKYDMy*n(QLoe1UB(KDCtz&6%E}8@u;S|VFcrBR zUwY2Ftb}|AQsqF1nMzqB;)=JumS?{74c4BwfGDa`ECochlqc`s!PT$dL{d+Xr?ey) zW5~9*6_%Bi8MhNAA}3)Yv2 z$w}Vu(GT*|FMX4*|ND1&!>?XV6eZ|1bw-j=vddR3;nRP2C&>>UAx#Yi!LBEEwmPDO zu;BUlo=06 z)-C3lXLs}6uRY0%wewi9rXMLKan%t)R%nFHBAEbdQiOC|iot+@P#8>Yf@;-66+(=T zxaP+5xcw^+^TzjHM^sNyAARespKu-^5O$X5Nr64rDc5L2Fff~N>34Nbk+YfPg&vDjm zT0-FYWV7a+cMIbpMHc3ym9D`A1KM-pPr4Ct(i*I>_`YJ-*2An>znJwmozGog{wckk z1EHVlOIHrj*$R30)=SS!#`LR@VSRU0Igi0arhC-$A-+50zh-+$iC;pg|U z=7!5zdHMPLC2NNeo`>gq9NT|_#p{+iDU=itl(3$U2)mjL(%kRO95>bsty{(;-@KdoOFEIp zaOm(j)rkg`$r@`nE+tJgf^1a5EbdxlXTM8L?aWzaOqRO?Rugz3yLTLB{iapq2Rg`= zLJn;^jLjEVv|$CrW`+|`d4%#aypd5wxsZ`P`}o19zQDUa_GWJXkNX)tdXg2(`;k&% zk{FX%hUWLP{ka3gC?xLv8`CxFwFYUFqT>{;(+qPMw6VnXIy#E6LXsw$i4zk%a>sq_ z-?EoWZ@rE}uD}!bJATc5ONZy2-^`s^;7Nr^5{kKiT0LRcuA`{mdh2bUuqdpwnX*P8Gsesd z70*4k3zY~)C&pR3X&F{X0$*`(&v8~=vr2)}sB4RD5=5(MofrYd!keTOKv zml+wJV6bn1?a%IGa7izv_BM>wSxzlLD`(->O&wFs?P>y7t6Qn7HK07h@dJ}ASUyC# zC&$3DMLhe|4knL{vEdb)AW2;|AOu1xkQx1KL4+X(pWDtIfAd*>?PG7@{yQFL^J81t zvGpLEpE-c%!O(&}l|Ut8clEd%k)fdv+e>d$<3Li(h>K=dWKzTd~Lm>z9#ekIhdX z;K2S996vTjxvflFTZobZt7i&X$Sk4@MRNg#G#D!wT+qp}BV(xFe#;v^A#q_%(9%h} z)GF|J>fUWEnAgwP$T%x6T#E1ozVCBr&j@R;T<11eN>>Crt)M_NwP@iCguz5H3(sH8 zBR}{VgFSh)wH(_w!L>JB$hW_IH<#V8!FkDS^>5b6+cbpcGrBB5%qik1sTds@r)OS2 z9SgevL9wgE1K)Xw3$MA5FrRZa>1U?o=bbfqKF9X&=O>^2BJcmJ5Av6QQ7duW`1pq zar}6yxnRR`f?}RZrQRA<;%cQq20)snTQjpW*_q!=E3C<+WVmi(;*^Wu{brusyr0Dj z`>01XmaSaOx^-)~zB~8dI?*e*a?MvRvS>QDkoNuCJ9g8aTo9TF+Dh)g4kU`A8|z zXsngQsbs1WfwiRCF@J5n9bh#*3;Ouj?f=Z+lI2jTy1Gg+$AXOu7}_+Cq~O-(`l~k) z<|N(yoh)8IpFM}hc;KO3+^qhy}J%kN~Sq+e2V4gFK5LCi`lhhKh>!ULyP;caq0>nRyp8jCANtRY*u$@ zfkx9R4Py)wQ*{WZ=T2^squOdGZA3U%H&__iy3T z|NXPH_qI`NFA-HEa$Q|q`Jvlbal<8acXxBsn_kCdzxrAZ9Gm7dAO9ETE$C-*qQc-{ z8|`I9;Q9ETzz-nwJqkIGe4C=J!^cw+Arzjkh!UqKEOfL}otPphgyh4J!#fU8C|Q=j z;#H(ZA*$mDUqPynK9u|0>0HoGWvs^GgQFZde1g~Bb~%ey46x?B`7By7gjWa|o2YU0 z#3YA~jbUBbLhZHjt^HTb(Wm6aE?&WW`hQVC9WGfJIbUv$&&LHviqq$sPh(5 zuhfzG&~3Jm80k4B-)!LbA`)*UBuv(Lzcyyc%zp|e9|M@%k(%*cC z!+Vc0Z$TGH3{u5T!*5fU12;kCEPA-9wn?p}+?MCSj^iK#pa#}5Z^Z)I7YuOtx&6#v zJ~RiK2qYbC?Iewp{NwL_hPI9}ZJljQj!ly9?c}=O{8ic)4G>l9tiJd%Ov7TLgqwct z&3x$-pW*P4;|%o`P%1?Uh4eGIyKIUtC7!idAm5c^;mSUC>=@_Z?qT9oVXS4rg{wLA z_*RNNofO)OeEl!K#M&z^VgAykQ0OE+wv&LG!z?-hDd}6?%eqyI`P!CUOjRPMd&wC7 z!J!@&Ea__%n3}U3Z03xqNi)|!^)dosB~iUWA;{qu6-!smr(TZ{LJ&t0xnh{DoR{Ut zuJdKi0sUfM%HXSjRkyr~B&snwI_?m&@AJ<8`F3_3nqd6M6rP_oq!{C{idA!F^9U{H z4p12cWyZlN^47ZD{czgGqs2)xdKqr3O<>!1EZHr#j_!_|a?qY?X#*4VbU%CkEs*|xLF z)?HPe**VE``>O0Z)L`F<8V^5ykm-7ZQcoEz6eo`i)4z5p^VTlKBynr<+MS>IPY69Q zhG4;3(zf|-^MwR$@d_d9u2{xYCFaD)Br^0+o{*_m9 z;HM99{LrzP#G$5n*PpzPFMjgNtg2O*zj8i}N&_jg?wwgOZewTq6`KPrthE$9&E&`= z?Ok0Cn8g+Pm##;Iipt>;O6B6r`-M54_};zjd~_Qf{asWm5q=@Z`EPgy11lHNXf!Y= z!hDV&fAw|-<}F}!c$_;w{yEg46D$ZrZVDuAL5?yOFZA%kloUxyru@kWwo1)Xn}P`) zF^*-}Sf*^Ahd=jyF8uHt+4F^O(bH8#3XcQN>|*@*aXLGDut|hh?u9}Z`uG+U5Tp2y zPu|J-r7|O&zC{Dh^Zg=1dQBQ}PVRqp9XIZ?0;B{D3sx`S$6x*)8?IbSm@k9! z&{4#NZ@iVqAKuEzZM&JjWPmts*2Zbg$CI;CA2CbO6gN_m(4%kZK&BfoQ0T-f1RUMF zm!Y+*5GHHo4g>!EPyUYHB?F8e8UYD|=dED<>#rw`V+w5r8j)uA(|dX7@4n6RpZXHz z?>@x!q2$W$GHddR{vaU4qK%-D8m3aqv;kw>q6-VYl5}{2zMRMWe894t;=C|mQ%wLZPEXSSmuIk>vrR+EM=g?f=Lt_Ky%M$&tPy8!lKx zlFc$eSfmhcOO!y!Ou8T?Lb~xioRE;=}X)hMC zrKkdG^uWT=kYkP>t|>4>LZ&t$*+#p1A!-{O}uh zarG4|3G)GIl46uVChlAncBU!Q3P^QIPhXL}&mUyn4HrQifwb6KjrA98C z@MWc(7Z-Fil`ldFY$N58H{Z-R|LQYD(+xsjfyyJ5fr%;?zvdMz+q8x|{`&I_sFbxE zmSNLOh>{5*Gx*{}96&ywr@s3*G?Glw?1N1pEHSidC5NBi!TcqQ!5Tat-tfV<@U6f7 zGUe`eR=?&d_U<~ulb^YZMLQ1jnzkH0ZFwTU#Bi*6@Z>Z*rJ&xfXj@!hsJny3m97V?s9Bw5PpDFKBNixT%HSFcO%m&Jap%SW--eu@C3DqDjmxgQ79H&!Adx})6kN|Y(zyOI4iBHCvzE{nDk7J} zQ?r_)3i#0-KjmYCeN=6N?|YmWuW|70SF(6nFQxW8rH;H?2V7}iBJi^R7RoV%ZbGU= zHp!n;FN`79Dfx0S)6p3^2j^4QuKPw73fZgqvp&Kq%k}Lnpht$ZWsohthb5b8WfNxz z8B=DhCTttzvNzwvPyXY(h@S2&BhfP(J5}Sto37>^|M)+t%wNn;?%Be`NQHdP%`UJR zFCfsQY05xf2ZweYf>PcUtg;WJkji7p`ZYABPCELaRAkB|eC;p4#^4XO@`k-fxo)tH zo#QqB)hO|*E-@=G@ib{1lUB!x>J5A$DRi_UJLh5Z zMMO2m*alJt&NysJ@3NIFI9X%!kAKDmuecuSamF$Vtg?tkLTCgx{o329-tsE${qncj z{?l!&T|G!&e;4&e!|6%1pxmBk&+bF;>gyqivZfLsX)riApUT8I;esx{@V~yy?mzuE zZW7Sf-_G;HlRUAopPmoAnyW8d&bn2rS-fNkq)_ZAQR)a>sP{vp%@jIDp@e(($y$Q+ z1ZkYIcv%;f>6G!YDpnXOwTPbHFpHGgW_S`IeM-IU9Nl}I8+#XW{`x_7jwIZ7bdqa3 z^3<(iAW$GQ1wY3#M~<`LpZ^E%c;!+Ut+Hgz06)27H_Dd~%d-xfn*HU?=FsP)4$ZDv z>ARd$3J8i6`r4R&_9&hTvNKgWFU^9!EauRYqfDMShOX5Jkoe_*d@)4CuzSm1j_*0l z@k7UH@9X25cil?IvgI&U0jUs3L@j3Jr5CZ|k;gf4@F4yD^RYIjS*psgC$?Ip80LA+ zZ@!=D6DRoTSH8=(2ez?xc^|#=x`+~ukrF#mp*qo^Sjd6$vjUPpD4)m=_{(cP%<6}> z^WH_>>>94}wKQSykKf4+m#!kM)tKMEki|=vVyY>!-J^G5A4*I)jcKM%RMtA-hILa@ zJmbip)L8lk+ZoDB9{7>-&Nwzml|@0%WlhqPNTmrw<+7C!BH*>}xQ4xt>?eL=J1X#) zwgx3VzH{&x7k}Z8xbEGzlI(s4pM;+No)%Ygu7kL|vynCyjn*0!2p77A2iDTJXfX%x ze+n-bW-41JLeM56Hg&%B=U-skPo5^OM-VEC9VL9J@O?$!f*v+qxt>cdS&uk>9fuB_ z;Ol?%DF!yK;dQ_BeyB7YJuekhE3CTcQYMG@lcse%&pWNqC~)CTRw}fYO1$p3KR{)8 zm`85^Av>Ph#ay@oz9M#?n!&8Wk0z&zuX^J&&1kQ_&h&k(c@~mOO^SoIUucCL^0wyQw_+j7> zw~!c{Lg4X*-~SX3edS(~L=(0ZkVY~+Hbs51#`IK!?GNwZ@Bi@g{K5PF7ytT?-=JEp z@~)4(k-oCeH~;KYSkDcfc75#%;wtT31Fpzo&FMsf$Rd|?l&h^wQ_dH;@%``M&42Mn z6t238{SAdG`fU5@qu4NyNmD50x$nmh@}0{+!mU`mK=S#~2`<0$ulVgx{XY5F(2>s* z1_2jec_~!f2U<0oE#|LWPPG;xbG~EgLghBsQxJ-5^nAhbDZXUnNR>U?N63X921&8x zV?7ChMEM@l&nQ|csMjM(J#CqLdaS#6J#k%=N(<7bGFjm*|Ma`u^v0X8lU0xw8J0+0 zf={`<ZR@kuECyi&Qe?+NwT5R5=UsLwTy;6oWCfnvO!epiO8rATc+X?}=+-~t zZG+uRCz?-J8~pJTpJCVi=u7sOCq>3arY36?FG^GCftLv%7>bFFrVOjx4-6pH`Zg!A(wn z=^8er^q`ohYp~x9NK7rZT;%WH^9LN+z8~W`Ru3tN>M>p+aIqNN%q;c%HfaGa!8j~H;1S=rL1v{EMyxN z1liCai(f8q{>7J3YVYRJ`yS>Cul_yWHrR~;{^i6N|JVJWV&%d`P-`HR%Bq?cL3+Br zB@rxKy8`2f2v6Yq63+wWdk7_4-%ITwi&u3~j|?J{(MJu2Em!1IY3Rg{n@W0@?6v~{;#YzeS;|j7~I3HD&FsS8~ZaZ{|yH|2W#j#E~GiI$Ipq(VH6A z|JSHlWKyB0zn!0c^f;zc#OHhQF$pW--ik?zC_uq9N_kZ?If*l9( zkB!q2C$t%hl1}(JrY+l*Vs9zW&7}f~5m*zkdFdb@d*3bC$qGV=S@tHeGIj5rRC>b%{TGWfBGBx7ZzH5eM#0;EVH==+88=|3v>?^@TDR#nn)WwUuNN7kUAYb zsvtSEYnZjyUkVlmhB_&AcS9O6zGWY|oKK@(r}LtVFupg_($yR?6^2lmqOGHiMlCwM zxOX-Ka4pIfBeRmKlwhPAUTmN`UL))(K(3q(t%I)ly|i_ddEnbWp*~f^_k9{u6;|A^ ziL2g!8^N?@`NeB_>L-t4y#UV>bd&;edD)t?YY>#Tj{T7Gutc6$4BeEaXe&XP^* z>F64mVG?J5jIVW+FR4a!w)5EC5A)@Zf1aU7pXb{4B87Z_Xv>j`oSUX$Bt~kwgc3L+#X)%d>3 zJ~vsAL@7OeCDyL$qo>c|pVS(rDlz-^jj->~$!yfVkHBCI3?Cln{r~&TG)Bi+vtcFF z8;niW@OK>~2t$reRako6g~(dHC6RN9sGBFCtfg4Y(MXbWuFT5jhbA=7eK&;IV10@7 z4AqfoNYWV@SQ69K)rHFC+5XT|?!%#hd}oP`?|d!mSFh!Q>o4Q?ZvT63{LninhAE5Y zw^MHO@jMUdDU>e>0?EMqcJ#y)OP98zBgydo5gxtgk(P`LYqAH;)Se=fqjk3N*Z=L4 zeCEcF@TQ%|xN)cpl?$n*hR7Iv1cA2LMhw*yGab{YCI|~f&vg`eQlWC*|5x34hskwR zY5!Ms@9o^v)01ZuM>*M&Y{@}#&e<574=2D2CYiK=m+*OsCM*jq3m8~f1IA=*oUqph z9B_`3vm{Gavhs{3cD$+T`=f4$k>vnu_StWqN6(CAr0$-ss#E8j_dV}x(8yQxC^B;Z zLV2KFLq4tf2l%k!|D4a@&T;ItL*Kk!l4*x{(m@Ns5y#Bp#B-J~>A=avr*tB^^Ell( z+WR|McFbJPJbejmU0Ez!kjkWZ@#$U6Ts)u2OXg#FTpJj5=DrH-n$AS1D9A$)#({{E<>pDtc6^0e;b^ytj(uxR6Pqvk6-J zx)~iDVC{p?U?t-ORhPLZA3-*kA(>2r2db<&`CX?IQyyX9)w_d2G{TZ!RSfSflTO8$ zF*i%nQfynj22ujunl&R0;i@5iULwVqYgSj!E<>5jFUxly=yPqH|@qsS!`eZJiCYQ!gXD? zJiCeUT?2SyW#X-Alv2z&av7Dfi zisDFx($)|W!0z8ZM%n#0QXM&FESbr&6AqyzpGOxf$XqKwzVd2Xe*Q;}o!Z5Cq_~}S z1W)W5W>rrIhy3&Tyzi2;=;-S)1w>sE&D_g3Tzfs6Fa38GwP!eRMjx?Ml4l-R#es(| z0oU8-&S|2yY2QfFT1LmfvIG=E-hBCox%N|^;DEy>^T5rwaQw-O$@O+ZDglWUQ2SGR z#{;lEh!+r{X8EDh_@@hh#rdDQn4T&9Fg_HimMFAaWaq6 zTnO{ok_`_B0Zt;0@B7FI{(ZGP!1g`Gy|=ICq;po_xvmL`kto89S>4=m^WA)C(K|VK z<$+i&dE7)hYwml3_0O#3zMuSo!1d_p?<3jSf?F!{+!L#r+&6`t8#Z#?r~j3y%N7xY z9=0vfNXkJ#VJyVm>yh*{omy}}Tr$;h$T$*53WDF=g|A@L^Z89{n(>3?aLQ*d;f1Hx z^VDa5zehf94W8 zr*}k)p9RqZIt4iB+G5noY9#c>_}vHo#9^nenniu%o%1h{*+)8Ix zmaUIH!`^Dh=tw}e;xiqGBjXOe={UJo2dN>@Pz?hrzNQ>#>{yI=)uX>H!-79rP{wpr7}$tQ_V#FLzN1PRvt!SXoP_+4>9-T#b{R` zNQ2Ip)30L@{4G#ak)eWIhTs0ncSv{5;oMKW69z|&)>5b;2q5Y4$hWWOl-3l=_u1ax z!{USb%^aqbkwT;}QArrGtX2vUDl(Z2l|qG7I{uoufXt*Eru4RO>+c>y3T>J{mXRft zu>=Rs?%}bAo(4BW2R_=XAiRL$-eEGmZ6FkhbgJG*2;C~3U3uCjwe!M&$DYkq=Isi3 z4>3O4*2)F>G}96mgMsFOu`+)yR``9f!X2$P_smT2__92=bVxS6=QJKUa5gt?-c8ZA zIi{T2V`=eAHDEt4%)u~Svh&^vbu)8JhP}I{Km^U!Y zq{VZQ_W$Su4iIIQ+~LNafOe>B8@@{L~|vwPZQjd^eu2 za4X|f$350P_!#&8`c@oA@_&DNIeKsmAwwdyD?qbh{bug}*tNX1JwvHfX5jewy!X`Q zP$-+3MkDqUm8}kl0EB~(4(NbHI>G4H-3Zb3iY%-ej%lxbZY%Lrf}~@UYl(C7kMHJ9 zA3B4I=hM=bWvg3d!L&A>e|8IP)APi7a$e_d}j6gsjU5oW&VN2Fhg0f|LrF+TTv0>M=Y}VfRa8Oq*dy7O8xi4NpAD zlH-oxutS$J>)vnk*?Di{J<`HeipO`2a@1EZWy)b3zr5^U`RD)o4tltNu$xp$6*ZUXIv`flBr?gEpg#C!TTm#m_^{>7 zm_LL6`pK`k=Cj|XI^yCM$0-gM@r%ZlES1Y}_}R-zT23^M7lB=2NSJcC_MPA5oV1Ot z;Vv2DvM;{_3ghN^LmBBWM3dKA7^IfKJdTp2(`jBRS6)T`*=h`p7789(K&etCpG&cD zRu{MZ?m?321c_{nf)}!M;WVCI^&HC8sK7%99yT@yAAdMPd%W=6cCLQwS6KA(yLe|V zNmtUQ;41zy=<~azX6F&@%s#r4nUmARr9ewfsBMzD1oIB;g&;I7<4S;tTclc&3~qhN ztbtplBbVpMD=uLB-XiH(jC%vcN#`5`in-DE2!zdC_n3Yi^P_JO3q;#a4gbB$)K*c4S$Z+c0&LESDF|utJL)(Y3 zB`i8(A^-5n3;5cvui(;8UqULK0X(BDW?6(Ohbzv$k`qV9NhIRjx@&~v{_O&~+EbDI zj0eIqa{(MK#)-15`A zdGiNPMkgJl1$jcYZrRQJo($SABCR}A4qCvjs?E};ws2Z&64~8?=lMK7T;Z9n1k+D! z=YX~(It=jwg>73TvvI~tK28h<2TDwzn?nm5%Mz45)2NB1ZN^{Rg;y*S6Bd*!9Dc@1 zZvD))^m#t1^9GOsI`KdE2Bs2Ol|;n|}BwvYmN$50_auua~DE zT92xR2t0O=jq|=mZ{w0u$Qf;ElqK1{x5&NoW+Fdy28SKp#njG>(e9BFJ8q*bL03;Y zfN**Se9>&zjxOoUgoxH9LnnEtlc9NBtv@Jn{@O*Mjm@vt`GATr^bUp;e>bpp;Lc zP~g}zPvj#%yn^#T@pcY4d@%_p4rAl!@ev>f$t>DQ;@FZ{zLoEN;2O>w8Y7=cux+r& zi!0}H{#%z)dT9XV`;AktYV57B(YlVNu4{Rav^0g$3XZg1(}-KTuFHzk4`R6J5@>qkh&V%WA#zCCF+OH3j}z{uc; zA(H}F4$jmbYz1wDV|b+^dIA|$Hgx*J=*_5!OzrnKH&n5UKYAhe%<9AQe9mc2a>I!q z<`>`jC88r|Dk2?3125q|5~@ubN;dYHln!kjJ)~P&q3A)Og7!SL6sTklk(~mGRv^aE zu5JA8`g{56aaZsT*C(BH*fCsS&C&Du%J(n9Ax^p_VW3`V@0YSSQP9cA-vpteyq_Fk z|8Ikoq%#TTESpKW>Ow}=MVlp>GcP(EolM}xZD#gmx#6aV_|Nmc#>et0JX>Ienjh^L zX6(|Fxa2eMCK&PPnA%Hz)jciVDSuOx)CjXi0mX}JZm1ymUs^Q z=lAYl&o}SjvYvK`*=!oEu<7(=yyw$rvUleYe&`bVe)FBy^n4L)tC~zUq6knb8{1yb z+aEu0U$t;s5>!GK95$ayU^x0lMu|3$vDX zGJSp?A++HLI5tSzAb(9^Tu6zn1;PvPYqT-M>(dq%Oxzq%7LN{@+SA2X*8hrMb+of_ zPnk3OdpT+Ki+o_=+xW&OzRq~14Ec{#6F3LGC4;*!1;~x*RrXO z7fGd31hqVI!eNjTdRsP95Qfm+l_Qf&8sWj(Jejr#rB4_dj@$RYekYf;rzm=wqz?H* zPa9X>cNML6f~mb-Y*_moa}PQkRSxJsU@nsnIf6agpQL+oM`Rdj)HAHe9&OT`Hafa< zw6-QtXvT^@g<=IOl2lE^l1=xzpgJ&u5Q1DRfhBBn@+~>>V8Mlj0Rv|I?H#T z`8i*^^h@mh#h*EJayy^NCb;W+H*>-FZsowY9KmH*T*Blz{U+h`JgA1I{Hx6uMs>Vt z(HhD)P6`MZ8`#Tzzq_Buul_IESHH+*U0GsXDMp5i+>_66@oisV-kiRu){n#qLp9Mc zP&wvjwP+)18Leo_3mht?G98_1WAzhzWg8~L2Ova5n0RMjCzWzQB59^ugj85qjFqeW z@LLaZ#LjVONstnPKa~{kxcSSpT5-@J0*n6HGboP@k;=3~NpsO>Kf*UJd_V14A!0Tv z2(cW8P#Hb@mB-8?-`is7TaE-bVE2|m)@|I2>$+q+a*-m4N$|q45-}+WViG5An;!Ci z_oJqVVA<%=aX$6q&vEsPZhrIGYdEzl$EiK-92NLHb^ZN(?YH;fEbOQIkohbp}}D`ui3?kKUioLvss>~-aY8IM4UIGVt8b@&+EhcOibjrOP7>RzMD0qO zn2PbiYyOS@yzHaQo|_>zsg1#H!z7Y%PB~){c6-K1tjANP%n>0yxh%Q%G=IG2NqYLa zAqb2-y$aAn1#H{KOW4F>F|_a2O&<1JqP?zsr%z}evqJ~BYThnx<+K9!Redlz+QVm2?L_A3?z-ov=)tf>= zNTur2)tN=S4rjLdWRO;ZbW6erBI*QM2PEPS4?VG&WrZr~e3Bqkq=e?NP;v3Jd^E8m zn{{81b`0Ech~+^jvZ+?i`{z$_!xz55vPtbMIN?YV#h0-A+8~)VAyUNAB+O!_s#LR8 za$P;xovrARB2o+7YDm0LFy@P?Br@he$(_Is-YVMcj!;>}MIhMn6LL!lu(*o5P5%0bxZ7D3L&ir6`V%BSW8b zCX0*!tNpiwqYV&-p`myyMJAKN^OX@{h}^bQ2@5Zx7i%oWT%W{@HeP!885W*)D#EX6 z_X*QMtG(&&sL6uefT?rma`^dgbw%JbuqAZhi16%G-v~1LHWQaWZ~LURb0piQ`B-sVP|&vB|yUj#|v*gmM5&A{`ct<(#V3C9H7J%% zm=g}0)@{TZDN=SMnfcQqJ*wK`iHVJw*P9wx`(OzBUJjN8%0rKg5m%anmn`Jql?O#u zxFHm)>>U|p&(J7RX&g&p#~iG9jBGMNz9VlIxavWv1Y?HErKMFz6b-Q>!UBXRIE&QF{^*)#H8`)#fe)6|*2Cqnz^Y3z7MS-1*ao zAvXyf>w++e)@dMx%x3t-=dNPO@heEQ<)b%7Lo&uww>`)dE5VMkPyeBd5LLhFPHeV3 zt$qDEO2Sv{709NaIS3)6d4UiLCG_wZVyFmyKxejPFkPes^QSJkj&i7e8`flTP&6HXFo}=4aB52q6(yQQLLZQ&kP| z3PtRgV>}gwdTmM=%FNN*woS^8;d-EDFabg=dp?IR>*Mi*rm{&=lQ;ng-#jd-Hje*>rqkfOE; zWYnszyPz9yB~vvuHI%ef_h7DP`OV?04u9K`RSjUr*|ud1*`!2;nzFCyl#(!gnVkP4Bo2)*ECJ4VqElg{?A>_ zg7#Lnj8#~4@rj7C_xi=C-pCLASN0uhNTO;Yt4Pt1*NwWa<}QyWuoJ<2 zMG-;RjYtBJjI;ik_4M|%QLR?-%I?b+dtWG6D3DT1eBT9aVTqdd-bC|g3)33*N+o74 z>EKUZKwr#eWf`vd;T`CdjSl@rW*yQPTnK}vPMvS^0_{Oq!5bRp$g`Hwl27uvAN?b5 zx%{m#Iv$BZDnvfRufP9$<~+9z+qT#=wUebs9gKE8{>MMG-hbQo_52^%)UTg#qMJZ0 zJJMjNZ3Izk{?rs!?559=`BnY?H{HB$Je=!i>iF3F-dE#eQuC02H1$=th!>;L0AbXyAqUZafBx{(~}z%057 znrK_fc-L#mKmYI}OkX$^Mu*Lqw@e~!n>B0Jv-5L5V{Ts?Yqt+F=i_h17DnRifAz_4 z25lx>8CE0jkX9ofAj(^W6(xMp&t=RceA0^kebjTQC4kh(p)q!D-bzLqFQ&bN<0vg& z@&2#%0)!CAP@Crr+$zGB2*<|vjHsZNR{iwbwq#^@jCefGg2{QhE;)?Vg9Y9-y^X(o z@cTS;=Od8HMBX~l0ELPu&b9e|9oemF-?(HsG3GCuZ{*v34R#8g93ma##Vy;o;f&94 zOeRUOP-6QD2XW4O-ej1+LjTV!`d@R}U+k%%*)p)rGH7T!sM zlLadatqJb=<0Jh3_)l_*1ZfGblahD-auIYdRp@-RH#|^^mbZWTa1iu6UDtD2HJQ%(hoFYsIfy$SXE@wF}z|# zDHf&_RLUr16h|v$GAU$>Lt!l7O_wa=zWy9LhAMohE63HxUe3+Gz5~&fGu-w_BEF84 zMc#mDKN4N=Bkx@YTH}oR1+MzwRcyWF-#I>)!V;373>A3S&0nH5m5NF(QJ;wYyPbHg z|9)49+9wL=rtn|q6v#RPTH`{PWUuyp$NbC^kV^8z0}nHOQVz6W=Z+zU_Lj-z<0xP6 zw*t^Qj7mg>bdm|mB^P0X>-wZJDJo?bTQt-qYvIBxxFk{zcGAL@8Xag(yZ8_uJaG}L zca8Dk8Qnbh-tY0x7hK6$P=##5j5vjXxf%iwbV#LGWas8BX6Lu2+4RyjuKxVB{N%__ zv+$NDIG{U+UkSLn?D4_-zQLmT(;$aa7z`tXh_Z@T*+#{@8IE{Jioj6>zov~`9MC++JE~4-~85XRHwC(=xs;FEGSip zk5$pz2bo%OS(>&vrmvj<#oD1Vx0fo+zUWB4^SuwjxMvbXX&Zl39WigkevrKX4U4b- z%GXg#c{BOG3Lzgqr7{V>u??4jDY}*1EbMUJ^NNuU*pgw4?QK`2!? z{LsZreemD-`3J9}=b?2hY)!H-m7rRzP<(a^fmWoXz=_$!b4f%l$w;Nj{kw)(g=ESZ z2Xp1e-o&&6`eC>P@kHZIs#96ygvplIy$Sx_r(XRx{*&IH%DQYCZ4*tii8fe*ZR=j( zhOb}4r@#F!#z%_m**?Pf;5f%Eo5hBa0-)=30i_xY)_p-!I}Agz9W9KE73pY8VA(Z# zbCX@0u&B5$xqQ}W+F;BHh0r8YaY}4%=;k?;N&Jor4FUPwM} zk#a0N1$)Pp(O$iY9OyEOb~!$J2cIf{$~;Ifv2S-op5v0q(f*an3(&2`>$dGilBgTrWgu zV=pVEt}DV%csL=j17DNt%(7v_F1kCjNNpi(@LX+_x-q7R#X^~EHf~HBqk#kq3(K-l zVU^O@IBCZ+UBIz2iw~I1;-7zvJzIA3^doCo_xMJJx9=vNN+LRvOq)B2;|`e2yhSri z8CdjTv=kXo$|la6nlPAI?}5K+Xchmv?)kr_%h(KYGm#Uhf&kIk!B@_CAGQU@pS6rI z@c7eBt2ts$56Of@RVk)-rICJ!AG2_@AXE*Gr$b9B$sE)Al{SPf6g;}ya-4L|N_fj*5cmc) z^QsK*8O0B!8On4^O+D^?YvWodv>K?{gj4eWOSpdpK4Lu@=q3P4yu5=H`pB+!uKnbf zdFn5ZbH!Dcl1j(9@n?Twl5Nq`or6H)p=rs)3DMX}5LiaO-$?%$kj`8UrrBpOtq@u} zTNvM4A@o90S)1|k@&qzLwNxRIN*YmPWf(M4gYSn-o6*IpKi>!0Y=gWYnt+UI0EG~W zAyfj&g@9x_OZSxSM!&mO=G1B4Rs;V=H<35OeHuaHe=g*!H*)dy>#PC*KR|c1asAi6 z!}Z_&HShcUTj`zF$uEC!2fePxj43&SAV685?HCEij`o+HkUm*~#tI=o6JU~fDTMLM z>z&%p?meRrgrqVlio*qDlZT-412eHxCmEQ{5r!c>eQj)h;&~{JH{Kx2YC^lBMY0Wl zEtSoZ%w)|#YYhXh_qwf#Yb%47pL$D7T>Sm@1m1|v@J28CtCfJS6w!hZ-IeE;pZ^BG z|HjXG_h&C8+mhniFI~^lT$)LJ?f8K}2n$b3;>kqh*$~NCYmiD~1yR@+y8b<_LlXhC z*0khPR9#I_sFI2s&l*t)5h}#@LxgmYG1-I?>iTCXB|RM(?z{bd$YvTqMGg3{LY=fs#Vgh@kY~7 z8IzCZHgL^Q%jiLiXaG851?PHN7~WgNks;-ZkMD*k1?8$w%n?{I$JmfG^^>KPR4Ohj zPFTQASN%2`)To_&*?=sijKX<@ZBM|sW!=eR0@|g;MBZBs7YeKVOvs5kb+ztU^e zuftO*+9)dN@TH4B#3Q%d&8$T;Ve1~|r{XMMJQWpa(lJRgnIw~plg&AddW!b8EPk`$ zc388qt$8^pbg1jUWBuI?e4i<^dRTks(;V29B@9E9>mgD$<$_Bpn=z8&5&tR$@E5D!%CT#VKwm@ z|1O!mp7=Yw3?ft$LO$6vL%VizEi7C`VyA_3V;Y9eg|-FSmS$6X6J$jG_9&w#O0Y~Ipf*X$)UTXc)U%@~X~TUt z+|IARcoj!1pUK>wR$Af~BV~_g*6n0>!6)02qHp?Sx@Szqu?_d|rEPoIv}=%-T#||# z&@5~bGEo8$zh-k|H8y|{0v$rvv`z+gjFC<_6vkbW*(AlGGU>h?=n#d26ru5o(2Yis zl!_n-IQ#s=_}Y7}cbac0eO{c($PW6O=O3`SELW3k(>j&uC8~3-~!%cBZ zDP$(jH!u7Mi)Z#T?~=2bI%SH1rvJMMWG!FW-(v`k3VflM7q-nI6h4Fo#=?f;aar16kvk69qia1)b zcW9J!+%a9HDEpU9dQ65yYZC;DxeKPyAzW_!`cDw;T~YE@qc}%w7t3yHe@5&VqQpqp z(J(}f`(CRKwBZrF0a?AVn|D~UWDzGhojm!YUqdYU#=*?5LN}Loj8rmBuB8n-nWR`O z<7DEvuFILKre>alFi%-*yy_+;}^8|LR8Q$VHfc#9fn4l-xxN+suWRHI{3Q^KR`w zOSi<|w)o9?M5W3B7o5gpFFenbxpN``Nb$Fa{raC*$?The9$P?rR}V7|T*Uo1-o+C? zxR%ovbW?Hz?tF9$L8hJ4&N!UoRxThDk5esH2?C8H8q&T>DZ(%$mWi`v+b}a`byJO~ zeT{Bkh|mhD6*>qBJfr(8gqeq&kON35@v1(v51z%2$2L%`1W-`~S}{^|X_F9!x^DHx zgc8F@yE~SoSa5m&Kc2yN|K(bOKylI~=b{Hk%&=l@3~S$Bc#Q}lnyQ56#jj1u@;a{p z^;%&%q^GNw#J_)>T)quTmA}gUd?I4^S6o3gVobh`^^ZKx4PX5Z%O{PVi8X+{sFH_+-Hp=rUk19sJD(!7qiUk*ms=KN;Fmy@Ttzt)< z&zK#Lay_Zd5Km+v9fl!u7Ej~ZzdVCdibA}*mk8h%f={$uIep4+6 zeBcvj@WXHYit&L#&i&;3VDDHX{J-oC(v8MZ?Nrq?{_2}TO#}k}))uX*L99NliL4pj z5`Se!aetk>3BaU$`3v#`$Y$}2!~E)sE2%vDEC=;=vS#}jlO}g@;;{$e#4RKD9+>d% z`#wS_v>i9P3QA#|>7|j~5sGy$?xKHAH=gGq38R-2+4pG((FAC8IRq<=%)Twtnh{!} zl>}c9xQ11#ff>u^Ff=qwvFMXXI7kVls=LoGr6x2Pn&n7@WlR+>yZmgn-1iv&@!=~W zpNnemrjB3jDzNOv;w#aFfk*0}wc1v`0o}TqAY`3aBL0eT@;Z<@G339jzE_QEKpEw4 zaw-0F^^f_#XS{>*Q_oQ%#&|5rTh2L}l`9tD`aYFF8`m$}A`HzvANm>@DwOA=JU_aQ z&0&ov9G+RfjRO|Xz_0rC{7fj@_&F&P{=JHitwVfY5sSr8N>M3$jYbg4M}+21@m!Y! z4xL5C6^sDew?4GBv+eok`Q}CM=cQF^7$2%K zY2gemIOk}V%$m$2k3GlsO#`%KlUPa_uttFn6_(YIe>cOk3M(E%YmHy^*|uYtOjm+z zBJv@O7QMOSrxbX_Xiy{+Shm8JnyuT1u|9gyIiHsj{Az$8#P`6C+r$$Nm9o$JXSXnQ zdI!@N_OkBT9rVxa#`F9r>^Ecc#uBDJ-E3K=*&sF5s$yDyJCi51@yoCNjM33iW*@c~ znaW1S&ax31>PeoMhzaO@n^4HC}n=cR(nbB{kydux)`wmd=#9BC1R0hVo{$1&9+Eq9PWK|9m|iLhavDs3m!4Kl~xEXs8&2gwTf06JDD)86%AW94K{4!blgT& zjApG?ifY;A=(Cn^<)5D9#M!;rwvFuw3S$+VxK+RRo1LjOvi@EOtx!T@D@7i6`S!t04}Wd|R4W z%whYs5qc-(>J~DhR+gCk*K`V^&Qp|;B1{W12w>){9u_T}%9h8U<@)dcf?eD9FnQ4| z5}oZv_uls+uu(R&dh2A+zr#$xz5vsNfm87+q!T9U1uBhh&9LLyC;87$ev!V6!+GbQ zNJ~CRx-CwAN+;x!Y+ApYCGR?yY;PA!W-lZaPg1E?NeG)mPCt&0h4Z=Vt_Rt>V-E?T zXm80Ngf>d@UWm2?nY6=g_dd%pCoD3emeFf4p3e#qngP?&XcbFqM#rioQb`;E8qJHF z2Iy#snGGO>Mo0_K4Tx!t7lhaehrkb+KD(PIAAOOTb9+c7W2}3A8`Jtb2}3nu10b5l zkfMqJZ4^93RMtuJ4OAEqXhr|5E|x5t34?>&cm1ubdHfklr83!$R+8teGG%A^y7jp_$AX0?9adMeg|F581MM6&GK00@M4uv7CT(M;lfw7DWe}-CK4t zo=g*~RG77J5n&KuOA8?dZrLT9Npr-T&tmnajkviUo_u-(qr3Oenu^n!&yvn%czo3c zG95{#P0ka#0SIldjd~8I2r2=7ah#ZhZG+=X?QKUOShsN(BLkznw6lnK>Gq%N1|X#I z$HojrO~y#IWw30)=Cyn9O9lF;cd+jHog`B+`ucK_Ag5@WIMI=Ot3n~_FQk_38yZ3k zly9O#V3HL_NMbRYYNgEXodpKR%E*>hx);u(cisXz`zF!S(t?h~jC6|^n8B0g{(AEQ z*7$X=R@&7U|G&{)RDlWiwr0&ece3iv`&oJTB4*5*gj*~T&!mmeL-WS9Vyt;`9lyPQ z17}|NW@gNuZL^iA*L`exU==~dC!LRR={rxP zP^w~UWpdb1PpHbq_ZG;+VC&8?+B({3&86^tm;e0PgB*LzA`U%hj0AyT$B>@Ov*E9atB)ZRJLv4r8Up+qOb82Nn|QVOW!0qXH27e$`o?#tq@N} zuxDTr%Ft{KBNt!qQ%5c7eK~i3Ln2voDRmr3I_!M%QSQF+kM!kQ_&;wt#<+y~A@SDM zCU8hMu#V7W=A5bMn;&P!yt%mL3YIWtdNt;^<;2;(>LDn&RLU;ZLW%y_bC@@OA>l^^ zS6_Yw=NvtkO0f{B%jh~Zuav@)l98b?VpydQ#v@<$9 zZdAhR3}mfw(h2-RiI`;*ma5n-X{r^Ec}r*U^fQ||}!t=^hw>;0TXCGrt$)gb3s7#i0 zPY07`&0@-|X>?8QA)alGa*5CkmAFCub(*G_XyWBJ`3da1C-&v(hYFpEBN7QVJ@5ec zU;hVENRC;)fK(=h>$ zz=pMJm_2(o6}O5VxB0~v|AiBm^dblmp~ebD-6Pnvcpb*Z3dFP~lZx^9bK99cYciH) zao7E8IrzYNv}O_%%2hfJn2o(}{q7{=_@ydZD3t4=RMI$RPj6?<>P_q!7^A)rIV=Q;L><+Qh? zDR?E)`4m$ZPBu33s;NrZpMIeNvPo{d?g3U@{0_8drml2z15{~JsT6no@|W~(e4Z)O zySeA#_3VnJIP<94wB|B~C#b_n_JGg)MB|yp{4$kwj+;?!X8ie8*W31UTt9ljir4>1#FQ zR^>HL{e-JT8rrobAhggb;I|LH$SLPvK($nUxqw4-pIeC-fB4=H8Gi6lNI0B$#z97S zvg}eUE0kwi?LiRYI1c$#n$E5k+S^)iy^vdOf0!c=oyFP>yKvg_oOtRA%(S4!vK=$e zq14MaK#$Lxp$s^audFAIOgQV_jfoiai1GP`C(0DwlO{N@TA(Riw7+rf|5GjyEw65s? z2VsaERU$Q*-;S_`gd@?R=8?7A(5Wm_`Z{r4L$2JjXM|n5huJ(-WZ6Zh(lKW$$<{nc z$Kttr9wD~sMNT?rB{Ns_8?D~JWB+obq9OcLlZCS#HmuvoE~}S?2OmTbyo%I~(j+oj zwr$?R#x>87AkMV8GuXXjH--D|WX{}aIF?O1VG~qbic&IK33zGO0BcvTV`_gdRyN17 z!{;$|au;sdLj@r+5yOg*KrJNp{uWGpc*|;TzwS{^Iqy(bp0J49 ze)|Y*odK;aHeskITC+4~{SZ-<71GiX)*z7SWW8)@Tp7|JUwY#Abp#;`Z9EK=mRJe` zZ8i_WCKRg4K*OAV!&V8=LLhXA9}41*&H7Dy*r8$^_2%Pg$HH@6{7Mxe1S^g>kVjX& zK+lvZEI9OFdRserY4c`w4DDv};src>`y=$u>V-fW^^^TJrbMG}A)>|C0#Y$HzO;uq zC$1py+`3?t5~{AlP>TrCl2i)gw72J(|CaNy;u0L#^P8KQHmRLN(nk0&UUG>|?IH{` zW1F|r+1JVPm5XV~w-Apxc;yOiHAGv6per2*sT3h5>b_zTY(*M(!nPPIxSV{}L9BZ4 zd46%#?RZt6voBb|V~?$;SagkzYMmw27(1%Fb~XQ|!0-=|@??|gz?6MTx;(V=3f z;M3aH-QZMU79Z89nuHLzfloZ1q_=AlmM@`T3Ynp;+i1;Y@Jl{nGEVyeGik}B>FaD| z#UTq=x_AcJWSlSzsg|oKDNq=I$(A-~v-m;?lnh_K0qS;QS|DR_gwi;c!moN9wqicd zu3FF6Kk-YRUA37D-*+6lSuJ03!j3csn97~~SX6`6Me?Xv=$%(pio#eCmiL7` z4mG~72$jHcB4xJ-43dE&KqDehNksV;f#wybES1p2(lJUE9~(gs_{^T)&BhH=Ir8KK z_|1>*Fv!^tyYP~4NNFTM!q}LH3v$SDle|t&0&YkGIw;X z5zu54FU;6bv?0SN>kqIcs6e2Q2yI1(WR2!6>dC0N`z5w*7)rN3wt+=Q9LUYr{e{5u zi8~f&9lx08U);vZLl0!lbDQ|fx*c5l?JtlBEkYgQ$OO+nzM8qG9>KleyoRHvPr*Lz z2&!cl6N;F)K(%mRXWKP(BQ2Ykc5S0)@{~x0t%*kwkzax^B%5j_n@XE~ReR{@I`K*& zj#t4~kZEr*zB8 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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 bm3jXM9qA<9^=zj600000NkvXXu0mjf@;FUt literal 52172 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�Lox+uRFJ`X8H5yMq=ifdJRd6^$OPD$ zMoKq*Nku@SG35#~Gjkk1aDX+NHbPCOA6K=odRsu3aNK`_)Dv{K2ae)uTJm=8}4SvpLyd1G(Q$I3r zkhX@2lREG+byyJB@T9_RfSLqVXk^(n+n6i~N)yva<)L*Ixmcm*x@0?-GdMW(z(PaQ z>w_JQR4`l^ou{{}z~JBzyB~h^;(a%LES{J?$CjIRSC(IS_1!v34t1L`WKC_Zh-5qw zNq}$q&+k9D@50E5=H{j>XJKTq=Z8JE-nd$%RQV96asoJU!_FT+-*sunjc1P!Km5$^hI&?XHL?1#4wfz}&=9)h ztj3m7I*1h@U4>C;C8=64%ER@uWc(1}IMk$rCmpKeQ#||hpXgrEOGi@yU5>DxlcsMW zwKP&!VTu1*9n%|R5jFv>@qLfVLYdIZ;uvL8*rU~`dQYJem&t`8CMuIDG*g|tz>crb63sy;*!ef7-3u`i6dH?8tH5;(il1j z6XUv;0Em(@I3*^_Wh{cwEl{aOyz=rOp@pq0)?&|3K|Y(N9h`LhW7UUSh9(UmN`WEW pXcX4qXvj!~sm3(=KDDIAe*l_}+4pQz6QckC002ovPDHLkV1fs1)P4W} 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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 z 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 Z|3BWF-Xs6Rvkd?M002ovPDHLkV1n9XA~^s6 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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%0DMX8FKsZbRSr(BsC7^eD>z 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 z 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�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 LNkvXXu0mjfF+eT_ 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 zcmV)GK)%0HNk&H0l>h)&MM6+kP&il$0000G0000h0RSrj06|PpNPQIm00I9eBuMc8 zcxyzY?qLuA|3yxt51JC(r8I?>rh?OkLWJP%P{AGQ0=4VmSKN;tRDd4t?ox6jEg?7@ zZIA@FBt$p1$?kq;R%d@Z^V|Fx(f@>gXImm6dceSMGAJx}azFT!cDLF=K=tBqSvY4& zUZI5ot)`WW8^M3Jw6Htr6%St`M=VztTce!&~!ap>- z%l+_n)XYP)(8p?t3nn5=Wg~{R;kIn2nB{T?dpyQ){Em&VvV(J@=`{RqzL2KeN%r zeB;_9dVG{%qE4(gd%++4O6|uF9mlA1)Xzl7yx&+jjXZ3o=fdo#i$8_BmwLJZnKx+- z=lVl$7tno1_UcaUp?-IZy2^_m2mkQFk!ZS{J8%+Coi|c{L*OrFPX4ET@{ICudTaF) z>M(0O{7vhDQ0v^MJ|DG%pD7gprQ%iUbtU}I8&9a;S@1WFZ&A;C>cO8>4~wV1C&8Z# zqTc^RfB1y}sLlVP?oYi2e-PBRJ}Ta%0Upk(1^IWI6I%WbjS&l_7UW(M%^P2(QI?<_ z?Dc9v){Rm3E{(Gcj&fxn=hvS{*fkpICxk^p&SguY0XCRutf_;BLDH4G-~xyc|ZgI1u4dyjXQn+(V4jOD-cg3KWGeDMs|Sx42?Y>GA$oV zV_QCk6kn_bdxfhs`dUbF(@>}lVrlqG!H{9!SD=1E352E_rm#37B zsRJaijA}+Jcn;2>Y;woL_>D8Ero9eN)!L*|J~@!UA=*#C)B8^;p(H3Jo`b1^fcG4I2XOZn`9 zG#XyIov1$Q(7Z-pIDV&u_QT+v>Oezj*tbza*4Z%l?o)76yfoZIQ%1?C5(r~oy#S>I z?9Z1g567rotxCUni0+9)QT)BXBBJyjVbSs6EGivn<_8Hz^ZuQx-`G zCBDv#W?4XZ-VnRJApP12u|7`-^QQMvFPlun&Ppj6qHN;f^A?4eZhyTJHOE}5z1 zn{$;svr#zG3y^Qd7m)Rqa9%Q(nk3Oyd00Q+Q0<|9a;N`uxxldfE+ zn|m>E8ktAA?1c=96)y#~(bn}zv|n8RYfCt%QZoA?mEK=Xob+`QsFQ2MzR*Uwq_&4F zN(aF0yOp8NpllvP8r5qTgQwM$jb$5jQwF{XtOKy(n)Iy-Ry*K`aJ3djptHn zPqI-aNg>b;qbtBCA5%%1{(T#`t374Xw1;%M_E8~a@(%Q4<|+7Q*cz(wtJb-$K1tXA zA(b-8?g{;TSIFJ)(N1bCI3Ku%QaJ!U4bSKQ;G>o2s3G@II67ofCi&k&SEJ250w0x| zd6(`jTr&pB8?ls1JaqPe%1QVta=@A#g-*7tgb_nv|G+|7q~%b_8w6dodNJ$)l_T(B z%hS>J*dO6yqAZekWl*_Z6}lS!OXD}6QaJ?Od`fBTdn@i(X~-O$G@YR)+Rk%dMo92A=Sb)TKk*Bm&1X6DHs zpobr*?c}OW1Nxe2s6*v`qq1Z?blTTKWoaq+y7uuqbhV|VFYP{{zkNWzvQWn4(@^Fy zuu+MEZoki>`QB*ww9)FvbaQum>$gk8&8r18lHQ`i5vgibe`!gCZ2 zm55iq%cZ&$`VX0sMf;hbA!gO)bULr2AZIT{l>mm&?w>g><)i%A!j3pp-#KCG4Uz{y})UKqFdyg$`++ z2+b{Cn)o2$Rz9^oxRrSGo!}O*SB1R_1{P3B&8GE}4rzQ19xl;{=JC)WmF({DqHc|V z*)b>75}VQBbN@hRZKBsOf#^j`p4Kd2uL_S$s6K>;ITFuxBb2^JnGZ zW;xwiXr!M}t`_t~yX_PBx=Jy$PUGJV=(9iR3@Jt%;-_Qx&q;`d`i$mSI=Tij2LsguhJvVDI9CeP+S9$sLU1}uC zKq+1fzNWi}ec|I$n01{>≧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

Y_3_JWE@;P)gtlX>0$LE?*jsh zPc6;y?twJt$*FkG2vzVIItY*y5lE>j%1tWqEIK^_P>7_L4!*pAah7LUZV+cp2<(Xm zcol~+R!@H7tIgnrzy;jaN25y^a{MOyamp=bIo}c( zJ)%h#4;j|CnHv9v4t^YCHQ3c6%S(^jSi-u^`~kWuMgA(u#cEy`%R9cot<@Ujy_(U_ z)I&(~G;DW)d?A&1r_+zgbr+tQ|DfmTB0(Nuw9<=h5CAmbJIqxZP zu9>kVa$Ma@_obZ>tRd7C9uM41Q)!_Q%D47<=6~e;gA4yjjLgtSHNq;;$+b7y*9}YZ z5MHLYR0ayYh?Q%UDCi4co~tj0e$u3a#6X^sL#F&Fhwfm3il@{@N6nu7l>+bS0fj$|rWx~DA9foCV(_nic^VXka=bK2nqTP@ORBklYM z*EI9>sXjbQfD#iAJx#>h6%{uk09CIrm9=HkECs#%aW*Xip{}1R4Z-^O^c;W*I|X7=Y>*`#X?=(iC|sr5JMKL=Sx#KiZ5xKYsP=X;_Bc= zI%iZJB0r~s1;Vp2 zz0M&46P1Q>1;ObS7-jpzNEu9L>+*d#SSO~>$o)c8-8r4oBr8*Jy$@0aNnBC$q)5Gf zNRqaxAPMn?01ZfM3IZ4+@gLKtKnJn$AJ+J!DSbBu4Aem!MYMvBCq0)elsvX)$}X-a zF$_C#$!DDlYY#wUj*g=JQ&?r}*3v)A$p1>Dsrq>XBcMNq&3!U%Uz}z!|NU7cD<9(D zTVMzQ(wL)M)R_Uga*3eq+MF0@#9Wt%qA*AsBYqR-Gs}OM8E$3lFg3Ju*SbjUBCR7_#%vrhdbaEDW zbpk_W(fCT|t&*MTdH3rxU?Nz3o7F}uT$!a#`;ji{4~qfxk+~VLztTWJIQOT4)0m+l zSd(O!Nn5|4FR`ELbZ|3WD&Nu^l{u`sOiw*z!5!YY2X8)5ZrwC_%0Bg}Kv-RKTG>gM zlBFHjSAs}K68K;rF;01?gXs6>v4b}az2vIC=o$>R^VvR0I0IqpPbjN>#?L9(myjT+ z1{$i&9RU}|(NH)P2@RebdbO_F`H6%m@bM|i*+xl*Yt5tUMs~xF>4;b1|7Uw&ND#~1 zGIi^gGcoS}ed_f`?(&w$-M#62Q29~yKRE$v>~wG>$M1cPrep5hGPLQ9fdKHJ_!}-w z(}d~Ea)5yCHz#@wey3!Wl8Yn42sWO--~KohuEJUx+85_$oTkdP-E+N$V1A*+ar{&r zWPEcfQ+)B<(f*6Qo`A~)K#QiZUo1_WM@i+0{lT4=9W861_G{D!&3t5?GSNmPtnB zhR9c$`_BLXgj7gJtNpYoV%HDpSH?kBxz41L+w;fyIgv^%~C z^~B>%U@3MiZd>+FEj2JlRNwl6eZzL@8Vh#kFW?6UZ24-5KQg(bdUO!{e`SJqsP5o| zJyu3UJOb#8vLv}bmq8~sS$p|CU;Nrb?C5%)sb(zBO0ntx$kB@vu?%%$%O;_Y&0qXT z>(71KF+|`0;{!;Nb>-fS%H9Qc+b{aMcjgKrXJQgsghxg&=+eLje6mAe0__Vgb3-aA z5cp0}*yA7oKj&|&{2pzT;UYv`-y0R0{&mD4ma^fu7>0ZPS8vEPbX6PNneCI@H6=(` zqb>MpM{qf$CGr6n8yXXnI7A$5P(G10Z1$OAxK1;IG)c6}mkYnHYwNVVd2?P6*2-Hw z2M;*4#kJ3DfGvm>zQ(x(ZT%c5hg!N0QEHVbJDB3_QfsBz@MzozH0e__6k6ILN>!0? zaugl>iG)t)GCwrj&CL9~M-AV&LMf_aB$6?TdS5PVaiXF5cYO`sssp8%>wh*6XY1&3 zQFN4cSu5z^?Dq#YY7TPzd=d(Scbk_Zen`yQiH$*(*(c@Zn*r}((xJ?mxf2q`XqO)J z!Kl*}rsN7*1Po53cq7=GAOT1@W#y?`A;ikpSFM*STcPIS z1;L{b7)L<#+^?h`ysvoHIA)zx8=jV)2vSxk8?4LYN#&Ek@wbb$EC7XwT7$L1AwR+Q zRc}5X_U$}$C!8pBvS2S$k3k75R=`(CnD9cXP11QerfwX_ zZnoliTuII{TIg{;aG#YwkR$JSN9Ggq+neZTaL9rrk8pLk6HOkAV)5zBKW@}q_gxy! zwYs%_6U}kI>N*=uP>9n*<;4(=be^2eIn4aT4AZ*)N%8q`E>j_X=eyORf*?I8upv!Z zhyo&agOF6d>Xo~rzyJCE>KDkq7O^|UUJKVW6wQFWs~t~EIy|Cb>7N6pme;fP8Tco{ z@cUIo#;C;MOnc@?g5Uz1885opgbBia+EO~H^`80iL(P(>pRV1hkff60s7qJpO||>G z@^$u#bScBZYvJ=lmgMo->a-hqj4+c)*8ZvU<>%kZL0nFxX`Dyino)$BajY|la*-L~ zdIJS{^@0_N%~JI_$hz`NL~z1e_=V3SWsCtht^WWu#UfwMehf;$7O#% z5{smIFD~W36e}QG*o%B=zns)|q`95$5S-_Bd-SsRd)us91GRV1a4g*qHp3Co-M~^c zXG|ONP*?WFhY@u(c%LN;^utMvfDHsKH9B++C>ef>?mc-5il2T#ev5QQfyx(Z;Age1 z+j^mL0UAA1K6^ENIpMzVU!x}szHC$)tU;D5#LRZ1GSr6SMA-4st}aN?U(7CJfvPFq zg+P<;mkt1G*aSdkzDpaomh-R+5Dw)9yV6J22~5&~*BeO;0w2x@flI~S*KN5ILv+C=;kl?Nz@ z0H6$Lk{bMWZO61R^*Q^kN>e30@F;x&B` zy*Tf&_N?JRHMxK#8iM`2!oLtoy5x1CB}XJ^3Eu>l^$x{7@&oGcTpNQiy`(Pd;KYlY zTOxgu+%si+=#hKYDVo%SfmY9 zc#XbQ#m(O_`8(L)8evGrGXPS@wZw(K9hjpY%&rs?;seb9WtHNs&!_jK1WLhu?3rdd zv7wZH6{m&rA9p z%b?C%)iPH&w>_Y%cNu$V-q=91pNo7t>NIIfM-7xY;O$_4ab!O_%ZNSgx3y7&5-;P^ zt?Fg`?+aUna^_zMUBZbSFC>O-po!f&W`ud^6Tps8cwDfB7sB_kMOdK1cEcASGyRa~ zFOBp@+cUOgCGm|1#R-)!NKYT+R4 zj>u&25`zaTB&efA?}hONw^iZ)LnGZ*qn8OA_BjBto42$m3x&^I%0w6_uR$1UEZL$G zVSMS8zHYKUHlT+g>Az~Fotuq3BZukLd`1!jBB~d^7#`i>VcWxAxEBq#7f;1qs!t%Y zO_{o?FrHJaaP|-^3~-kj180zmPesYuV;GVqEJnTccheM@Pa@Z4u%LhQi-<``1>Q6V z(gpf|#Nd1GC;BOIlBm9vjPovrP?fp0oS-kzwn?dD>3ojJh>G_PQXiq-%V}*feD!>P z2Wv%~NyAWHAqci)AOJ@mAoT3AzX`+uYq!y&H=@fcwCgE`*CRx*)`)17 zAdG}R^95&zPK4uNH;l1P3;2Mfw#{Dj@M zyRyJ~h`t-9GM;Snb-yQ$h^0vWye3bzz(-B4w_a^tb38b8Y%cS&PPH+?80a?hY^mJR zyLPzN1Ek=h{X=dstAj;Gqr3|Pkqa@$}UYs{H@lc6ko%o@= zL{5kgP|%p+W)egTdZ3Nw1FE&gUD4K0$i+ zJ^j(Bd;bE5!44_*Q^l)Ncz_0gf9a3Ba<}qI)kg~mHadIcd8u8=`Yl*djr7&4Zfl4C z|HcpIRVwK~cV>>|`&@%Ornw~s>l%41W`Dd%QK)U1qnAHHEyPoBnOMHOr3F5F$e#F8 z@8*;^hunboO6YtWqp@Dp2E6H@`25W)Km2p!;uqY#PHFp0vX$FOY`OT(*L^6D z+F^X0$E>8n`NFXEhPlD>=zkFZX-stWO$;<;f|(CHO(lR<#}D8dUEwjFEr>nz&in*) z>H5RDavL$cn}T}T+O~e36=w@NaGg*rc8eoKT~`SMV|ANqhv2S|5D_#+@8)+syoiix z6=`?b>jX3HEL)XQMK|+^c4x5V&$vlllo6n8K)KBx=jZ{Q=PT~h&8VQUY&)cVG%wDP z=_G+yzA%U9m4f@>Y%l-!OP^GTs5+&DK^uD;iyHRb)O9#XH?n=`?^m1oH5?~47wdBR zM~5|+i+>d2ZPh}4KabD0Yb?*kaFuE4BtkJCCUSp~gF8DE)O zL`(&JtVw?QR&s}i1G5|rAn2W}pztbVstMU%@FFy^j@q>~NNPvnx#Be9 zJG>ZNuf_T`C1N7m{%}F3%lMk;01W?c@|i;Y#&rbH!&KkxNrjE8&^T*Wdu`djnr{B7 zAXB}l6d}}tWLbD=d6K;;ZA$Yg8VR&qYB`t!4*^YB?$i7R`GXXYTl?vhsApy8ZtRimzQ-wORVd=^$ z(g-6ck^3uoL^dC?ti1c(t8~x*74>{rq;DYKyr1t4ORK7>_ZTgey`fiMZ(^f}{dAqU zd!N7&R7lKi%Ntvd+4=NHUcvIqvPuN>TK9F^Qy1R%UN7^2ONmxIOOt2JO?o>k-b0ya z&CKu3;9YAU8S|vkeYjUp>$&eXXFxEp0stgNf90&_36iRXp|TmW7zq0@`92nl>LVPz zyY8KK)E^HJPrDIh%xMA7?F$)9Xwwu zQV(u%ftdQ~?6gxZ`~D#i=8KjK#oQ17|NV2z@DpBSFg?jM;Ah>#(Ondj+4Ew|ZfV1^ zq+vdgv3a@OY|JBx3Cy;wd0o5I8%xC;I3w5*U}E0mGI^VRpFtI_q$ud zEZKBp<7KZTW?Sg6K%F;t(%wW`R20H6Vi<8Lg?Xowa=`AP^y z*2AP}M;wAxXO=;S?CcWf9TWV*ImJgb%|&px*!g;l>%RUXekMkE}H~P#*=; z7q}6K&3^BC_Zy3CM_gdi?UtdAat*&y^A&2S$ICAC+t~)0Y^p*LV3zeP@NkD&hYR=q zb`&SX4bV*h(?mj=B~3eyTK8hyTPLw(L4BE1taB)8$SHku5L^(-*SK#OF4rBNHJU@5 zD#hrsD~7a^?8k7GO{i=*tm)cJRNqe#6FjR6I6ZnwO_-52bci<9#7;a za9jH!A%gQs^pL`X@p;p7bEfJrC8YV{|B>k;+Yiq~4)hsj9p}<*2W6@EfuO&U)1w`v zf^+-9QFz5cQIoE}*FrPqg{GifxlJ3F8xMxBx1r}UC!lv%vDFdm8ySSL9?Hz}am0>} z&?CmOSRXGjZuV)uyO;W8z2Mq{x27Tqv)+dg0!DD{xm&j(IkuW@?=&*AwnVzqm=;7M z_o$P54y(cBYnYh%G8C^jp=NGsrKKIU%}tloD1YC4^ojr;{2pm>d;@4dNwE)FCs;F67F+jGV}mui>#r7ivf(_sKC!9J5p7>&`iQ6FBg| zg!<^v4XKkfANj!Jt@6B`HQ>68fOa>ae5;I^bH=eP;YyX&G}%q)wp{^#!DH_Dk_q zyhc4lPq(6HQz;MT`1o@^4@*c>G!K;m8`X z5|WWXJhku?srFVQZ2j(DQ}6u2;H&1+<1{wtn}ai7 z-hlu(XXsibE-x-O{tAq^7m@+>5Kw({G-{?XE7f6@lKoNPH)k^&B){meFzI9a)gHbS zTf`H{p}aKNdqKye%AfS6mIPRE9X=co)h9y`Lnt}NJ)LPb%1+C%8E3343kNV0v;WZQ ziT|IXPj)gPO#CA!v?Z8AZ|gW2s=1y@HuMNxv&`msS+iQ^U(WZd4pZBiY<~TWhO!go z_WeFK`GISRyEhuN7M$kKU_?^8`|%yD8sBxcq7Ec!aSjp5MQ(-A1_~L(CO=oAp-BIv zBQfj{hzdHlDG@Sfn!ItDBZo##cP4?z`SwdIV8nmJo+X|$PF{&WDdRj<(kIB@Xu5LG*oYY@iV21H-< zpNVZ-0@p)BihA7R%5NHEIQ>#-cl7x7(+<#sz$xWmOLS2N&2AU0 zSJhy&r!gBHoH|Rr3pcb!-+gJO4RCg|=A^uiS|z`(Nxec~6`J}e3t-H&=6kYf z(l7A=5ZegeFslBxk|F@o?I=yZr>OL<6_k2e$dcCsDiW9*NM9ox*hYpDok<}Pm}e^+ zGGO!piZ!yofS2CvCy!n1&%BARWV%IOGbWY2Tpyt_`}Q!VH0g*?U3p|Q;h4QSrj>u( zh}RJWMXOJ@%~JPqj=3L+=VmY6+y{BHT1MkYTY_*fy*(Z&@m)}0aK2R+#(Dzk=<}6Z z;m3WGa({98d*fh5<*WXH5{<7AZ{HtG8WIePI2MO=;sKEy{rWe?q$@H;PTB$=IxpER z<8e}1+$B$$H|VoIXr3T_y!-!DCyjUgyA=re>InY@&l_W#Jacl)o--$|Uiyd5NZrYE zX&BbbZ-LKQ%N2W}#Cg>_BWEuQA5pz&cfCIOQ#A?fhQMd7T7ka0h?9XgVPk0VCsdrn z7!G-hWz2X3-sYvnO7Zs9>Zk>F{k7e1SkeeGzf~T$wt8zSb@^~_5h397(>vFxK>9bO zJO`SMRg$ymBRUBa>m*!~T;u%(4OWnW4~)uGCNVw%pz?pFx{jQIp>zUG%ps8x@D5iS z4((`{QH0hJ+bo1{3WfX_RHZQ`xW_tx72t~QG=kc_AU?}SIR>u;J!AannZE-L1@Ftn z15r$p0);t<^1@7Wy)#IIqgEY*TZ%#94Z-LV7P414LrCeL2YeI%v7Zyr_!D77rFH=8AlT=x;C}6yGQt>3 zu1a^jmiIUvS_dn&9>Lqk7Sv03nw)7?DJ?jWwf45lm>)nsBI3E^VSmJ3w^!3&fD zDQ@${1F~I8I#oI{E2)S}KU-+UB=!E>*^fw#XH2=Kx8L_b=GnLmah_ft5W}dRA&O+E z!o!(-_PJ-0w!*uqqu!2rw#mzus6~SB`&M$(x~e?cu;Zl#y3! zmrjExHQ?YEj834ZpN|IG83$}^p9j)_L)7PK3W@f&x^c`0qQZJ3Xih%ohmyjbPHE1Q z@Bl9~o-axo^tk&g6lDX$vd(3n*xRSL!0b#d%Y=g-;K1fVjII85`(~ChGwrpC?|oN= z-2Lbp-;Kx=#nv#+d`luV@$<|`(ws>fb7Hc@{tprGFuiF3w2dg8~8eS%#L`?EJ&?28uSUZX)}OnS!CF?T>4%=Hd{v;A}l z!rOlf%u&IC)vRV*2q*^NYA)&P~tyUG8kJLVFaI$G&)J$8TnYWZ#fIW!L|-myymSFlop z99!rje}US5|NJg$F-!gGAA#g#~v*1|_u7bs)K zsq-~QBvQiug^Z(F8JR1SB3W(84n1PecYwk6KDh3?j)M)$qAr^CR-8&VdQjvy=L!g; zIcMu!g7xp&;$;Ux&f;k$%2ETVGC$C=$&74C_TnLd%#wgG-^|U3hLkZxJhDw;5KSnf z*uM>WRPavvdJf7TzvD49psx~O#Zp;5?YCXd8He+_GJSLF2pQRT+m)`~t>#%|Q!tCZ`DseB<`_yt%%`He6Qstp*? zIkM9y9}q1vGQFn5WkKZ?53AF}xg9QKw60vn<8R(;U{%nH4D=das0n>vXu%ni+L#AO zk>ejrKMV0&d+L|S73bH(r-||O*0o|F7jL$uhS5R_%Mkp#)<;QUf_{Ho{m1q?G9qjKoXKAiCq!Z-40V%pHGf zj{z{I9&#$D`+@BgMJ>u|&O^gg$O>OSZZSa+9P2WY=LHM#Hk6+EtQgr$mX;k{dUf6X zP8w7Pq*IcdHD{m)ly6OdutfgHla)GMh4|Rcmlb)qA?yOOr09TBrY8ikn zF1c2Bl`_d%cHRipN<4d1CTSwE8*qV7tJboChiCTTA!J?XP*jV}qOS`@@nL*hC27ej6Eja?H9b<0ZxEl-AH8Fx zZ|aV?IM)A{Il1&#L5~kFBhRE7nVCQS0k4gIEoH(ERl(=uUH8q|7 zb|>W2vp@f#3!_ZRTG7p=TCJXfr%@o1A4(Mhok3)+8qF@SF2(7bf%%fZHV)KI`hdI~ zUhDZ=#P#_~bcX5HB2%9IgJNO~&;R2O4p0Be`XlRJmo4L_3@_WCzxl`q* zlt!mQ>JAMrQ%}v%@0E;X1>*^1EE%zVJ5vn*c9Iy9qHFvZ*G4#?`BPLv6IH2w4JM@h@!?^y&zS8Kre7zu#+HdRsb3I9u33x?eVoD#p2Zd{|?CrA{p!#98 zSf0D=HSE&Z$Oe#RU5cVAKn7+K3wpk0>uN>bvhKVp?o@nQt^_!uqtG2W*6>+3sHk?G zKg%H1Bo2l;SbAb~R42mSXyGtk>jktog?hWQLQIm_aVAkSZ-O562p0GQMHSb5Vn;~G z_iV%YtPtJ@`}PeT z2;5F=j@ni6Ei`NY9Gkq$SCSSMp{BDX>>G_E2Nc!{;dFcnUh^Q~*mhB@*g~}Kc-bY<*7Ksx& zTHp^J&j1O52H_=~2M+V+% z7Wcw%w~^) zpg?<_?1I-y86(vMAMQ;`;&^O&+V6&_%$);&MZZN%jo(BDmsJNqg^Y;nV(~XB=f83VZ84vpk z4#p8*W;DEr10k?~bKGw{sqf^;QCox6yMr)qTR4kZMR(Qk^KK9Ay`_gR9PK%SGX2C; z;j@k{=B3-RTRJ|XteH5vG9lKnh_kXF)70wro(;v<5=8{5Kjkr;`5egso^ z>TRT%`1=H5=b}PJ{t`>7mY^_DnZhgw*<|!9d>xj|YvC~gi^3=OOJ-vcfFzV5QzjqD zQ-VctGt=*EyI9G!>hYIalBy9_gMau6msk~>vWQRchVh~dhRG-H$r2IHlUS+?bZwZc zeJNa4xRZ=@Qrxm%!v0Y0V5|k#89Z@_0x0saX(rfeTa%*<3pVWCZdH8M_vxCla~pXy zzpNzeG+-qvXC57^`k&G&$6ef`EGq&sWr`Vcz0_a^HZVrQw?3p0jHN3%d*B~jtt3Mu z$H00eKyP9*URtgyLW6*ns`$g#(0osypW=byb+}Vc%?XUrCzgU$yK?Mdyq&uPvkM>2 zF_*X}LsOJ+E5u5#e;OSL-!N`d4!zkOw=MYlS8xu}ha3$-gY!5sOS#_%=X=9i{RkHh z_dsnq_)lLbX!f-B?=Bhf41F!+=XCw)o9?7etV*`~f;y^}GnQjw4iF%8N4I>Zcn6;- z1k?0a#cCA%^KxJ3$f5MOfJm|)b7kh6P~)Yr4cdWvFGr<@Tej+d|FCLLH~LBl(qr6m zfKB3SG~qWYsBC#L9l!q78j^jsh|E!tZB$GMb5-`au5IE@TdayOaxCUVma;QK0j>BX$6KvKh`vw2@ zIMMEL#s3I&WzikS$38_It_Ne2uU-N+Bi>>Rd(04X+C z$Bwb%53TD2Uo<($000O483!waQymzNu=%QQ?__o`7&I`HF$O#})>jA8UjS|Ktcm=a z=%c&6%uyS7dV--n4Y5rj{yJ;7&HUn7d zCSWu6iXMs(>?pt?(er;21JvBPoPuB4LOk+E#*3<|YTgaj{~)#{_Wp0Mtz@Hgf#nHn7nuMdLa%sUokvmyQic*E!3>QSR#L$DL6-%w0Beqe*@Tr*{ctY=z zqiYrA{5G0b*AB9#!DxYb5QYa}X%{m^H&*y(*dt`E`@rSE00ol!%mf?)tQoQj(!qLT zR4rKO0wZ>-NUDQAOr4p*tH%PoN)NimbPHOx2hCsr0r&!#PdH57>#J!7pA|0*$v(ed zmd~JL*`aOrzcfumiuGt1&zF%^76TYijuR7jA@hhN{0$H^8r3a+-N?+sfLR!k;##&x z0!0u#=UqmvW2HH=gd*6Gb@+Mz@io!+H??#`RWZx&5;}ii)MMrK$#^cbyA@}4Xgy%j zbb?E~D5EsJj=tlhPf$kep5#SH*~~Y2+iRk=dT27wZLOPc&N2j$^4NK0iDC7Dn_2E- z&%t)bndGvQq;;TNpi!1CB8(E04Xa=G#Mj}<2;mvtGsa=iljyY7a&gZE&UKA)2k;DE zIR^V~mrp8p-(I<5B`@7nJFaK0rjUbSV*CI65oKEV!}nmkD-69}yTI`F^pVYfFgC%O zDYH*N@A-olZTOsmYdqC8ma~ws$>`(-^X&0%pL>H^(;h+<5-BDPryr$DxpSyJVnIYm z=BO`=7Q4&k&+cAX#1u*{h(&$l5OTBDj2*#E7o%g#%%-rBWUa8#5xq2reDiKeIsRG{ zkug`%u8P|HksoWX^otXNs5ob^QYrcJP1%+(7kl3>s!vLW58%p;*nchd9Vy>awVWg6 zNKlNk0TlCY^Gjzt#q|xA5t`!fzwZQEk(n%xup<`K+Pc6@7LQ+2?RH7w*x{&9uUHW0 zDq9B8Nbi+>B;OdBw^%w^HngLv{`@t1V%wC4huCbuAJQfp}-pi|CJ7aJZa$n*< z4Wb!i?(aIz(J%&;-*|A4ac|&NmKWOgaoYP@l%|@-B9Hr9{EUQYG3LHUo#(v*i6&lH z90ZLR7QOiTt8b*-nJNohvcDnP(DucMleW2|YI5u5tn+7?Td^=SqVdwc*p|8?c8-oUiV?9>XhgQ_Tb`x z&BZQ=fkO!(XYhC>{I(XQB58LC0Lk-+zrRuu_%+lGfu9?L`-cF!ofK&I_w0y-!6dX8 zeoiVfFIanx^r3AFTm#)xQOvV0Wt;F%R@JEG09{KRgrUWrs(mCaCY7 zE^_U%y@oUH=If=kPdxC|mb98;C_^C%L0nU?cF%Mf_RyAoZ>@uT>H$jq=EQCH!64{7 zNPhIwjuj~09W2gI(vN$G8=XL2*qK=4ku?vNT)YnmdQ95bpWO&c%oy=+njjJ@lzln9 zgxCbWTx9EEhtbRib^_1`Pq(#2e9m$0<2-DalZhJT5F-bmBk^}i{*)=-R+3KEz%nTY z{nV2sw_C?FBau-bW}Ql^M}f<|CcXf{V|7!q`8D0m+4vLQSXh&cbAY8D+3)dvI?^pl z3qta_<*R(T&CWw?G%Py&>7$a;!jTAtOJ z2j7dnWx(})&Fn;O^d3>y*?guaZC-k7&60{6a}+4W@Ge1EEW^TP`Y~~YT|7+~=nkwc zaXm$fTPa5_ohR^pl)gITAUF6NkFW04d4Ath>%<Wl00sEg=DUJas#>YOBw{FMmoj>sC&<$$6Vx;xN_4 zfyD8naxW3(V*FGfbf_TE9TA6Oc7Q$Kk7gbbhePjIkc8L^8g=zL#jhx$=3-WZMy_$h z7r$|SZrzpddISt{ZprW?=R3Dd_AB&+n>c+^8geIe!iAfpiNDQ}Q`vYpLW6h07FuwgPghs6xsR9uBih5R3N;=%4YVVz? z`)}Rn+TIt=`eGxplJ~!@ryaBTX7PSZD?s1q%^W7uBoR_Ak$)>xIhoJP!d{DcG1|x8 zF4bqw$vgud>PzU#p})Xwh7#=cd|i&`#5phJQR7(tTn?0fP=H?OMXh^A(cp5f3dNQr zFR(Z|&aKl<)j;JaCYn~?G{GCKQNzetvjTV@yI<|{T6RpC-4N3h zSn5?rVD3RlL!D4vU1*&Hhbs_UX|j zVV2KMnPl$qu-`$bkn_O7G0sdOCH;loEk4K2n5;<4CO4)vs}~`fL+!KyiBKN4JV&f` zLkui#V=%?~@(nfrj6!cHwr-I?$YP2|zrbmFnbmfG5&}`mS<`M$;X>02|1r9vIK6v{ zXKunHFlnPcbfmuOeRs_hXJ8H&Ozu z|93T(58hteYxl~L^XJPHF|7F}bW-3qQsYg{;Xja2HAj(tqc;g`>9ZLlSzriQQ$7@ zY?#zNBnY|wrZ(y>|47^G`}J&lIu`on`Hr9GZNkA9yC!cGFq4Ygov`-n)GS{A$^*Re(UrH}bSagXd8L&d#$!y| zs_Ok2J$6)oZkAL$BZ=N(iY=Kt7H^ju2#k%km6;oDW5}%)LiI|sG|`C+023A<35yV2 z46t}$?{jvC-UD)zqq!uPMN@9M{2wUM{J&0Naf>iE6jbKofb}zhy7^zLALHn%W{KBr zx?5Wjr~R}}6XeJFMKvesZm($jy=xyY4ok*Zndl>jcM4eC;VJHOUq3+cuu?;6*@jJ` z{tKOtR^mE?1qs72 zrkoD5LmCr>Oj(v_CG&rvpsn8gXK6>c>AHJvC4OeK01`O=_eBbJADr@+6cF?CV*^Ta z;qRE#Y~hDW|E@;DwmhX_k?`<%vmLX?zxJ1XQf!my_I*AP9P0?+k|X*e#RK{9($h+< zKPEI)+S1SkUte9%|N3L#ao#K>KCe5qY`I;yiA&#B?Au_o-Y@NEAjI^rL|_f3VEAPD zt1?p6mWJGUgk<1m$rSpCNie+nA;dwL%@AbuBQofcrE+M0_DE3RzL#WrTXO?MmQ@Gg zK%&geOu=m)%Y4EM2nRsTQ$uZ<5=zt!L}zzBE?z5@eeOw{r#{Qv>W0fEhoEo~^)L?H zCxLM0BmfJyU?Kya@=OFlI9%zP+7~f=UOFNZ(QJSnl)H zoJ&?VC=jOX-*2@w-uciK>K#{y2w*^Bh?!t3IFoNCThcD6KdRK%>AhtxeI@=bwbL3& z8@kkxumh>ETF;PG*~cNrr4cFTVgDb6ULkl$fVJiTqCFJ8_wmut7_89Sg^S(7ZOF^r zI9DdP5^d&Jd8XvLAvvfGZcV1$O(Pzjbp?_WRwbgWa(k}t&Zwo9j(Ba#&F=bh1Wot` z|8Pbu zo~-^{j18<~>&OWrG}_j*x8Eg&maRVubPt1#V$jDdXUppdkj$QVIX#LJ%t#5Yy=~(f zlnh`dFmRM~RgQ8=+#lbDXB<(|>I#TbMy~5%E}F7!Gkg;b^rTGQ5G6XBIj=NJ)z(MN zWl!c{Vc0-*%RcS@>{=K8AU~G!k=!>HnTdAMg9G_ETp>7VCKDO->P-f;?^ThA;&{^P z+d>GTxQ}<0=6{)Bp?2zE|7=#6IoVa-Xm_{lEzqT$Ibb0+sI;V~tAI)hmrq2fy*pW; z-jS6TJOLH?xvnbYwl5k3D`POMq>(NQ4Ewa9Bu}(VeF|l^0;lepJ?)rQi_|7a-Cswr0e0sF2?hWlaXOa@oHB$6FX&tj>Ije!8s02A}C`C zr<ka15D+erPCE^>}cGQq1I?7#tL6`AS-D^$^~6v@yh&|t*@ z4@-pIeoG5}!Z?B^^x5lzZXv5b|3dRZ?jkx^p9ugrn3#=b(}HJ{*fdSbg#^E{bz9_d z6q>mbSpfn4uITm^GiL$1Qei4Q>?%|Q|BTf$tmW|shR4}{we?9Q#Wc5#OoKz6+c$VBz7oA|;vD{39VyN}9)qV_5GNVdpAouA=zHi~W73Dz^hhE)_hK;4K)?$(75)HLBF+f_SDd-BddDqEhrZ)60{{d{BzkjyRh2 zQ@;xNQzh%xh?JaP#A44IfnpF=Cb31!BFfXhHnyZBb{N&{aHoA#GTKwtlP1Ljx0>^P zZS~3gcl3XQ&w3R8F4>5A@fNd|r}sNWZnr1RgO#xK;^MP-Re^Ukk#VBV4DJXL`?JGl z7%h2wzQGEBPWJDCo09-lams#ys zDag}!EA;9>@oT8ECfsJqqstDfuKU7nM9-2)wxaoeQA_%Cw6mu_BxWuPB3qLyfG?z# zwo}d6+0C%*ul&Q)q1ic2?2hOD#DtyWk-A-M_v9zrG|@ODuh4$j-{9cbtfUq(Jix;B zKmbmlm>|di1woTNj0(Ae%HSjvbx&M*T})pDpUXsx{!_FUN7nZYIpY3y*-B^B9icMn zG{dJ5M)EUSsw*US2BBkDm%7A=W`o9_!?|#sH``mh{O0yCfBSW5eD0z08x$ zF;{xLv^a$+E#9NGeEurpK#!FZe9<) z1^=lFuST4ircbUxwW|BC%NkzdD^)gVg}R8`t_;sN=4^UUIlWX*;x_=lmC@L2-QMK~ zzbRCk0+*17El-m>w<8{(T0M7p>Q4@?a|}~W9z2eA*dH-E;4K!TV%hfmv&5L;G*~9j z8g14IMbaz$WZ9^h^mx0lz+}u5q)w-F`W#I(i_|c;yDH39Ja@T09!^^DP_Xm*xve zMu4=bLVh}1OMCuQYm7fa>-!^I_YOTV3|wfqd0amRqN@*}1-xKF3iAm;b3id|X3C1w zoeO_*Up8%vFKF`7M7%e&|NO3M<(Spo*`qCS&+O=}c@-wRxpA^w4*M`HdRosuc>oB_ zWcHyn*Cm6!s%p-BbDlKIA-{Id!$Q?dG%E|ucdWJ+b1VOkQ#yApRsD@ykIJ3$|ABvN zt-IY^Yq>k;M1PT|&yNlvrMG=XFIjc*MgrrBKREL0-Y=}rpHNrG<7gc zSY4y?5UqV}FmBl?FN-iG8}{(3j_=nuvS0>icfIn5@%g$i6?k24E%|qIs2*dV z2fXQ%1<{T2Pior_?sp?j(?~k5>=8J49}&6kS>ioX)rb85IbiJy@XAB!ZDBJ*NVi)H z&VekG_fs93)`19J%lL+eyVHuCEVZl?V=8O@MzB)2Llkv1H*6EUFx*?H2+cx&8FZcM zAH#YJF3)#WYCHE>0Mq_)>HQ}>AhatSi%)JxG~nJHV+qyH3ukT}N;{>x`|NW1DxJi6OmtdC3tNXnsw*|PhVficTW>M!GR`5a*npyVl%YG2+4( zXhDI?^z4ej&O!G*G@X#(jZaLpLl#?%t+z;ri7w;#n!i2X8(M!VCAUNTk}QBho_MDaP-;1YBs7<8aC8a+vb^!+tx{t5F~8nq^8NwH*BG1 zQ%Xgholi94Ov&5Drbt6D8Dz+d0S_a;QMLCP0RB+NJS+s2d)CuE#xts^chkO|Gs%Ai zIg(5h$@|GbQPkvF?Y{HoECnecHhJfCq zpv*|t?!tb0NBn{Jqk21*3cpw=bjIJ{px^*NCjsqyzkz(!8%wxn3Z&-WTE=30KB5H& zVJr%)InwwfTe8$83otwUN60jIzI z*M*YCa3ntE`QzNIy+4Q^+}AF=>!-+0Q-JIp>H5}pHX zvR9_V-eqVGU9;Oc9T2)F?my!~9J~8KS@VKh?RyGs!VSfvrlTH_CO_`yHK_ojUJ> zt}WZHR8Xl~DP1;_uH+eJcR4!7L=y^NkBITZwqQ^&Wdjo!vVhd2hCXO8#27Qu{Zw(V-fx#YYW>N?b`}InD zc&>|)LR~sC`7nH!jndwOY02NY^eT)Yx;BCeAgRrWyzsldRmg5;Vb=Q&83L_?UbkzlgPaB2$CH0?P#q7F z^U3*%q*K#*;TGn!$&}#3`Zx&Q8oihcMk#riZl!{p>w-_y6|Z8-qs@EDvPY;?ou~J= z`lsf#ow<+72d&DN+fCMf5~<%^Kk@r(AbNe_NM7`AJ~1IyA=ID_ zFIE?Dta?iSWz`5+6hb{ASbNY_D$s7$Hu6SFKJ&(lWAALJ)=_a7Zhx-fL*WAXgiQ^* zAO;0Ff|h27&bQ8t(Z*XibINR70(gw*JjZ2v8uNvPbV$W0wGU1d|Nl>H;FpqdkPy#I ztphcq{x?iV$`-_uG6|59PzInczu=MDT0GvpTk5U-=dDZ_v70c$Dqqi`po&Vr(gh}{ z%4^?+U%&0pm{h|zU=-S*_*khi^5)o75x_-(6hOJ@p7AnhAGR&BvE z^I}_M2+voo>-W|OIjLu6V>fELQxaJRSkc|(vPG=wro`Du_LkInd@58NqTm8FZ%Y`4 zA6qYhuZ?FkWp z5E-r4J#eNosEi*VMajo~p>qLi-?9#}i?AG>yGehC__m;p9*CZcqWY5NTp5$wJg`Qe-KghUch$ePx9XW=e zakwv-=PUMH`Zv>zmbbYlPS^=hLpOjLlw*ZHaSyb^C^K55Qx7z6QiL^tHpn{+&-y<5z~zSUV{8({;XDf{UmBw50VcVC6pU6%OjsEg{dGG?pa1DT@?TS zrvjpE5wl+tTG2rpaVMH|HW{3mEJ3Ggz0f&pOAK=PB%G8W z@_1XJJ2OH)r^Yq8&z(nWt?^Pui{pFDHq1dusF?F2caVnFF+$B*u!qx$6fz!O{ha}n zq`uWTs;qg}L;9QtL!`&9Qt42!=TUFyPtCX4K)nf6u8Z^>fGjRdQ908UQY~*y+FIG` zh>`nmTQqeb2d%f^M=A9$qfqlmr8q z-qaBK^1Q3fK#RQtdhSZ{GGOBiC^4jb!$3sv&=EW|2i1Wais|y**PB=g9)L;wKIi5% zj;!&faBFHV^CytsHQde{`a!-t?o`y9^Eh%@VG6kdQ|kvpe*C_-?Z>J|?rICvANXH# zgyZkU>DeYWSED=f&%m%PTtOJo1c%;3hAk3? z!&K>bmW9(4B5!H@_4GRCt_hw!=Tb4%+Bp${?^$SFx|3DG<)Q^zcFpcgRyjsMbx0^1 zE`m<7f4SlG(r)lN{Zao1)~jheol0XrWL$&Q(NG~C<3C*b`%ntN00f2SErf=Wxir#X z?CyW|y0|blgN4Im$KI;@iGTmzfdaQCQvU@pS2|gIyoZ}!>NL^l7}o}^Ku=y$2BNB) z5DyEq(eGIbibQF}UR%$^x{-ly%7rTC$KRD=F*g9c6a0Lg`b3lZciD=S!ci z;L*M0X|o-ZjW7y81l&vjuuYo_T-37U%4un6XYbu;Dd~zyZ2CF4D?{F(1PJyVR|TvT zmyEt)$Q^<&6q09P=q$X@&<$gr6~iVC#jh!_Gl!=bljmfzs9vx%;b+X~fNGg_{ZaOv zF024(UY{KUDaZ;+XNVi2uBR>0@|e4MEzsa(3HPmLABIV43;FZRSGYXJ*pFYWsXp2;{?>I9BDoY@gc@6`7e-4Y5ikD_M?G8$YW)JsX05 zcy;u?ZNniY>K0p7VR%D;9E0KCB8+y_ z4*)^hMI=yr_)UOX0mo}QGyR07F%rM2`{IkGeR%?g!uPm`ahk+HNg1R@ZQm;Dq~_b~ zM7^3xYjNg!ckO5)Reh+Y5bVLq?{EAAiCJi?)enw7MpM!?ng2?pa|%v{EYd_G7Vn*& z5gzx!jq8a}+OFMmG-9fWsl!Q`AVtR}<}|{H!osL%vVd8Q&$A+EhB=}YgFSgGPfre$ zpd{W>V~`Q;&y^=nzMAi$;qOy10?A$UUPckovVvV9B6p9mkhcfyoDbbZy)nFYMUjPw z30GWV)5+%|t7*aX7S!kY37D40jp!J})iZHn^J0XdIOipj8Vd)U>kr95u9xC&ryAWZ zX9F)F;uBfhqcurJoQ9>-EgIor)P^B$U@Zb{#f#s;S!}xSBt!5aoadqQd@!ZArA0x6 z0Z=0uvH3wgH&{bCbB=1!KXme|ZKlqXkV|R6M8r*urS-EU3AYRr-UHudw#17VQ8@n5 zBUH8S9Cp<$dxw2BOJ3pgosG5th4RJ$G)V{aATCFc^lA$V}t;O;Ju`|f*ns_Rts z`FCn{@4Z%k9jUJR1s#PL1quoZT|r(300jjD{Ler__?YRo%2@q)A($(Dk%4;u&-1gZ zEcs&w*+pK@9SRB+=RX4)Dl3QZV-nFrK}8mE3jqs_8*A%;#2*TZ5=ucvQp;!gq{}zO zV##RZvF*N_^=_4$+1|w8WOmK2eG0}d#0!}!hCqZ1F~Ry9G`cCq-PQx?%034yNPrvz zXq6BSM8H5Jq>;c*f~sQWwJV?V*Y!MA($P6nlCg3)@A5x?aW#?Y{8N#3ne+Qg7EVhC z6L%5y=XLz^p!56Z*TCS=DZ<#Eh|BL3@1GmfOwsc&fpGdj7JP?08v9iOrFYkCQPa9J z*L#9_edW73;%H6;d-QoA7@_L+ZFGOq1Z9TCXXTip1YcHsW6AbqbcQ`|Z)94irE3rC z%)g(cJSP6%XQV(`Dzu9dDQXi}ahA66z=$D-7Nz#CI2-;%y7A_X({z-8&f2dRxKi?% zOJdNF(7CL}>?99i8innogiDlL z8W%5eh4tw8xRF?;y@M6s8SHYD_+9WZ7h}V6vyPzWWjD1Z=q-8$(V9Li@EN1CLY<|H zUtuOWR%zeXxI0wYyf|VJ7@)>w%;<9QAjyRDk3lUYiIPUD{S{M+x^_Gl&8!J&4^W;#w2jPe5oegsEzc5lxSn7PQe z{BjQ!gu8ww8-c;g$DdZizGiA}my;Rzyy(3xP*TTeu^EEP!_Vh)#GL;n3U~JlQu&2Q zJ|)ZMBh|=t;`Gq9NPu`R229F6Mx0}_S{R7=X(3+1v`V%NId+aV%YO7g710*WCTxz; zwx!>xYa(_KUa^S@qvugYnOq?JD(vJ1I87tx-7%2M=DJFE+N>cdV`h#`1ScONyoWDy zo@0&$ag45~>u6^UDu7e?u_*37I@v-`*;Mp=Yx8o7=Inta_s`ir~Nq`tB;}-6ix_Kdf7S58QjOb;GRL7n!66-*DW?~&(Vcnxc)=_Yd7+dV`2-=vL@A)Eub-{=gD{nJ=?gY# z0GkxaxD~iaoSo|%x>SY;WhxlIL8nR$$dvT%Sj4(Dg#s#qC#nd$$^V39l7Ah38-1J1 zZ6d|RTU$M@%a|89!Po$NU72bby}P5Mgv%P;SQHduF*w zXyGKvN?wX?tKox1S0t$z{P8E>D#$A7$tiknoQ$~2C~ZZhIh$1@Gm|||MfEkT7PHHu zwm>xrkX7ojO^AxCDEmS=#wG&`qdzX>+M=?l5I*Z8MOA-2y7`w{_B^ZekcsGN{nK4- z&!lUmZ2OAL!3od~9b(#KfO~Rs^4He8_4xO5xr*wRzu5Bf$u$2W&xeHc=?QbGQ)n3Q z)bsd#{FQ>KW=t~mygdz8J;{Bcl0DbIE;W9af=4|Nr$Hb8EUNF_c2h41BKdQ$Y{C~ema zmD4F#;1Mp4dxMYSExkS?JWS*Z9n^8EglZc0_Q3X(R*#E8K1m7r}dSLyO-^&rMG=<(=G`vE>N@AM-1R6Po**4?{j82e~ezp@Y9eftocFm z;l51!>IwQ(=b6IL=kSDmW6?Ojr`u%;$!vOj4!x&iKB2iSj2(@8j+)z>+U-#k5E<4CrIuEN<~BWv&IIB+5WgR(xB zA!}?Ex=yvx?trMb`_b?3m!qJiHkW*|7`ZY=oept`O{i2j3ToVM4@3?`L`MjZkoti< zM~M2FJ2bX7%hjs8$u4Zq^>gR@tGMUrWK%+*R3MKW2(DJ{pMgITqeS#u(&8OGem zL!#1Bx=T^o;shN9b--KtIgalga*QfxtWfhDRIVvhA~cQtPm(eo1F}A#<|VAuE;^n4 zF{MoWARteQDT33fG+i-~4*66El^HIPg)-G`(=I+G3Jwp!C+~~3^>-=Ut#WGOA*YZ- zC2b@F*+r0P9(3d(nL)P8(~@;tz@25>nfO-~oK`*DAjlOH5rDy0k)rV{R48!!w1 zqSvfVg?Wv_v}qja^7D!?3Bosghrkt!#RVHH6`JbN&PkHpF?)!k|{5xfbkPjw=**Xoa845hYQ+?va6GDZwDC#Wc0PMCUY%oD$kg8L%rN$8>#f z`nd$yB#(vw;-yp3>j{djc^NbTU}p)aUZng4q2*C*J~G7=_Dw3VKO_po5h!Xr1Uoo6 zo{3%ep}rdveJZU=JM|KhqVYSjkOT;G4q4Ro8kQ*1z}sT~Q;0+f*e?f+4F44MLORYE zKpO*&N}!BEQzJ(G5m6T0(2gHB?{&GY=|>Ym<}@w#ODa4K`7GROSE3d8I03V4)7WHl znvvQ{($Fwt>fHh_M2iFqp&MdcsQASo#UL0!OSDhaIRBMrg4$(!@D}aVkLn7Mc36Z|Nn3fG>ddBblj#XK`C?Q4X6WOP#=m~j~A7u}=)m@Rw5tKoO@~TzU2%TQGyrZ9&<4SJaC1A6h9sQaf?ww{moh z9+yLPl74OB~eUcbiGM^?@%Y?Aa9!&hjTlRCzQekiyWwM^Sq z_H~Q-K_MrPr*5KeIffEiN36gip9oY-^daxwcO2!2Nxk9d`;C-qmlu5Gn}(n6^ALxV zQuvhtEmx*29M|}7lkr21kZqq;k?Swx4Ih=jK(el;@!x>RLpz1pC|;%lwF8A>JYadR zJCx7`HKK*zm)V`_KcVpUao)gockIDbZwTW#yJru86GhV>Q3` z+ys_-zX?ZleuNM_zc?H6i9@eWsyPRfD|y&Yd}bzzyBT7N^$BbV_{B>JmP3-{y&=rb z#~QjDzfkAjTR!?0aJjo*n*TOJ@UG$LrAXFsI#CrFNuN8;J+!o>ib+#+j^sms&@3im^1tule^7-3)ig0o6%LdL@uO#vU5f@i zntDag13=b}wt7LAIQgJX{aEAo`SYpkp*Y{ku8Yo7Cr6hd*qXN@7q@9>`tK`V#*uAD z2zIH@0FdYH7ig+r55=iSZu^VndXd<(?%G9AvIwZ9VM=Rf0+>EL!tp3G*raO& zEc6XsItDK;Pyz75aT2AUD%BQqgEvnMaBRIexrS31*$P8Mj@rF{EO^*p9XT%*S+O}0 z=?pw%5-^=F!&WZdnWaf(In@J7a@}8kG-Du%55|{U(3w(BISXU zNUe?(beLfEmL@B7jSe#U#YR)>$oTx(lS7yp;dDsag-Yg*YMYF#Ed!%>Q+fGXjvKR7 zre~q|eqpMEQ!=s$L3Tk=`b|pj^)_FuRRyW5_Q z67l!_o%gzoBZXJ#b#{@6di`N64W6cK8BvxwxzCI)KW zCV&JMZ2Z`k`p3bNEyB~Z?@l(}a4x+S-ks(tCRL(+SPsczuK>HRa2hTVqBR0v8n?z= z^y{Kz#HZ@{1;-@bgI<<_c8IQQtdhb{KHy9mx@%1>6 zs#b1hN#FYT$WqES5(QplW0X!=dR1j{v7(`QN`hpo;+;oc4eNpY)GImyRkviZLZ>)=rx(-#62% zVSKgj^DikPv<$20({V=`uq*h zi66*v*Lcl=i*0Jiez))Dh9?97$Mq)zK6aRnUC<(JVnBQ)9}$b`GnwD!&!MMVrYo+v z(VGnPk{jy&;oby^#J!b+N0kAzv3 zvjD948JVQ}7gJ$l<0_n-#zg>TELp1VngpUBZfpm6S_>-^Ii!pd$h<&~n;4+r*SJ*G zAT5;4I=y0I4-|djUWZ25yl**%HsA5ugQ{}I6~4DBc8&GGf77Bw>1IC@aZAwreplDM zvN!0i>$BB&h@(4c5^y=Yc5mGMZ$g;jKI~utoJ0V|Bmr3MC&s9_e|v?7fFyXCOKcWe zS|47n2Vz;7N~ovxJ(3DJ99U~y_Qpm@JFjtTg7;U-V^px|ZL3442dfSQ3TBqg{OxhX zQOZ?Sv1DWurbD_(FPi7k*r;a+yb}AQ{luoPu|~YFekR2YCSg}<@)`R?QZiiTJyYxy zO$LEHlF4JSdWmD5C#j&R$fT(o6m(HJ&^mDdXz*NUbvif9817@KxPcLWbYV>BWfBWT z#1zW4`cPquiv9R*_JeMJe>o4{&=r3j44e!&yZ+%cR-j3Ldg{H(mKXG-r#OdFX6z{r z!kKv;HveO9DPfR1y68Hl?QR({ZlBKZ7zHTEkVO`7NiAl~UJ+)ga=LX7SGMEqql~4w z2#DHc%AIFJC8!QfQC3#lRlFe^zy@skqHO36tg+dy%9b+$^`mxEe%W01 zyO^R)f*B(KnZ2m30ehqdmB*yr*G^2D+*;A8uNh>)Am0+4^OK46(TMkwy0*(S>wLd& zg}9~L{#TH;>y!CoI$N)o3hcMbKOiTo7)2_rX;jWGA6Vs$1opd&gg{@6Z|3-Z0VQ}a zRJlCXRk9?xA;1<40An$|#>lR0TfS)V`RA1HSEAupPedkxY9ufqp1$kFrKR1cX%y=k z7MIs2>{u(4%-Si9Lm8<2LoZayG9T7Q|xp{WAyZlXp~;beD65 zyT$>HiMg@guLY&d>{nmzXa)y*Y)zMhVTfKW2VP%42d$unx&!?7i?w1`x%vr^U=QB* zH3fpT%esd%%ep07>IQ*L;q*oxm_i3J>GvE$swZ#RzQj5me0X{E)MgWp%M)^$p`)@zoIz< zpLCI_Yw-kIJ6n8Y{{Y#&k+%dU(K63!!{W*pMGNDdLC- zGOe0AV|w}Ah>S)CQ9dnpB4$MJMa4qkzPGc9F`+Yf^+(>@%l@w#Hce8Jl~r+0vAQ~! z+@}|j-4ViYu|BkI3@{#Ha$&63IV){ON-oL5QoCQK9aXUwo7L_%ezu*EYu^)uuad+w zA&N3e9w)ZBJX=0B`$t!A*-5;{#`;d*A!<%LETptdy>D1pNSCurBx0bxV+G2dA&tpr zWC(+&YK`tA)ojaBXV66|!eqqNAxZKYJM)reV03f@kIdikOA9 z=La zprAVv({i@!?t0EIkz<}8eIrI|f#Qj-nYQ|$8^7^*_9~T~`Ga}|{?c;4XASHAwXDN2 z<(_Pn)z{^qV*N=&{bAU-G^;r7FGN>8SFqlmmS)z|-(S?ay*IWry*FNuzQ6f-y}#jg z9w%z+N6l$R;5PFQINKw|!x_KEBPYBySyAYcIfuCimTx=Zz0C zB>t=rjszX{y>e%|(0VpC^CVp*K^iBQFbz$Xtwj+CV@bpgr#!EiGHNNZQMQkWr8m&P zaaVlk^}Y$oY`8ZJxMEcE#z~lVa^?@0RVSr*p7JO6FXH!J=;|TESBi8q(*QdDoi_(h zWZ=u%0lWyR>wF)nLAx+>c6jBT-8{1@n)7@|V6byxRk2n%WjOU2#6yS-4ikZ@L`sGb zj-iCZc4U5D$aQtAxx*Rc=7(~~2sj;?bxca|>fj+k=hIDFa%|4X$SURIi^PgjZIN!7 z>AMzDOhqYCOy!7yF>|bMZ?$nYY?tDVBA?QKuVH46HZkg~^DF3{dvf-kpFcl!VEI;X z2EgMmu2yFzAc~H{?Ze#iPl>Wr9E9mUibzIDM{?#qWCN)=7_WCRB57Htixhy_f=w%gqD185V%cFAA_%9XI+$>dgiq`T^tw1~T zcnFit_I!e^c$t)*UB`q@>+;gEJ&;I4-Qcm^?$yDT&a%Ms6S@X1{eE7AZpFhcuE8An z!Vn2SVaAmL#-tR}ZB1_ztBC~0jaMt4`&|@Nwrm7%EDV*F>S|#IC@S8{e8VCE%1?(^ zku0>mY%47~Wk+5T7wntGK?MYV-&2d+Qh;pab9c$~lRn>nG zW@pm=KndKd$R}8^u6D>DbeAcsbkhntD0X8f{Av|4Q{>sVAM~pGaNqNw^sy&SK~8h)zl~saj9S3fjHyWPwo-bF1y_D-Q>Jelz2^<|JVz^WacWpn+8^Dq z*z`_UA`++R@KcQ`*gE;fx?_A#+(h8j&C%O70#ML7{+Bq zOV6>Q@5b*hRS7FX^YXa@{{nO_@rTIxYK*NOce%%6bP4nGAeuo|)dY>(;d687^DW-B z3?p+)ii{q1hhD-w1|6p8yMRo$x)Z3hvWwL3^T^Q))l*JQt;gNMnW6&mn4vVP9Lqmv zGF0u;$5SoYas{LJ;YkJjVvZhgmu&#KKmXyvYSGhun6OW@<2ski5vc6W&JLz4Mjc+S z2)Yw_W+2~G6bSh*yaBBP4HVIjYQhwCAxcqzD~X9MgQbDnHsC-Hb7x7spbD{o979LX zs0lPcwEx+vR8ZAr4FDT4C&F}?G{9+$x+u6x@Bt=*ML{~{^Qg+FvN|_mE#UF7tHN|* zIa2#-la2}g?mHfKlQubO)1VEsjmy7*IYL1{?5aXj-Xzw^%+nm$cs!sIub{JZ(6|A2 zdt5QyAx1lZ=X)-_JNy>?L{`2#1g~`u;bt$ShgDev-6D zAAjQGuQs%@zZ6Og2b70ji8vh5jORA-PUMP?t~%2D+^{;E2iT=)tw6g+TxaN29rAbs zO$=xmWR%Ay+U$O_$P0YAy)__0cQYn)r!6^0I}Q4A)d%fv2|WpG_VCd2OdIqX{LKF{ z!2W8#e)n1Yd7QovL~b}=L~{RMpf4g)ARhZ0+T_c0xnQn@JPGG9Xc(e zBYt6ngW93yiHeRfls`D+lIgNqS4uQC`iRFD7vWMBV<|5*0PXUKjDOeIblG#t>beF4 zF*YXOK7W@)Y2+)mjfxXK`LNr2O9a49zudzmLAx1EExNK_S`|`-4wZ8QsM2c4-L-zc zTW8VM#xH;SB2dpytDPp`<-DWk!*SsA1LwFvB`~`jF64BRuqEmv{fe@L-{AC&3H3Ni zCjcY-Py&_*H&rszk{$U`$?L7AE{MEY@Ph!KLo}J9IBfPhlk|lo-*q zPKtY!L4erJrGTtr-!iB)nZG|=mbVYN!v2TDd***Ze%AJk%gv1g7nEU}GNQ8chs8;G zolAFoeMEe|C-2*N(qkG2KXip|Ytxx!^V1ZH;F0rRVx}*M%cA`IqSK)6xg-@Fk5wwi z7I}TvY1QwIGYCbDa#R|G0>1dY?nJs$OuF#pxBwF5&+Blzwut0)zpnv8j0er>9hsheE9D(DFTI_ZJ`&fI+a|2Q%Wq?CZ^{ z%zAv|Q8~Y;=^P>uEia*tS4zls8rTof3ER&P%%jf_NXxZ~{x=kVaZq&czB3@HSfWVr z6GLQ*Ye>m8ngf&51926nYySTAW~U;c$a)3impSkgsaN~{@-Z3`qib~4pRg)Pn!YNJ zh1EYa1_ae8uL%=;6p&}Z9pDN4*6GD^1ni6w< z!-9^_cH};02^0NU_x_<>)t$@q;gN)7O2h&&h%>U(Xk%|W0ySmd+IL6A+1%>w>*x)a zAC$Z+d8bpyFl3onet7Kd6)w>}WX8JsX^45!&{CGO)ltXz>y1<~!Om5CcqQqqk=WC5 zPoMMKQP1v|;IsUzJc@Tc!`U_FcmaQiYNs^quNa#CDMD!Q)WbY^{@b*PS&aS6w!)Hi ztrK!BA&nLW({FSD&~Eo@|8pr}rOd-3G$=Jcr{35pjE=ZZ&jIZ~fXv>}Nvz9j_iD{dyG`<63@DuBxz|F&F_KGc`6o!OhF7LN%sy zghZfcMtSPBq;I%WPX8gb{{9+&-WG8eaWlfiqa9$DM@2U3LJRr(tcfG)2Kz@{2xu92 z&UE4rx}86&U;HpX^G7I>SzP$CA0opUqy|O6gq#!iP#t zY5PGKg(43dI|gi(Ym}7C(K>SZTvy>)BBbOC413w-;3AO3Q7&I8M`7LT5OnSgu5WN= zji78MAH?K!&k6W{gwz=_EG}1E59sFY9jl3dNg7!7yk{g2T5c$>XZii6lGtAq&T?Le!T!LZ0by9KjHU|zcy#o{!KnajXIfri` zYKNC}_X$BoJ=U_?ug-6Otu|qKdP}DrzFsB_pXX^gVqsy~)QuLo z+B>&&d#8e#^ma(`+QsU6h-{rP3-rVTp>scDV5W_zI-PUscI~E)--m6dW-=e5_ok+p z;=DbCQEc7^f3$fW;@9F=Vd8HyKYBvKM8_%p%)ODTFt|uRH1}WKk>x**?8}4bFVP9X zEZ61iSo&9xO>$w=DoOouAHki20wTK-q)e3}X@L3UXi}3jQ8=aIU;JGCX@QakBMRqg z7V+9fms8=6j`4RZuR<@2jWOkq)Re>a2<8(%|F0TwZCl&Kl}>aT*5>!he~57hAbRJ@ zSnVO~Tp9{vqwdQ|Azu{Pe^PN?^*l4*e5ap zWz6tb$CKNtneyKr%Q?YM@N7mtnO<2J2PDSlYtI*97>^c0-L> z@Kc8qRjh2tQ|l!xEW5pB2mV?wy_t}(1ar(y#t7{X=c0xC69!{fq4>Ng6tf0MjYdsR zP|9dqDciDc;aI6Ee26JS`|{ViWzMQ>X<#|Uu4LfBCswL0oB$q4t*x2HI4*T`dGUtn zl~nZ)O|O^@;Te&DP20AFbi>?kd5w5~de4P*q}?{1(q zF$mG184`Uba{|66RNuz^p_oBazBShUwBwo?kre8x?WebEX5BKrH^P){Un!%AK4C^E z|k!!NKM8KY0K|Q3pHtbV%^?bTFfpMrBow{oY zP;=X`H|c*X8ba>>#ePpz^t{h_=mj(~rkl%Jf_;uu+yxg&e%o&NXZ3sgJJDQ!?dzU5-VZ z`u$_?e({~2-CaZD1ZM7!(3YvOfkj|`CH8j;t=yVOv zt7ezXu^#2JX?W(CJ8qX~WpAvX4(wMXCqJv^3F)zD+ZoZOrYPctUvXqqx+aNqwY7Wv zt2B85*x!!uu3x0_yYIhZY?j-sBi&l%9@#QFFonm|(WxwxwfXFU_1goM=3DqXf(n2+ z%i`Ytgb(7K&Fui7iAnf4@8UXLBL}&xvJ4fx8rG9o_kMjr@dc7-#5tqPX4pAZA(S@5 zPbrFB#!wDQBq<9_>JY!83CM_0bTmn$0cRhEfQL&O2UBfxvrOfj1(=XaP@@RQw2eqb zeUaFOPg7WSv^2IME$(V$rTvxb+E8z+n9(d^RC46d9e~{KI_5svIc5@gS$T5C=d&~C zeRXi$9d;P_Tu&bSNuCf+XOXJZPX3BS%JdZ-&e4!mYMZOfVVjAMtaln#jw$|WpvpOe zn*K979?-({kd8SWGpuyKE!@Mp4f|1bHf-TfNE!5>Bx%<0KV>^gE0e#D5L>NIJ&ThX zvsu%sjji1+$s*0n3n61;aPfxBemPTBq5>l2awXS6XW6Y?9dx=WsBv1Ydr^!8jn_wS!H7<6+2I=}oO z^HA}zoWvXP4n%k!lKfz>#NRd%9~PYhGrV6Po*#t8A2oeaoPABW1Wilav1_2(iITM^ zI1R`z9!x}sj(hIkrc^$tk&m<6Zds^#923e&BPBBv#m3s|mNm>?uC{VzbFkz=_@&#Q z;eXHN_^{y$-?_s4dPl}(RYM#7M^4v{matoJh$}sttJ$a4*5Ki6bIeO#g|m;3-x+QB ziP-T3yiD`z^x=!p0rthJ3LRvPd>%q73 zk>|d+QZ4Pe1>aflFZ(bL{e42>kwn2M`1Ue6#bC#Iy^U21*TC%YTs|D`XF=Z~g}qWM z4&m$^-#y#*Ta`~^s+@XN+Yw@lZAaXPzIv!xF-yZ4%ryjPSxuZrEv>_a1YnPKfk=Cz zqB@4HiT$<)g2%D4p5Ch`{FXJ>nDfAy<3(>D@G`1W zx$JdV-Jz7wy1su#&&AAVS6{SkTm%|hA3~@V%0}M;XHK%SOL1k3*D^*4*j&&0@Zwgz zLjQT^$tXk)BmhSbuTca34OVK=?`38Y2G@(|`0s3I9gKOZ$n97`m>`cJLz8X51-=oZ_VY@#w*f!&<4i!>-oDa zj9%BSr~)i!^?r%mbhP=c(ea7c-}wGJAF$*-ERIx@hV7)j5ZzTy3K)t}& zSl-xM$LjnJ#Xv{Ya2k|=ktU`H#m5a+_L`TQr)^J}Rm%!}v-j0vSe#Z4*-5)b)mq@R z){I^A;^N}oRUbA%4O>HdQ}ks(r+FkL%d(>9*!E*-ol#9qwV{a(?>zhYEJ=ZE)CUH8 zN-pMhqw>6mq0?7O8LP0g=2Y9*-uE;@ZR?&cRl%sl@F1qS3-eqdtJtl^Ac0IzQf`cj zSyBrlvb%iop)y^DpHZ@%zrOB2!vWek>?4g<9HYjo=tfu2k2)Ly1xOIP51h2u(RT?Fc-C=>v};av-Hv22&s|49bQdVWRH=HrpL*MEC7~qfoOS^H z`LNSeA*;T<{Gxo9y=j^uvBJDb5FvB%hR`W|{+YNM%f$4`_cYEa6DT5XC z$&_3EWqrCY7PohsLMzN~;IpA|ayRjnm7sN_l&8F-t`iyeM73hJwjo$GY-6~C$o_DC z8u}E{Bq?hZyfy`Ed2_{|`=#!KRB_oQUSKnl)bEe%M-<=?j!@+_dFV?9^AQK!dj}#m zJX_aMqYfhl;5#{7Ee)Aar_z!Kj2Gp8ZpnRl`~`e~XEfq1wAbFT=!5Jb7-VOUQ3%SE z>$}(gO!41o57}3=&`tsT0k@d`0E9$aBpN$y&@2xGob>lV@?R#t_za1^X{=x4h(AVc zjzAf)lpE|77&#-;pkPGC+v_nj>|3o7DF~6h{8~h`K><1vSw$s@KXs{Hg}q;p6=lEv zZ}jrA@S(dOags=3^!|=+^)e3Jf2gG7=|MC`bR$?T`63c+n3T#%A+;|6H7+nhWHx?i zn5k5YkV-paHvfFvL!aPsha*!HF-Y?tZnR~)?aR0E*x2Ca=FU6Oj|isolA+d{RFv{A zRu?HSGQA|eh!>(??8)Z`0<_yIgUSZ??$~Zmg@kVr)~urCK;KpAEnAl6$E}KrnlAOL zsufweBAZf-Kl0q7a2hnclENX29d7)G*(i7v$qa20zlV{k8VZ6x%e^9{d{c0oa2K_O1N+nFHxA+9U$zW#ewKA>LC1Vhk zZ%0-W4gA*ga+Pd@<{b3T`|6?yW0SzJ$zZZ)2dpUimG*maPv`lH-2yDTt+)J&kupDY zu278@o%AsH#pGt0MX~6DIS1F%X}s#Zo?ai$?SbF0;2QpiX-#%6VgBDBXP=B; z34>G&A@f;&MdJi{BBS~zOAFnXoUR9pky;FwpDO=N+Eiiz{#GpLb=3{5ZiQ);mL^P( zG7()%On-upMkY;2pOzjbci(M8S;#vyVVrIq9}4=n@OW?@6n_3(7d&wPzN3a!vCMB) z&Ekm08P*PgQkO4``JE(at8Qgl;mzc6=aT`e4>d&5ny)k|!r#6X5Bbmj7A*VqHkIAH zv4hul`q2t4%rHA71ypN024vif!$B-hgCWSuKn0G?;I-JHiPG@wJf$zPhzcDRvlw+x zOz)D+Lg+90zP7H=D&$git<3RwWwFoe@}DWUkZfOQWeUfQYs3bK9DZ&*Y#B`KD-3sg zz2+zuI^^e{cl}ZA^_rL`6d9^jS4n(Kd)VJ2Du!sge^g?^S{Nc3Q z#SYqxt~+#v>{~NxLsD*cHVittFB(kVlGWC$oDA>Dh1?Pz4~g-0yYq<55E69IEZ7s( z^a=U_St-+XPNEi|-`BW0gib6~SHnF%Im`>rF85^gzQ-#RAggyYK4Ant>rje^1V3y* zeBCe;lBV=>9Or39W46HK9>=cEJPLn9JA7S6znFq>xi3L4}YSG9Ik%?MBl(P^9P)3m zM$8V4;wq?vD~BeMY>8#Scg_nhAv|jk_}>t(=yDulB6>+XJy4YpNc$HlX6{G8?$5ad zNKCranSke4MNYU}3oxu;yxfFLPLG>E&e>mgqHvDo9E!@h-Q@`QX<$+D7klgU#p1>l zvi97RU+VZx%Ma&`-dXP#C+_TuQ_I`hL-(#ePFYbDly!OrN#Ln?%-4l;PDeTwLxTjT zm`F_z`rpNI8Z*l`N()U>)l@Z}fGC7%W3;cHsW#K7hI9D@o2a_41bEU?;=RO+r#y#t z(he-+JX;Xeu;@yXRiGX{9^_lo@?m7%a0~f5KgCIQ=A&Tpa^3jpX?~S*oKc>*h?=GO z(V8u3u*252?`!PjOGE{4cIVOGAiEOk59*Kaa_<^aa=@wGrmoKTblrI}h`^9(Mw~3m zPAEd9P_>&$FmX{nDK4oVz5^U&YPkM^?Bi_jbEPdvXw!%#l_*LwERT2% zxgcj>QP7muGOx{TK@z*=ac1KYDfp&?Vk=ktMf$^h21orN={bCxfmI|!BLmr(9RL>^ zrd!mJ1EPH3egsLc=Ru2|UhhtPbFQaNIqLBW{c9uwXOxY39R9Zjj?nKCk1c^PTonr; zhJZ>Ny1K58Aai{9p`48CSrLi zX)Ezf)aZg+X)SBfSA-zCus=ZN2$TR3aT&!BcDxb&0F%u}XyIGB1)X`l zAfT z3!C7wyBL|=bBMSeKTeiM|0~8UVk12@(f4nAnLY=s?k6C61~V0$ z9MW`S|IqtC=l^v1D(hG46x)cw+pjWj56oP;MrdS3wgyTo#U8Q0kqGZ$?fU9;jnABt z_lhM@e}N}z5!djXLTo%jd+`*JYh+FcjJfkyJ%cTnw0o~CTj!5#ExjOxkk%XY={`-rW-db1t=o6C(^m9)=svE zkoz2A_q(;**w|NiGWY#4ffkB9P$!@dC|R*~gl|s<_m{XTw$hLrzmm+~stJ~q4pb-O z++^n{PBN^$A0a>4k0G6skU>MIVxFR~=)@!7ZK;s2xAfN>%mgVd_3~ah&58{5*!cZ8 z%AKrvo|&E3P6|I|(X!Sa^tNr8Hm`G$I<#{m(r}HKxyJv+-rM?kc@7mcDgNf8|7&5l z^3z#zLj()}r4jp;ZsPUftB7k~?M;acxr-p=PX_c1fe*GCd}qKdLh#)~y6vFR`Wcq_6|ownJ%IC3nZ%-}Ey9dp4}*&QfI;z|=U4 z1aRquKoe?~_U8=Ri2C~kLY=XoX-Yco^;VHTi6sVAmoAi_)TfZjH{WFvx%MF%XCegP z;|5Mx+A)#oD2#0cB_0AT{P-yyfr;4AUX%UFhIs$hzh@(18g?l^l*#+r>DueT%6xc{%@U($h7vA4^@^A_?7(K$NP~c@`=IoX7JXxH zJosJorR)A3EyCrh^jUH;aOwBo`K)GMkJwUsLrInKdm>$Ocp5HUu^CWx5}HMurSWpG zWu)X^SzrR4Wb}_K6RP+s zmueMt^%zOEXsyuLxwf?(65GVeC&X6*R4$Dpi`(1Z=R3}}?+N$^**>!0cq<+HG;#hc zomfnCY+L)h&!in#Dh=V|@7$Ow&J`s*@0{@~|`iZ#9=SHgHv9(EOJ7>_#UF=dny*N(9Z^YO-_WKvlK%F|1TV@R@%dymC1WBtr=K9iOR5h#70Y=CXcTZg5EwjRzKAlW(TsOuaz~ZWm)x-Y4yAunpT{KqE^mYcb)WJX;nLg zTQ-aR9+4HpkL;SFC(gWLyU|OXAc=WF_?3{) z#d$uNfnw%lY-O;^khGyAm|phl3pJdoSid$kHP2%hm;|11wzt6^mLXNaIiekWTKQ&2 zD%cvWBYL@60f^Q}m8))EElsapxwu8zCxix&qMCXhIx?Z3)2rtEyE#N{6c(lX{#~Hf z8sj4YM8SEQp-e(aw?1rZaL>uk7U$s+x-(En>JAPJy+-r&P`W7sdlo6 zrE;=LfDs#yCJ1`lkGS4{i$-;~9$WA7*Z;EqLZ_#c)Kv25+u~*m{f{L!5q};?d@B^& zcvohvn*Tm`F80LBmLv2#&TouN*qdX<4ofzw&o8o!RKx{uuSoon{>eh!=e(w+7;W!n zqiBve;%6QW*rPpQua4g5ahmq5x5 z@{N1VPp&GU(=5Q@%C95VO^;$jE0ehnOT<<154P@=R-|N;W>2di)Y~Yz=0lvQ0knHu zEL91Yh#)Ox(wiR+&21hp611=UTe_m(rUiPRV7~OXe!};xruk1n!r>RaUj}6~!nIs` zZ|A>cm{P?__3Zsj8yYJA6Z9?fAUHp-@T98hq6x95Z+Z}Q)pH+xY{H2?&fFr2GMShu z&as8T=V>6vVVvCjJdxx_(T)k8?_Ce@gzjA5wA$hXgNO@-#foDZc5MR1% zoBzf;dC(>QJ6_e^m?c;nIfd(^k310kVudbb;l`^yP@BS2LTyGF`22uy5K+S~Nqoay zi4YH|8?4ZrBVfFU|^4G>R45E z+dtoL5{Rqv!G4qjj_DqD{+|T^(jItrC^u0%33W!(#7Z2GOmsc6Jjp>FD+0`H|F~se zWYu6Wj{3wl=j0pg41(y@bzE%D$>sDvo5r5Ixd#<~*%$RY<^3N3#y~m07B4NZ+Rm^R zrnhZlWNhre?G^b)Uw-M>&~x8?%B`M1D<|uzm>C~t-;V8!w(8W;e}WuKkF zI>@!gNP)GMl}?vS%WGU(UShr5GX~X;(qwpY+je%$?4UV1A~GRZ>U3FMUSVxv5!>yt zd;c!>9zH;0Yyzh>S+7IAIYhlt6OmG5D~k)w)32WRcdtBu>=&MS{7G6$aNC|8+^~Iu zW|V}mMzGiry;xpXH~0yZn_=jR6RwX`{iQ7v(8J}A0$_Ok(mY>y;%T}?kAL)&Z{y}8 z2T9X}OgfNYU8Q%g&s+(t^^&0O6_}nzm!6iA{i#F@h-#Oito-@PcBxGOOKylt*qBA- zHvC;|@-NYcRs*WbRokq~4K~PIPJk(!o<#^@6@F^XIE&Mo!WgtMthU=MwmZzPwpm(Q zV!hX)yWYlHhb#&YPO~mWkz;M)&Hg6GXyXgG3+-b?te;#>layv-h-Rz7*!Tn!+a{Ra zzKyBv(+mwaskeN|H`1r78|Sb>VgoDav&(#VtP4`OvzDy0&bPn%HC}u1MQ*{m(i%yvVdHut+qvOeFPvp$?@^B4dItjWzC7zZKz|^@vs6mOeSk$N+J)=9zPiMN zpZx@nJoq{8c-K4lg@5@k86MgW!ItheF;_X3o-uTtA-9&oS)35mq+(b~T2hf#z!d>p zwZqq!k12!qN}^G&K-vPFMq1+!7S8s6OB|NqTLqUhkkSH0m`PttJv7 zoG}O5o%RRMoj)&MeCeeZ*4Ng5qp;>(?X?wRozbY(Xr(pMBn5&|T}q#(9r+Ecrww^t z&@~{_RF900&9|D(f7VEAucc}H%gyHSjn3NNo}a(;Vtakf>Aaw(BF0BY*tT;glQY{$ zTP<8%$C-jSiAj^1QbN9ad4BF^zwx=x-}~*aJbEx&SrNA$ILP(8W|?l(P*QklsO0`i zSt9@{S2>Aa22exMv2f0N*g_Dj)rMF|go=3nBtaB=_56)Ht0;FvjS^eAK)lC2+tIofL4Esag+QcL{$c4gFMG3(|EN}r-Cal z4+>>qR_|9y2k_ZeMb@a4mt=r@L+B#?F7kK^R0%zyuSXf-;+`&T{}Nf@88GX1ACa+wS(433l%9)s^Vw!9h+tEp$j3R!Wq8yS!;jR6xriaDyLNBr9l}Z zROG{K{eOb6mes{M9{lX5`067MaO-_<6{RYSH zj{vMTz%sXq_i|O>bwHi`L zbe^Gg&g#kvub(-~xeJ%5kBu-syZu5Z%eT#+KhK2=7s)#vq{B&TY^0|9hjx`#t)@Ts5 zh7d@Kyzn(SQVwDD9tv~sYcC!9z~?^pvAY(Iy)?ExiMeIpUUp4PlPb>`IXU27T!=xp zn}!QSDPac2f=a|hnFr>K?vMFL){$GwV=ukJ_fMYU!0s7-<|p3F%+w^>Ir0FCoD6_S z4l<3L!%taii^&bTFaei1KS3sg4Z70HR~AZdkO-^Zo<5mW&Fx}S+<~~v{66q!RivKk zOH!5$i|Wh2Kwu~(O2$M&`sj>qm$~I-PRz}7?Ccz;&&+XpexCW2HP*5odEO;aijl}N z-Ab8FER(5XIJRuBMWm6VB^;6T(xFresUlL%H{JYn6eYiF zjs2ujDpNxF;${(frc8KNbm9CN9(v%@Jo(+nIC{tJ{K7{*%Gl&=sI3t}81>YT#*sTq zX8puq?mTm419LSJz*U0U&u=!()#Javf8Wf-Vy`emue1f z$#XgfPJwgYhuU2y89j12gC#&j;EJt%;~(0Jwl2Ze`u}4UdB%=rL_^ouHI_0yH3qfB zkIICT>IPV}+2j7BLVtJppRaTfbp7cbh@zNAt$_$PG#MPg*4lo20$3W2Mwph0{YR`f zgIniVS#Q(Hb4JETXpIh2Q)(uur#B?qCZ27^ip7hUSYPjAtn@Z#4+31QBuUzcnnOdi z-TMwkGe@r7oi-b%g_KKDNt(?Djco;S5|JheQYq?jY@~HW+DjBh8+29>sR$|6j#SBi z(_8Dj=c^BXY3M7T`Al=0$>cka9ANLZNm7w`Hbpp0NV)e$YUfSn{cAR0`mFPxXN|#{ z;P){GjK*o}Yi9sy9f^u)Ys(`qyub@*PV<(VuH#*AzKvF+&U#jWaM<9rDyIWW&|?IWZ_ZCVX6$*&P9}$;&sc!FMMHdtx=SUZ+Ug5=)r}_SCCwcztX%?54SvLhWXJ`l*NhQ}rFximI#!Y6L zDHE}!DJ&uup_kDjok2?HnL8yBama&GQ7F`wL17gL=?4mg^g*t^ms0sK+Axi$$||D( za2*ka(X96j^Gg}0=ht{`sm&{|9^<8FpChTK?Ao!NJMX@e8*aOqsp&~-wFD`o_ZV7B zZgN!IBL9=_;b;fustT;YAee5%KKoZ6i(dNrH+a+RcCMS<&rrRFkSg>t z(ho5dLSXw?Y5=NSm;|ua2C!qWI(V#%!5WLO{!QZZts)$!7Z&-_3ootE#dlNSu z*+)G|$c^@v5`nM2alYu;gC_rf>cV0Q53GD{R$yI1XQd5{m8=-ig>)WY^-JSK#fV>( zn5*-RVowOqeSH^%Be{?C_5Aaq(X-z{%$v8ZfTttPo3xS*G_Zn^m)!)yhOIX zOq#&1hUH)@WjZoU*A=ZqFdkXz!jcL>jKfg?*2~91dUAy7C>yNPLLjXOEMDci5;D}B zOA6-@PGF4xcddPRW~4mdqr#@IPzW4S8q!)sT#FeQZLn*0gxd>4TN_SaZ1c*Q6;51S z79Og>TRgT<%bOg== zTU!Lyssva4F^2x@O0X%|c!REdxj^BIoqRUM#oaOR9b$vEh@dGNJ1yrH6c~+~IOBO$qHnj6z6%FY~Hg zNQrmOhol;dHk$c`MJ}x@VnP_F36orTmeK2USzlRaabXdy3v{U25kcmOql8!~#V&#}L8iWf6g~a)GlFgSwe2(G%D^Ym8@YI9{zwOWS_Syvl8S)!5@U+K&d8!8t|PC}rP zQiAe0MyZmOk|?G8HBnyn>VMCOK1fy4=Tk*dV6cLuC{&PLmGX)ELIw|+KuJ%fXq&LQ zt~tNB&a0PJdEx8==N6Vphg#fl{dL^^);Dwb$Uz#d7MT^OxJffjdHlh@=gXh|BomDm zw;kEbj$J#5TMbl_LKFcpFP)f2?7jZnzI)$sf34a4onn3M(%RWGoH_mjUViO4PM&#% z?fVY!{$KepI}hA|k`W?YlQNK31DP(!t)sx9ox?hdb&gsT(UgjY5+vmjgu%36$z0h! z?DAZga3(NUMR*^WE9d3vtMK9*?pdyK$`gn%pdo!xdL+=j6oL zKFh(o?__${ZfbEtBo#^sA|+D+Ls&a6Fe=uCSvq@8WZj;doR}mLF}>x5_kQ(r_kZd; z4}Ph(Kat$Ie-Ev==BFS9VT6AmV7P&ky&r4Q2c03mOXmgJ6xc$0#;Uv);j1F&qb9m} z&f~{V@YLzk9Gur>{Bm>rzFp&YeHQ!t!~}UOGiIL79>c6 zSEtlB((kS+DF>6gAVNg~ji$GDfyv3cuDlUPzQS;8ZApY%Yis>TtDCL*cGO_>yo>|i zz(R@3{&=MDxuu~Yzeo?4Iefmugwf(CM&M=93{r&tgNrm);UCegSXD1 zG+1dpyATCgQA(unY3GB{ZGm!zBF|_{O|tvoUZ%E9lEx8AO8A(?vM|=aZ{(NC8@F&aAET)TuMPzP8GnuRX}yZ@H1iP~C5I#sf0H zxl8YNvV@64i!L-e^We>eY$u_lm-}2W1()HYE*SEq^z#R0;Hj&Go0fxH<=48a6Pi@6 zK{-w@gvB}82V2g0#vv6kJ!5%kewBy6_Z$y>^E({7c#&ZRH;y$qx~Im$(IztuXr>uL z6gX=raz{6J-cx1_M(e(8YRHKLs2l%?M*1Q%2pI#KC{a`ffY4YGT9 zksD)urj<1a=>u@33T&VR+fS_sW#a^omh;y}IA8fvj~%vlOYKhLp? z^Jt@a->uhkM6O_AUzXC2oCjft52@iMTlQ0nK4mL1uC2fGmHU9d1N6#TCmas z2nI5*aAK3yN^B|)c7u;s^Q!tu<`pm#q&A0Vcp|*->-%Re^7oHD%NL(}iM9C)?5PVr zbfC!{J8DcNhFBP+(J0^QdErhVgpdDo=997#42nU67YauGVq<*5HsK$NNc^c=t}0UgdSzQyzc+1c+lrx zM^Pl%J)W{_WRf@Un_#iqVY>eDAz+^mie1W!&*t^FuHmRtk z5%sjj^#}K!{bbum2u7TBa;JH$PxsuL=*N?k8b0s$2axVYyhP#sx7H0*KN*NyA zan<|NWknWSYYSj)t*!OLagYgv)jmjY<1ufO2TO=6oEF~*V!2I{p}Rr>+u%s)Cx12q zGlAcXM5TL2h=Bpz1usRB=PWEQ;UYnjL^y0eEOLE)jfG1WSy^i%>vfXJF{BykgBW9s zXELnA)>2e!l%Ye1s7+1~tB6SZO->0F*cHq2`3t=8#Rqxy$*0&pKEYDn6Hk2h{y#hR zt#ALzwZjc=yY5=*aq17hq1sRe5MnR8#HBn%=t#5}Z83$W$TD@zqX#B4IOZyKPGcw@W@lan%5xH!QD2{TbK zxpTZQx5VE*@)%$K`Zrn0a&DQZ^S0|JI675lwAKaD#n=L?ePOq;o|VfBL!LW|o(V>C zQO*0ZwHcFvg#mQ(YqTytiDTW&ha-o3kN4Uf=FTO2(yBER{{ssHNbljlcozx&>g9=!fm z*Gy|DJXr5DSI&pETB^5+0qj&#QWKJz9QfrNf)0)j1m>y=Qw;>>O1X{9mEec?tz7l) z<1Uj59o6|A-!&S=eo!XQa>lON$Y^-K^eT@uDGE@g`$%~xz z^))Unt)i7=;^ z0L#7vYqj;_jq%_r>lPGw(f3UW>19M49OY8Tz+i>ZKoLXAthc`rSA zUSJDDoP`D5rXeK<#ue9$PSDkkWdlDryT(fwdc1IYfnyh*e2Wpn4=I;weMLr-g~5y zh_zmi*XJ&9VRZ$BBd*ox_IlLggg4!MEi+RSXz6QhTriP)g-`Yw5NADz>vc2otngBm z^r4udtcvLy*Jc0DR7}@?>T>Y!LMenAnA>GYF_%(Q2J*sV3+biaIL5^>B25t^qo`T~ zp(L@C=sf53OACDFYmf1F4}OJqr_H@nE#7&}7<+~#DeGto?-z66tm)^1S#2nCO_4i_ z%wYcuXj+h@4<_FK5+#_Msd2FK4|;K`F``QR`8lZ*Rr zy#4oj#{OFb80A?_D)&Yic!Q|xVXml>EW%7V~fGsT3hRf(^js+p_tA0 zs8~T>aG4`rY^2*aycZiD_>Gw?A;KOnE`OXWoFBx7A{g7BGz%kv{Y^>;Fh;Z5S?ALF zGE3PyYuz60tiaV`rf<8ET?h6N4-G+7$2q^T4^G#Eg!FRb@_@^t;u;gBw@ zhYlY_#%X^NOyWed-jsLUcTe=S`yc!zdEwk|ynX*Qt;4fBaN1#v?#n=f=j}GgpFaA^ z^($yxFq!Awj841bjls&#SqUkzQgZD49OoAo5Y90>Gew$4oSwVLXg%SM8?I+!V%+;- zvnB5<^Wm~(KA15Wq(>Fu0b z(tP9i5}$kMQJy*WI&XW&JzR5OFKY!n{rGpc4NXk_Mmou2mE)qF_p#?w+TRPf@)j|cex+q z>GbN{QUm}1AOJ~3K~x{V;>YSw7M2^fz)9sjJ=XZFBIymO&T5JxXQ{oy$@w{!y6dzp zBts+YxON+(dw0+rA3-|lr_XGG5K-UfVZ-Cw{_WikK$r7gnxU}~+7~YJw}1B6eDD{4 zo}<@ai&7B+S(7Tc2c^!RIeF@LR!*P!>GxiabuvfHcmA@?lG_o6+m^NaF!v4=_cmeZS%dae{0w9=pkZBzbT4ONk$Kbf!8B?S2@cwh6UWr^(9k z{K((@%7_N3L2I8{E)1qHtaf{xURz~VIvP7>nccC2#^e~P6?>C3Hb?>kpx_~u>;q2G z=gVAw!xF(OQyUs$a$<^SzwEGyn)f-VG6CortCW;OXh>HksISp^xbYwx6`8*$EY$i z(^VohTv?DT25}LULg{kb?q^_?V8WFfx8LH0Qivo$)e=;rNjf}4)N0~tbx2Z(V{hjc zurNQ*Bj5NAAAjI0oIZVqBO?jFddDsf4QuK`BV32fc$UwFx+I^bZgAFnt&E1OXUQ@z z3wm&6(H683tQo<&g#w3*6jAJ$sHIGerEH&QGBGwpy_Qf*W0FLnlo#c+Q?&DfwZf8V zLnqHM#!)zj44r}wiIY*Ek%^TLt`tIIRfKR!U4S&W{Ca16CC?8Xx%K*|cON}WB<1^za9;ATlMzUT6aN0^`c=|fp7t$(wY9d^ zkGrl$Yex;}Y7b;KU>|pRtitB}DItCcc%ttgaU1*+l`uyqLJn7;?p=fvNiY_BKZ}SQ zE;2Z)aYkd5M%9{3&&)7cgSeSOB+$xXf^^s~4Gx(>2pPN_eu_&332g9ON)f{$j3JH` zM~@!h|@FGApw{dEz zYCd))b5$7fUQW^Nq4ON4H4)NJ$IhQ;v=;IHpLiSNqhp+zyTC)=eu`wM z!QD4s&uF88v9`~_d8w!jWepx4>wQS;os4!Tqmd-UD)N$)16zj5%98W&@e9ILrW-?) zfeX903t^a2IGi&c%%PA`MB1nk4>gI0hlqw+#H}Gjl47L71mBy|(CM!6#P`0>|MjW+ zdEvF!IM|eY_}WSC+%ZB+tcL`CFjjjHp4FbUvKHrju&lKXT|m}#WIaKz=g^s>XDpd< ztXn}BK^sAA4b4QbZ8&AmK8n`chGZ69Oy2IDu7=+YIMSh`xZxSR&zQM2Z+lg(FfDV>Qlcoc42d z%_u@CNh}m~C8nnNf~fqdmjF5ZI@%5Snqs5;-j8+=1EDbhFVC1MRCsM{PbEKf z=M0Buhxzu4$NBDKr!n;zS`%ZRo;!7JFB+_|Hwf%O1i$Cg(o6rIKOngPtW1Uc5mebW zi*nk>YPiA|H*fM>xefP?E1lckpmCx)K^VZWl@Z7YBYb+jLtf^3*jifvYin(-|M#m? zgup$s5nNq%j}n(NR-13)n;EPPAZ+8;J16`&f61Vs0?>*)gC%?pleeY#(p+aPLRq5J zF(kZ4sZcpu<#a{f=kQCmU?4`A5cg0D8M2?CE-D;a5cHhHvrE&nGhBD=Av#ZfpOvL0 z9{SQFQ%^j9>;unz`&$R!J~7FgXLh2jpeRknLLh8)IK14z%H(LzTp4Z2yBT?>i!C(H z7|$$NixiHI)x3P+B6BM%9GRKo-kWY ziQvuEK3o)5*SoBBGKOk38gW7tCf$tD{Yn46fx7y(dA|xKZExUrUgi=4701+uTGU5| zNQPU)LoH;zj*DWPEbDo^nLh7!dG?jp`1AW8;_>gjz?3cc(7{penu!^UA!eN-04P(A z{5jKqPB@FT-a6RpTDomRmO+*~^4!uZ3@h5PCIm&unwo4nc8)ff-9E*QCQdfc@rKm}Wq6CK{6Phf~xzzKG2B|Y(3+f6Qv0%6s(~K3Vut;o$ z*>W4e%SKO}2n;O)DXl-rI4D)N&LXYBO31Aui3~SiGr~37YJBUJMIL=^iPulH_sFz< z_R`5SADZ4X`}?V4+F6U0e!^0Q5MQxD;M*t)27%L03_8=F78F;Q7VOtpZerOtb6Elr z4o{naRXMgP%4|0euskQID5q?HB=v7A3u_BtZLO{KqptPkMOssnkazs#;ANhx^2Xx0 zD)-+1tKhQ4_rT_e^5`te_#THyVm}-2VlWC3sCvI;Y>2=1tVtlX;=nv9>xiACX9YU4 zWRbyEyeQsht_S>?E9J9|!H_{7YzCCl7Di)=oOpPML)RZA>*bt2b$aOB!u-E^_6tw{ z;=6avH16BElZJ@NZFqd^C?giD0*DR4i)B#0DGYhHM{m80vxYM0+Z6`mEX!G!S1w(m zYjSShH_P4ET}P}Uj-5Ep7oU2T*7ykbU4N7z6;TAgSf8n?{5X7&roa@2rIj}8y`14j zlaX43IEp|B3Y~|HrRw!!5E~&}B_Oj*D-Xa*7dRz}Y6-RBCXKNX>Z2n>%^Ip+_x?E< zVT8hHix7?|vb=ug93Ow+VLtW9S816JKRGqZd-u&UQ|p4rF-}le9|7Q;0p|;>owEqz zpJSmNSqAHESX~qJdWufr=xEJqVd!W_rl2kz(}`o>wqbTnx0sk{64w)4q%gork-t?}G2P)E=_~S@XlQB^M zmRdWy-5jkoQd*)&qERRb6dAJ6B=rWvjXJ|aE$SmvAcQyVcX}9I(9JV07F~3^gL0ah z5RBJjCR52+J)$l((s{_>0yEX`-wQv(s++0&{kCqm0A2J~i=`&wu%Ew;$Z~;T^L(zbKV|eTyhYm{6V*NkS+{_>JAb>jr7i5IK#s z+V=|zcf}@%D+i0Z{=I2n<*yvg0d7l|md|3v?I_6D1-V9SH`4{U9* zm0Rl#*9YJAZieF&kVqBzTa3{@mq!TyF=G%iMmQh77)e2|*I{nqBER)JALhRIy%%~N za0MIUq02s&%Xo9KiA)z)kOnL7%Mq%;;utG&PN3TbNu!C3n_wa^Vd_k%!KP3#z4y<= z;qeS!o-)GGQyI%~hi;T(qY@AXS4=Rqf`>_wJ=0MLc)>RUY{6bBs+)@b()H(TWrAt5R~~#uGr6 zwH9N+YRlqEoAq{=iP2%k8ZDAI#v;h`%%{UU2dXq?OYfTr5e#Loo{SVSlD@oI`N=p{ zi)fCH&>Cw|8)*_Z8@}r>iXlosDu`k%5`iSshR;9x82{(rf1bJXbG&!YFz>o%l)cS} z#Q7{ULLxY&5!@EoFyvs2mpSvovbJtmT{Wz%8@fG9W-QCrvg!<0gn-S_ggv7vvy;P2 zjx|YAK@>yB8rFJ-#WFY0aF-IfU^^Pf;ABt)E`ZwV|suS}4xUo#XuLuV4zOwZ;%m zpyHImfsA8FB1T#@T2aDSy++MxgwqIDP-F$GtE-q!o5&VSG!;8XhZsqrr6e)d%j$sz z_I|CZ8Qk_gbW()eFDX#ry$rcten>&0REmmI<~xGVJwE52TvX!FJ#Vr9$-nrO|7~J+ z``=smMieQ7ij|BaLy}+^6pPz|UEc=*TZ3{ru_+J~NUQt4s|_K%m)%&z8@%6ipArJ= zBvvSNlwwsHfYnCdRdutKz@)gs)+;M0Y-?>XSX=8K{+etKLC*}BjMA{Jtslh}{`@F} zL6*5W&e0SxleHS3{^WxkIBr7Wu~!(w(z#1~ z=gW^XxqTP6y!EXl!=oS-!a6_wM@V%SoAYl5|CjPbZC+q>3a33&)Fns6gbqV*?ycUQ z`}TJK@uon&8zEX)G|sI_+*h4cYjIj58daQp_0@a6{J_J%e|u|;pE-OT%0U)FJiVE| zDp^!mpiqC}d;Sbrr%Tr9hSUk^vpco#8NAS0i$kZr}ks;z*j7(z>?E1ZqetJ_F|J+_XKgaL=#m9K^`_Hng z1|NOX9Wq%1BsGOo&^9@XOI_x>mZc(QDVLadm|UhrtvW`fbhV~aXfoYKV^C2< zl*UXBjWN4@f}OLoOifNOH8w({UMEQtVi_ZaH)@mjuz3%YXSh7`hIR#Uy@rfq;#v)- zVxp*qR58>NzV*2W`9Z^R>)p3AxqT;DQE=|uCFU1bn44eW^rclUtgiBU`x1$Y7;n@W zZlp{Mr3{ZWnHn8O>w<2(&8fvjUOK%(Lt3U|!>(qXsb)ewGDML=3E5{Pg%JH;afI`= zKoYRR=M5XY&rP|0u}$GLPQ>gSiut8`cZ!+kmiXc~ACZ5o*MIY${Q8G}b$0i@*L#Kj zcbe{B#PtSo9QoHlf2OcnXYRdM%6h;kH#w@VRBPilu)}W@`BKJRgaez5C{&73DUJc? z-wfO254+jgEy`mHU~R4cpq9)v+J-r4%N3^iOy7@G`V(~U-XuwrQ!l^HAN==!$Upz+ zf6U&S_ItKR+x{t2Tn45X6mykEa^&Fl_Xu3M%V!O@h#YIM&HSW0f5IeR_W31-*xidWQ)bq4jE#7(K5r)$QgT*>g*5_4w zh_KG0jiV?ui%aYDvVxhZ2_}a|P*P%?W4*J^YP*ARmRcN#p1X=gMGOKm{R>RuVntF< zsE@R0jyGwHw1{dcD)J1MGZrTmL<*@wuceZ7x*4DQ>J$9#-+YEzvBJ+B9OpfIN0^d1 z#uPYW-@8|>WD4o&2I!YJElh1Hr6Cc z9ZovtdtK(&ddwG=rY#>}>HcI=p8$Lw}m z!z~((CW)#eg(8h3?^V+pVKlbL(4AFmw~Z@ubfJ*~;#!KV#l-a*B2I{t8c0Q9B_s*? z+4H>gwa0nWPrZd}Z@&fSe4NEK*X)42AZz#N^$L2q;q--hUOfIf-#>MZ;}_?7@l=l} zi5RQbm>O;|(Q2@B=WdEDqrKKCxRc-tSfTphY9I;9 z%eeTf7$YucuoQ$QqOG+h5pJ!2(60;!ANjz~QDi=O+KGXgLioUdejd-D15WnC0A21- zspP`NdB&SH{?Uit&%N(?3-yU{A9GFU6$#UP2pOiu=;!))mMIGH2Y!-DifVsH85iL2 zI+=zd$C{k{%p#Bd?PGlV+s||4-h23uKJqI>Bg0Ub010Kd`MUu_Bq}VHsBX%>%Ee(> z7admID(xskN0vB>!)ca&90Q_!zKkg3-N5bAF(jOPF>>xNg|W=PeDdx;`0#IkYun-l zK5*NOjMrQAiXu!SZM5u`KNA;Dy4F~<*63V&=BnEb3F!*u0AuMEIj2{bx!7J~YN*BS z*BxPca)NHR%Z0^79{BDvD5ZGk4M!QN)i6Pxvz0S`x%K!yMaT-n!s05{!uGLowv7y< zgd{6+7Taqqudbougj$-QLOqTv*$d&zi-i)%NTJf0s97f+Y0?@SqBc|~sYgUf-cu3gf_Pv{KZiBS{1kts2)%k1;*bB1u4toK;gW*Dbivvvfp_j!4O5 z9jy|ql;}v&%`=*fI=gr8;NUeoxMueZljFlQn@xXxqR0bCA;CpHQ_Fxf23zD5y)LHL z#%3K{w~I8nzb;XXN@8TIiK^F7X)P3l`^U;8h?P|y_+S5uT1)YcU;7|wZNxLG0x$-f zW$0dkD}3e`LGJHm8Eb28PR!5q^4UwAJ~PL~`2{-N95B?+9=KWW>5d z%jHZp6*EII;|;|~qCCj5gWNRH_u5qw=7R(-1gI!Nsf0MK(MVEUp?Gys^T`*_aj`MY zdw=<#a_yV$KDXX!&n~YmF}Y)!>76@?qGVPHISRZ0UgA_bN8wp4FP9GvEq%}A6@bs> zhJ-=ExMNVhZA3(_QVLbW5mh8XQKtWGcx+X{Q4?2^2vz^gy0x|d*4FwDVf~Bue*mjP zkfjhCPJRP}E5YPRC8ofk@Mh;s7Zlp3dhgpa&E0q3%+5XAX^jn&rm3&>kkU`}MT$77 zA!{)zPEaz15Y#77VG^!9nT9)8$S%Sd53-5^bOyS>EOvO}@4m@HUwnci_q>Up{jJ|1 z8ESzwzAopoiN(tIbUCRN!WKNpT8rAOxGpjZ&p(V~JAv@Y13s%rIDtUkB`tTxR(wtx zcWUL-srUc+NB)~n7cajWeej-}7)?{3XdZ$?H~5|yTzlud_sM8Yk!!N9XRi8Pe*oIj zE;7!nuCm-|Gd8L>Gql+4N#?C6?B^s3@XYPeWx;C?S?*D5OHg3Y8|L&6H%gL4CAF zZKy_^rpP$*^)SNwa=h_fA<`PC1XP5^@y(}S5trn`>KGcY+(m@*gmnbxu1?Qv#qkylQhZG*>Mi&UHNo;}BJ(@x@UWc8-2|I@pn$n;!gCzNW5NQz8`;wwRY)R$Q?4vj# z5{d}Lii`O4OBZ>jGr|pTe?PbW-|W44ke|{j~P&w|4IH}ri()ij+uGSr@#IEeUC>!^cPK={$#DT z${j}zGBQ41iG06L2starjIb#(OHmjUg^Sj{G2LsY{L-4sPv3xOh0A-M|C z?IQb5RhQfZ)a)ja!-#nQpCWuGRoz-!0BdXg|6u*vk9~$bkzJaZ&Kc0YujsQ2uM~lp z##)Tkt~<AdIl-Fd^{>6e*PnQrT@T&QPyVxCr8F`KsV*?grhYNeIkj$J zvRWGyklUPf*`)4gGJsv%w|@IyBW6s-=z7Qj*tWTSkf z7Z7X1pMLXu{LWXN0Mq0j-@c89h7~HXj*S#91FJO>lm$?2i;Jxx&jl;BocCtdId^TH z`9_M;hEO_LvXK%`Lb)6a{%Rcff$_WA+I;kjkwdnS+K*ccekO{ zHP|&e6NKiKk!^0mvFjBX%hPVoI3|fOqM0RN)t#x zxqeBkcF&iT?Rbn<0IhR{y25LOA`l)cM)LeZ%=0&WhVFcr{m1V8U6lMaFOWR=p<`lj zu;2P&fF~>-88XXpN3*8xh79D(q=*yC+IBOz%(f=vDKVioM*XKW6wYzhxYAZXlNK?a<0$M^WC@KB~3Fv zc3=+!MIdeef&_O3MX0XA$J-w#|R4LxuRU*v6LkIuYdYge*ejD zGf~R<^?N6|uiwo}GgCZvw#(-(Xafg~!D>U&G+emR;Kg%GoS$8%k)-(6P*ws@372&4 z`&7#j0}~^JeLbv6!=i?p#MEj+qHf%%ZfatPhwizPPd<8#V@D6Mb9#zuZx5cY z+9n0vZ5FWFZM?=E2yAZA%>=XFK(E&@aT6WK2&+-jRly(yGW7AvB~-QSKElww#sQ%e zR;M(t&GOwp{yOs)uJOpHKFs(%cOpU$CB%lNJ(K3>IKk#Q)^vw!S~}@!v~$6XX~k){ zkQ3XeF*(x9Ll57}v3vHjYrG#n)95&(nQ2lqv5+JLtZKzvO*6Be)5yV-5Li&cCG*>^ zo=RJU&Kjf9xpm!-c}kvV=sY9HHIpSt$;O;Nf0>!)kd#Q_^dnMvDCy(-K4BPPrJ`QXNHR;gx0f)A3SEwB>zu4m z#ch%W0F+(Xq!MWbXiet9)Q&MCDS7q#FL3?p414Z6L|83@Hl4%<-baMm%GzpeK%&s} z$X3a+Y2SC@GC@&B?lVcgsMl%>UM(i5$1J`1_OE{LfBz4^wfF$?Kn=gnDgRJ^FZb-< zjqyX5LT#NuzB`4!9qeff%~hI{#tAyfFnNY8HYHJj#6%k|uC1`Jw#vStLGIeWkCBmK zv^LD%oacLQou=7n@YwBp8L3vtqy#B%NwscO(||x5!|GatTpOnP2iZA1jHeXqjXJYS zODwO~@xzF~^Br^175nJ<9#Pea)`OlBVNXO@2?;BK10YIts#RLfMh!G%_N6_0tb#k22$RLf-r|yTW3!fU2_1v=3jH&kZaP+SIeEQ=L@v#rx z%bmC1#=y`ZDhLotb`B!ew2Qc1*|6<^t2~D+$JFZRwHmh3#AF#lYsVf*0m^mTs3^p% zM97{hvQk3&zDqK<0+b?e)|q|tUB3SpPg0*>;>d$X+4bNNf=bCb5JguBxV4x#MK@!o zgY05V+iYJ;^xYK{igd|UXwCACIkYtl?AVS!*w5IuLH14#GZu!7_Ef1Vfk{%<>M@N} z5L?Nbmdq!b#b$yqmOu)8;aEloIeibj&m@5M}V%%#|tJ zf@}g+n*kPGK+6ZJ#pnO#eE^oEE80)86*#&Du(s9*eI4C*8`8>l7-TnPzPa9mZ!oly-8ZGqi>@PSyF*$yy_sWyEEYU0dxe#rPO|gq z;@V=E9{jnV73SlHQJCT8EG>n zPjgJ37pWA*ZMGIIEZ5dnxUsm*p1}d`x@|W@!y{O$nO|Dsg_G~GQmgUE{@rZr?RAM6 zQf$zjbtkS{oc?BctwydjQ-ecH_79tjTp}l@B_c=aS}*R5=0^8z8<1# z2^INxC6BNg5R?LZ-=~mh71{PJYrkW!g!0?PxvyQk#((|$f6R&V=lJY_QNA!)W|)Mm zog#1AY)iXKuJAqRBxkLbbM8iyw=S=7X0c9|rBtjXl!8D>@O_tM6a-X9hUnihMP+1^ zJoH)9hWT29g<74aP8l5?;$t5^#y@!c!#r^BA-0VTQ!YgaS1i~D;ce|#+vQpSmx56x z&Dn;AVa09p5 z;1-jnvDV;O%YZUe$yr}*lHeauet3VQ7XM`}PV$}8+ZgJv5-8ax1-$~>=(KK$&b6W& zTy?$gyRR(W%vGWyG8JO*3qgDL^{)GBcPqe>1;7f6Uw`X;0G7oue1^+8!%F%4Vm z?_5T>ldIjF`w1Jy33PSj*<1d;(0;V@2Q^l@UOa?!soutoBD67uX3K?Wwl@By#Z~+( zb8Po~sy$_tr`i`vtShgjWy_YuX5BRwQ9@!8&?U*SPdtQ`itjx20$=&}@VQ_46?%pS zv3cS$mgt<^TRA@6cYH&ch7~9y@Q@X3{>#RgC$c!6tIsY_5+0G@u7y*le*K+4`-{&f z=gtR}IN|nvw^QyPA~CIAH?!gUw@O!W1mmFy_!Z1+zWZ3fMa1sh~KXKHAeslj1{ zm8`693aAcki8SziU5He{b6*XN_#LAr_a5X7ZwM zv2*J>x7BvaVY-eNZB9B%5`?ypXAZ?mg;WtT(B$P3Mr)+B^p_&;-4Sv7=pgT2U*^pl zE4;I?LQ@Ca6pHnxrmiiENy@%~fML&|08?BKI+Dmv`kSzpqL~qCG<$n|gpGM+=?WJc z@#Dehj^A&rH-DkI+BCw5{b>77EJ9ex9|$X6M_^i7Gxv;{&OI+&5#7zEt+&1wf>=eQ z1#{JHLJ{u+lHNy|dMi1$b2s+#gzV?mj=9x2PufERUz#yaJ6By~-d17NEeOp)#gN*8CY&q7M!)kHNwVU${Rm$ACYdfPuLx81TukqG}3tV1Y- zuB^o<51|d7v=}Q21_8N!jHxAW1D-p}4W+o_f#Y>s2iS{;_f=*b#gh{Wj%&$aFHW0Px4nqwMG zbhC*~Q_$L3O^Y!YSX3Aw0~f)dN+GITf+z&vb0YVm;8>?ImrtJO?WbR$esi9&(Lwg! zy`OCd_Tl$biZu0>p0JfES8UkEI6-|8?Ap~|)IAL^M&V-9q}*b1L*9%D0*TQEX8e1Gus%P_wZ-VmQh8S{p1u07?%cnp;#btPwYt|+V$!1D zvVyb~EZmA@>Eyi?&zb1x1VnLUi)BP29dq@A<3hzPK&$Ir-PW&XxWF!R={&E#aDw|D zKDy!S!T^#Kq%YCQ7Qos9SX=7@y@V9)xEL#L!AWee4gMg6)qSjMosdPxI&U}VK)OzO z#i`XN{}tDk@DWc_F)Z>?8<0(kv^27rR)4R%TE!{?kcD+DI^&c6ynXAX!MP z)>tg5$++!y^bGX*TMdzI>H;Zp^c5VjKI$M#1yQjr%(HGdudY)~1td zLp_d3vy`!cA+`?=;|a;q>MC=~E7Y4!lyVtPuJZ(fz$dDd2`UlNb0b3GNy2i8s1l+A z&k>xu=m0duI7%w8PMmLzK_OU88vNe3U*^w`zro%85kIqkh$)$oB`Hasp!2L?%v`j) zuoT}@kZGu`Yi_JqF4t0Oae@$nNC_fO5qc7oPwq)%DWW_y%=oU`hz5taI6KdoD_2SK zm^%*a;8UM^m^=3GCI~~cvE(@@ArVR;j6o`i)){e}A%(<~s$kJNBQ1<`?Aav6Hk#N* z6Lc{WHI`iGq?Uo#M7+kwC#@8528mEXnLm1FnxH1C` zMg+)m4}KVuudg|l%v!8b?zt6;-2)XSMh98^Znq~VuHyi!*}dv{s%->pQqo5qEks|N4ffB2TKNmBQ3 zJu9~2u(kl!*80G&0^oFl_Sn$j_e03=AO1&nBdF?pIFQsHI~EuFEX`Q2HStR!%JcBO z4rArGkb>kFj%Ihb$b#1zRSKjr=oDlr}xx&(8jTd2WGbuDN^r1iQA45rhG? zdYy|iGn}|^neowK?%FkK83pW-_l9W8l815fnynhg12$pN>EUYXO z$0<_zNZ-dwf#)g0$R{e7@FE}Sxy&o+3xX&hDu+ljSdq?@={t-&`WooN+v`*1!;{I*X{A+B2H4>9+;>562Gu&L$+)S+N_Y)8) zL4W8Y10SIzI`r{NA<^Iv{bReRtAOWUdz)(uGaNj)iy!;+r?}^y+bH=SI)yCD-6V|S zz6pUfIrVyjxtmM)e#G!VA2N_`+=;7^vRW6wD#pYKCQZ?~Zja3KG$Bb+@D-)rK7yVy zsvIFp0V;5X(G;ot%JVQ!fY1Upd7eR@BTX^iHZBcc3V{tu*h&xP=6stiGY)-W zZAO|~@+{`i;a%+8JIcEz zCEGlI3nWPk_(fl#JCY-W;+_xRi&l!4UU-AA|4GUxe&Ta%o1DN8JW$%{ZG^-Y+9jRm zq**HCxZ$tY)($Q&E&gYLg|x}s`E!>pK7YKobmcmS`>Qm|A#cs!B#g>z-?0-bJv16C zWB~?kTW^IeSe_&!ttXh=g`u{bUT*I+?#~2VTUp^|z0PfegWSG-iXe)Jn@z6I-{i#Q z>-6>aab$Xu&{L$wxHtye8SM(uVX?F}q-jQ;r407;Fg`d$mM0YE%O5Jm)1;6Pi}(u~<+^b5lCuvQ|B#Trczm6%yx=YRX!H+k*) z6~1_QnosQ=#!qW3)m%!w)ft|2+m^}Q?bo>>&kXg%vRpUJujedf24f7Ng5GM0o+u)y zl+nJAR+7~7P<{QV$^dWQnCINg3{lzgb3gZ^eCkIYVqmC`G)_oUi!`EOA6iaLL!%aR z=FE9co;<_k_&9eTIY>2%iWoN6Bp}li6DQbaf=N;q6n~{xI_FkI!9SW4JlWuHD;-qCgm9F-CjB zlG%d66NT9XBu06}QI*82c5RVnQ)8-lU;WkQ>z^t^Z-Q{aQV47DD<$sz(A^lF^U9m=@zhuTibsF+F($U}Bq#+%SFr@& zb7g{YgGRG%>gy}3YxT9?1!%4;iSw@=zqa`L$)U+KIh3ym!Vo`ffvZ-Ogz3DNq;T4uA_mk86lZSC^6#E_nk%a}`T1jed2p+-s~ZE^_qH z0q!|+m}<3*$&7P$W*Ig~AWpD}dk&4(?bl#)y}^y^vv`rm#NHj0#zyceWt8V(l_*@P zE-peUmtWOfUg3>zJBsLovj5kt@P23R#sUL_{fv$eMFt+h+Bqzz zt*{x|SggeekK7B`4jQaX2aoR*6ZKYZb)^6TR-+ktpH&@?!XJWe%=HT>p*&BqsbLlMaU7O>;{zGgV zpCB(BlJ3NB+v7`72%&YHlQ%O=R*YZT0Mn^AWFb^;TEm&eCCWO^{M;=bQ39{IC_-K!>?%bpeVZ5YtM8Hb|ohf{0VsZ}M-y@;AgH=O5mG zJ9m$i5ontA2D;J2CK*zLC#7@1X-k|yGtOB{EK3R06HSUB@&r}ir?;m89MW39j%#kH9Qo`3E*v)AU> zyK5&u@v#pxGCJgxX=gwau=qYp8z4uZb8%9M@ zA@Uc+hB>M>L(iPLqu*2D}*4;GS*kt z2ucxw@1cs3p{5wuB`G%7E=9dvz0{?$>V8k#Zf{DFjBhk~lAxr^7&B?s8EUz1DEuhG z^8@l`9c45^cZ?v?xU~G$r6rn;24Rm!Pj5dD9p2B)x#Pr1o$=9rXokF7lVrE;X4Tes z{mKQr>oZJ`^|5ofmr%#p+$of#bn?W^x}^SLQXZS^tso*W`Eih`cy@!om3mf>YBAu@* zE$i=rz=~^^Ck5%&_Ok`Bw$=v}SQVEHZJfB*9ZE%ejIhCn{zFb=Y$R&h;gZ(c_C?IL zv{H0CaJoQ2aY<7`b}?Tr!CPyK6ar}6v6B&Gf;>x*@gIL3O<1dI)-pqE zAgZs8l0N5RwOqTnJXQ;&MIVN;Ad}1&{yeF@F3rk1;mV55|(GMc^i` z_}FRsB1FMMhPNNpv}D&X{4=Yn*!V9Zr1jIF&qQ|I`Fy!mctJpBy4{JLceIioX_8T2UMDP7F~TxBGQu6xW1M*F z0!I!{FeTRsFV}hJ>}AyS4vrlAD2*!@IezjDUY%Ryu3ZD{C<#hgTGVCw;eCj$i&kM^Z(r<=FsvJFj7j$l3x}Tk8Y6bW4CuCxy~k zr@Qj4*wp3s!+3Gq)$doZRh=Yf({W+Z3DXofEr-P7NjCy)g>h;(&2f&xVz=tfv9VnV z>jHt53Y10q8mm2Q;dlZ|@6aF*Ja9Le5!{$x;JwqQsMpt--n$bcEPm)SGCD+1>HGHe ztJfcU`j7uC=j?g!o{>>@PfntgVr_YWH{N}ZrZybB>ma3anOx^st2>j{LhNmFgNZeH zqOrNTMbK`GE2!8kV>gUDT+w7q26p#tJO#agr$(aY6UL{Iv~6FXoq2XzDpI-2AvyMxNVKo z>L`Vlu5L$Qkp@z$c=60d{{6SV!+2l7&mNj)S6Pzlj4WrJv+jkMlCYD;b`jm)xM zv(#ckVg#{pY+@!AzUL8mg1$+K1QNo^*(;F6`%8H_DMY(RiPtW)W<?6w<)e*hmzkR+#iV%$ zEZD9Qd}m0%A*_|I@LQ*$a==+<8QPex&OXxy(|8I|iZH^p0JI#66ry`i`qb-9;IpES{xc-ivph=-P4{mizV{;P{!- zZE)3Esc98r3$)!rSX%&VYkin*L6hcIqz;jGKIOqtl!0(~o{61R5Mx4}<0b7i|O z^j+UXbdpPpz1?L3$--r2i^#wh&}#LLb^lBWF0-d~HkZ`I&2edImC?zajE#YM<`dTfMIcEl@sSK(%)NU-^2*ANW?aUFjs7PF1n+SJU^gZiYS*#UUk`OL(Yx0Y9E2>7YbewbhS+0W2dEn~ABt*ry)1!o`xC<_ZW7I^vD6I?!b zg*|(Aa{v8zGdeN?Sn|X%RA}w8tkMjU1aebke`@!bO2z!bBHw%IHF96@$uEAAu{}HS z!pONOWpOkqQpBa_k=5&*e(rVNe&$tFqsgxRDm!;f5LHVUVaP+D!CgBjj}4&$zg-!n z9fix)GZ&~g>g?RV+c7V#h5!i|LmtPjTv=?45Zz=YZI1*y`MQqf6w(V z$#Y!1zQE@nILs$M@&JeL*v;3UKh9sgeS=4Kk8-eDMJ4M5Lelg+5-sQpB@a*d%$}X+ znQwh(V(;EP)(^{{zJ7J)n>%*xcv7lRTj^nhL`#L0%GGWaUSzQWc(#I)#U>A7$6DQv zCN&n%+9EN_qC}ww6{0&!uh@MBue^7PKmWh}fS>;PU);!N6{An#D@e8k?^^(CYkeS> z^b~>g$xMzF0@ZT=(Yj!64%q9u1a*z_HuKyYJcR{=MJu1l*iKq`>uXJ`v_cg9YOU03 z+1@m?Z6`b+g=4xz!NNIXHeIJp=@=fV5Wa`W6;=wY6c{NO-qy#iwQV#S4VrPnjfG`; z=a#7S_Ws_v8`}PwGNp5u~qM&$O zj5gR*qthIdyE5WVFwcfecZ6{x(6jT4EHxWEa{DynLnHWJ$kOsWufKN|;dvaKo+MO? zybV^nORPIgM5`tw2t2|tBn(4Rt*JK~tSv7S>zqn&AJu9V&)GmtfN-2wi=3>5O zwCMo1;3r5WIZxGJpNjtLzx?_|n}w8TSNfo?_xAQfp+A(WoWV zYdJ{*&BV}*H4Saav;)${YH}x9B2bc2=o5t@8Im}YmCvFQZ*cD?nLb{H-caERr`Db3C?0J0RBM)%f9sBY95TjlCxye#zjL34QKgu;W zcSZ@F>!O%ha^jtL`TpxCnA$(h=RW^9z5RpE!C7eVoD|Vwq+>c3FI?u8uRp{5nG5s> zA=}1=86F+LF9l>mqxyRonx3MxZPaxg7O!D#HC70gu3qQL`HS59*h2)Ra#5n(Wk1Or z5L7L5gWV`z7w-qNtZ;7L_Sn#%b%xGTq_HUFxz~o)2&-L;g4Q4veqRqLg^BZaDY`%) ztw9u{!)BunCPP?-wNMH?`pacz7ZzBX>Lt}Fky8BFF3IHBZvN=aS^m>EpXBVtNq+L9 z5An-C`5B%$aguL7^)$1iJv_2+jL;+mMq@lht}WZj0S`=Q{`Slpy!G;Nd)NI(|7op} zd}DcKjlQvM80llA#0Y`on>G*e?8d;*u0Yb(<78~-y%rcv$rz#{pjZ~&lP#v*jn-|! zMjHq`K=AR8K6>lqpmnLpw$iY+0M^#}z^**YzzV1E z!zt{oFst3Ew;{)~!)SH0QQf}^DUn48qmUb2jmEabyKb`=U5W4#oOrn?ym0nI58?S3 z<@6;=g#?jiWORsai)&oGG>a#FW@l#qn@d-({qo%TGwc@`5A51WU~bMpfuG>W`1r96D!$M<}4YpB)hG}dbvA?WSzr&6sHEJ-KH zzx}uuiQ=uwo7U;Q#nA;yvAn*@8y7B9@&k5m8$i|_Ts?IG~B?*0>YNbpNhQz{S-3H92g3I-o%gs6u zJ#>hF_KTlq*Tk4>1IV)CHD}uFsg#Orz0OllJk9x&=eY0io!oWLUGxtRgHYsI3Yo#? zDP)-&4d&W822CegLkdZ(HQ#;qB~D(v#792)As&3}VZ10pC)%-!g~7o{f%Fw7Y4F~Q zCwcvwFA_Erw)gciJUT$Rrw0*u=s;1P8m2rsPT1Gm>DeqID=n5}WtCGeyvo#pJyZw! z=|nBGM*5~;+ft|RvKyJI_ABKU61wvqbmh~a^BkRPJS*_L!a>@un=x%55%l$-edXRu zvU93wMO}Cp8mnudU390m7U_9RkB#xll^GW68B?X0#Cilm@Ib{d`H`Lc@mn4*UYzCX z;!}M7!w>SvvBTUkJ^9ULh7S#=4Vsp*?a@Iua=q zNDnI{S#DfrjTh42Kg86Orrw0r#f2~5xP1LLV4V6;_)_8aA9r%lV=>9nxMabfHX@u zd*u=r7gxA#dIuvtRoCHI3 zRwmCKwO|XKnQ3(@!p*f6zVXb9)X4dn|LUjNd*C(@k}S;~s4sdqWsyy_eC0Z?e)Boz z-nvM?A2KyINbhhTGW0Q?LJn5x+p~kxwjop)77VUqK3o(D#A|iledahp;4!&>4+zg` z7~9OHtKl(eiq&2EKkOzZrW10z0cEwi6{WyfO_t}_G$+#* zBU9WyHpJ8KUE@!ldVwEHGafj!k1v1uOMLZ-Z}RM^%iMR{7~_2&USP>H=r@uh6JvaD zW>viU+HrBuM?ONFXsi_zAa+_S-mrp|wj`~q?AoQr+s=rvOL1y&^<8D@D!pam^%~X~q;i8c zV`b+;ExJy_Ezx>c48exay9Dgr=Rsp^yY$(%A_cmm1-e0>k_c5e*$R*aqH7dZ1cDY3 z4^m3!o3f^z%j5*t9cD=jfx!heX1PWQ6$D}Fz{p7dZSS5s^Z4AgE1!#+^}fe%-^sp_ z0Ys9~Xf~ZTEi*J5G4IXZKq4kG2M59EcJHz%$wwg}a5A-FDx9fagw4k>tkW60e;+$?VcH#}3`e9lLi@3I%5umiVg|kF%1k z^OMJ>*%=CS+8}M#kyc}^Vs%Y3JJV!sDJIV|w8`5M6H-dF5MAZBZWA|^GAklrA%_bV zuBQzK2g>{p|M(Yp?7qW9xh2=yIn9c=4qJe$xZdRLSKi>n^DlGT^b`+2`XJT5ek=l= zX*b4cl^PeWNKr(G7^B-SO^fB+jXD1IxtAH68s%qy>eCF2jk@O}*Y1^A?A)Fdq;ZpX zUwV^wo_>j_7PGsrhvCs7qTUFr1i3G$PEXRiXA-}s>KG8y%@(`Ywa!wmzjcP$D>wMi z7k&i4QfkBMmgL7;i_qGQMXl`$3f}yl+Q`amX0mJ{WY=kqPBN76@GE5(ylYKY)sp*} zN=U-KDosQ?hNPW`W(`VMvMeQSq?G#H*GK_Fqr+7DhL~BJC*IymsEXIL43IXXvUY<{ z3|BcYFwI|_xXOS2##p-Jj_ph;nO_xoo9INjd$2LImm(W9{d0TAvieP!}ZlV zXD_|?$j+rhKVt>P97(ev}P!Zs7mQ% ziY+kIT|2b-8rBxa0W^U3PaHKA#+|Jx;TFK!S|7}n#5v9Sx*H8y2Wph`-O0|{_Bd|y z1!M!5>iXOiAjg=EU)S2KjL98yVCgU?)}7p2>ErDW*>Uht#4&WzXk6NJmd7Yhf$)$m zZCBw~6gmf^ktQ#?{RBpOAf)F7r7y0oFaE~y=U>`$>5bRJ?bVX^k)wArS}Bp#)=9J6 z>9sVFWh~Sh%&e@leR!C`zP_T#qv#JT!WJF9;Ayweyi$ z!Jw_@K+rYzZEfBHm#G9Kz?o~;IdSSV)pEdRAHARP-U0GFV?Hzd)vKp?Yhjiz-M5!R zgF_@~lRRlUMo;>zu4t}bZBkp#$g{jHU>8Clt#BzC!U?NWVM(RI>#NZ08DKWnT*`B< zr3rWL+sVKDCqK&_J0~Gc$+b&$clX5sG|$4y>>}TL>bqP$bB;$JJ;tHC4=KNMCSde2DoQ zwR9#fnhv*Tn*V+bm2pB~`qrRDcwu`OHt)fT|oTK^xdfBw(^1;6?$KaUn6 zm>4Bx(E;ZnFvucrNk|W?bENXYx?hdS-QVjBDLr)YtI1RMR<=1pDvXY?R*=Op$6tJv z#jDp@U0g=T8A2+AP_$xhOiNePx|npOuXnRmridSC3EGWuCwyZZ%VUJ>e7V_>a@e_0 z6&E`xg=4W=fr(;cJz|^3l9Kw`I+=~Bl&bh%$vNGOMr-X#Z>dIk2D5RUhD`a=C-3LK`02;#uT&9n=0xPCb6>16cmgiJcY$v|@iZoG z@`cZRl5N{3QJ!CN-=Uh>?LlqTVW$1572LyknHUJ-Z0{`|wn!BdN^6ZNl z*OoFydn*W=A&MSQjltw8+6XGX=CgN>aA?m2fBM2H{^+R}Id%3jAHDwwf#>n&tFLfr zc8RIcVSJ(Ji#+yLl{|mt!r|4WrOytIkN;6gd9xN7+v1>BBy2bqv$#fe9|p55!(=IW zGo~V7vVVYT5ER6qq3eXi`^VOFvs(t8B`7k;R=9HNIHGs2lW29VTz8(+Eu^&tu(sC! z%j>s)`+sSFp4L+uNs}yd{bD*x-B`&BJ0~PM6)UKq*fHJF(3fGsaD;7blVd4D=h7?p z^2moD;nc~Ky!Px%*xXVnmC?F5%@w7xrlmQsT}d&vE8~h*FPUloxv>VVajEWP`BlX@PiF|DQPSx^Wjm;JV>7D7A}t7Qh}YIJwK>G#0OiUMLI&>V>(;ms zq_#pL`>i#Hm*y9K=D8=o{bN^8y)$}nbdX~Y9HGBbMQTHu=I*h!PCRe4g*@l{>^zHE z&W_;`1}oJ9Na;dXR`e`t+t%z@(*aYRaC^I99A}KdO39_0b0`V--oBqo6mja@Io`N< zg+q57V63N#&NEE6Gpu_)(KQxqGf|>5N)JC6Qcs>|)JeXU!~TXUBWjJ=4=O?YpbG%zH00 z?~i=tdS;Ix#bAU&of{Kd-96LYmG8aGZ+?H@-}f78sH2G-hl(p%judBsLO?FY8IPdFWAHGv}p4w_Skj_}HNUZuaUm*>9tX-Y!_7F4OY z&8Rjr@0#qRX?7dx#5c@gVYR}y5B`YBr3Jq7bD!b92k!$%qTE~X zAlwa#TAqSC*|Y#Zo3R= znkuE__Z1LiGT^$zHIZaUc9$YCP$^r$CLE{bPd7MF!W9lzXRF-5sTV0?8(b|+tD7}4 z4UB+vqs;zno?rRw6TEo*D&ISHo}-tq^W5Y2u=BtHuAMo>QhAM1Hjhz?v24KP?Kye* z^w}@>ZQK54jGU2ygizwTE-GOh5ulW^2f$dNlw}a(Mo3&;Bj>n`^p(h^Qe8UH?)mHZ zoLG1G6b7V&W2~1NdJTGe`N*U9aPX~DcU||pb*vj>17K~8|D<950&}@WtkAlFQjJ#r zm}v?T|W(JJMoG8y(g@Bq^Fc=HFZ(})$BAcfJ|`Vl4H zLw9&si4I*+SBVf>pr)yZQcBTq>(pv>YV|6f=aNpRk&c6O-L?&bHlQ>{MYg;ln+I>( z8rWfeo-k$#6GnuUGL^acL=^8L-4s%~R&uIA>$qr?-nLd<`^tr*$G?2^l~>0z)n)hd z4^4 zEjhB*;s*2c^X%KSnbE!hX69!3(fcRavS|}L#)gUGh`1%gW!>qy+4a}dsfpI-I`wLz z1+MPgXAvFE2tu@3FAP`a=lI_HrzoWZKKt-}4EAJ+VueopujU&Ke)rJZq)o_I?%hY; zRa6^w;yA=XQd@)TmljxCtlJ16RO|g~Aqa(FO)1u-L(Oq8JsF;S;z`E0@8j_KbDWyI z%Dz40eErMMv19X??S>2^>v*O?V$DOPl*l;Z(Dx5=_=ktMXYUT4{OA*8^I76XWKCR~ zC9`H(x8>i|Y)nvzy@KHO;v(OC6k#btIr zbT5VeK5Gp@TYlY?AzJpTVN0CrR)7ym$unJ_Hyu8}n%YW*%3_(o_erNyxPhA-oDwkD z{4XgXJ3)X*rBSPuq-5O*+HC2+R;$^mN=!?o5abIP`g?o0Jzu3(7X&G%{m?WI5?T|R zIyGT1(jn`v@Z|?bd0=~qKYaZp-#&DN-8(juPxmrgUBh=n(n3)3UHW95srQd;PZtXh z<+9ng(mh$cAcdbzi}GB8Zvp^ zAg(Mkb^0W3w8r@O0H1mCk#2zH0nO^}hNJriz}gsplK@PyT3I8mSCh8jCXZ}7><&oE z0;0`IG<|7AQ->kj^uA5Vv!{7%SZt&7|ZhqDDl$9<#Ezgy%WrvRPtf<2oEK*ex`DVZHXq)YYqBfB)c*?hkL=5ciJ{^5lUh zCsalSS|&yR?yWrZ_=m_34D!P_-sIZU zH9qs)Q+)oJCmHDNu~~GXvaF@HqHC=Uf#(s`YP|TzFLLI{DL(r2V?6NCep1;qDvs^F zu5{P6Xk|bqRt`Fe6VeK9%`Nh$FTaLNd;H=r{Tu_ECs4|kG@E9)QVJU73ddi5i=!{S zMW9`F7xRn{Eb^-#^@#VM?F+Vd;wOS{WP7`F)h*XdO z(RSCWNER8TK)N1oCQDr`V);m0;}SyR2!SzzM#DZQR$pfURx0or=|1ONBPd%=Xv?P3%D+f`W`7Srsxaufx}24&-Be{ zF?s6j7rY?#f97&2ip3)7o;+?ohm%R;qyjw8$8~)i>422f%Vko)rfr)k6$|a(qZ3St z_Osg!xO7{4)jY_qu5#_fF(xmaC)<-{d1Z;6qg%Qk!KhswbfiP1Hw;HN0M^F%8yUXm zbfgPAev`;~;l2qr;+H`DQcM5Cz-)J4-xE{Oq?<4R7j=gmRj3Au~ z5)G9}3?Mr?0O3x6)pgLbois*UtxP7HqbFZvcIFmKi;HgH`+ql|$z0B)voC{CpwUri zOccGGRzRW$b>BifaNb)AD|{i#<78V$?nZQeA-TCK_tj=WE{kmrHj+d##H zNeQv#4Bwe)*XjnKi@6d)&=pE3)}6e&$PJMgw#wG^i-C78UFMBb=lJlR9Xz#j3%)Rf zS|J?EG`JpIT&eP%Bd2+AqL+I|GQ=vR)~FNJB9`WB%-^n|)*?J1tS@`g#&3jRO-Pn? z%%ah_Bg5=}>>+mS*@rS7|M9!uCt6$NAN}GNc>3`NFp(`U>sD39b{rZFzV8xNEBx_q z{|VPmU*PjU^>McE-ht;is3@kbnMs(dg!L6I_ek3W(A1eW+m#R6X}@5UvZbda`?6DYXYOEJ)^jf1U`+}^ zwY(FN;=$KWEwuo;1?V)5+8W9zA_Lj+VSes&A7;yN~*tn-KIPgdqtC#Chgcm7HBO+EBCst@LCUQbt&m*i> zal$$~@4bhCzP^?TYZqnSdExHHW;Ok6R0L}^uAMl=*<&Z^=`B#LL9|w9VtD+nYrO{R zzqTGX=87%bh<&Qqfj-dep*Tca<#VXNYM5w5NQ3|aTD2*!|W|x*Y zF*%J}D3XaH3Od5`Tw-k!(XMEN%Js}tr=a~VWngVUtC(uFg6la%!tmOWqxAI^czEA- zBv99?)f#U)OiC@dG6{1nesw|@^T#~GWeqH59Vmw ziM}i)3sQ44R7^a0FZVwE6oq^*H|OR#`ri9Y^r!eA{^8$eWUwFAXrPUNL{#016H@l@ zQC?Z$|NXV!q`th$&wT#VY?&BEIKqNoVcHpbO-yTaOA99wXQ!s!)wQXcy!gW(F}iJ( zpa1&j@q2n`G{WS0v*1WX5!2@{bMQ}oKy~^yn@a^IhD+poGQ@!o#XNm`c90(%MrKl$ zX=~1Hz|E$uAkQd_k#vTKTa01ZQ$Le# z{d9?KSP&%Nx7n?klvhnsHgsH<{-F{=7?#R4VwEG1Vx1d|Y|oq4HqOgH+^Dj6V}`y| zh9@4~Pd1<9U;W0PaAS1^r5g+fKEn5@*EP3iZaCxncE6yNdD-)Qyi^M5dPvU&A#sE? zO06$16RxiE;DZMk8XUA;xUuSfUOE}9E?d&(8k?|)u|^2P^^2D{aqJ|;z9Kz6y~M&` z>Q(Zl!81?n=jFFg{{`#z#@GN@8{>n=UsF9j@KW>?ds#7puv$qp?V=5q=&;5!-Gr6s z@|o&hplNQ(hK^Hj@;#+wZ0lyE6kI-i0TDyWx1xHLXjOF5Z6!L5KxsR*w*}W$U*$+! znv|YPW@NZAv1wxV*zpr$=ElucjM=`+aeuE+>OC$XlE!QkN}tlz>i=duw;IMF!l4TK zkZv?kN+66RmCg}lQbfA)&vL==$E(%(gD2j9-=91;{h^iV>B63&qVu;NyPxfwCMafe zmZ7rf#>S$=Avy}JrGinmC0#nKu9i7;`W$gC!;aArR<2wlmswu06~O4-d#w3(}J*VZnXdz-aNvIIL> zD|5%{ZCcusrhpj(Yg+OB6DPTF<0fBz_(Ap#^kak}(%LedXoLe^I?eY_p5wjQ>wI=+ zKYP?FIu~ z%w0>1tP_(-xH5U2@4oaJyYAb`7k>T)WRNCoM9I8b=1MeboO|aaZ+-hm__ZqAhWi*E zE|JNlFlnFc#5lzroAC<;97kGUAzF`R7u#z=wzZT{zu3)Cva~dP2u(ycgs0Q{nQDxH0X$h>ysRL^Bsa9AXg~a`zUUZPG`tx z3%v9P|CZ1G(m&vvFCFeaO*gBHjst4L7;6JyZHx~de~rhHAV||wk}PXQeQnM9R5|u! z-L(jayL9q&|Aj7!yTgaLbLYV*8%{g6WizhlbK}xA;#x@HdAP!FE$W>KXCiSCZ##mw zJKCCTi!mDM3bN@e>7e+HhaS4`)Da#2%8jd&j*7#F_wC*P;X-de#`iIewHKmz?SBjx zX1=Dv+V3~Y%hW5Y=rBYE9+~0*nbHsh{zQ3Y{#R~XIm5a4-hEW9g=`-lil2P`W6rkG zAgw1%i|twx&Wt<8H^D}(PzmgeXAjbHsu3TVFgh36^t^g{_;&ZJs~|zBQXQrUfcGJt^CJvT1>5Sm zOwjU16HU#LHXYtiAhpEvJaBF9ix3?Mw%O^ZZS#n5l540f7|RuMWHLUDC?twvOyJu5 znU;>MBQr2Lz(}KUg+O75$}5y_PLnQ{ci>2;58>EQa~+11mlj#NHOG@rKf$&gJ8TI&R^5y=rn8>g{(pzHjInt)XdQ9u z@&(>{$uc60M-V;+8BSe z@y!>1htK@)_h~(2PJhF8#y9 zIF7@u$(xu^;k#a9uGnVJnk9;6rl1tED{@6TjDn_H5b2@L->tPGwuA2AZN^ofurk_D@s5D=|coVs>tU!)GpVJLT}%uYQHBk(~PO zOY})_9f?xw^A=6p#lCg0Z+E7N&N5n?YzU9J5|MF*3HU^cAr&G%1GsZ{yKlaDZxN)tANtD855NI;$E zaOlz`tF;=B6${L-tT20Po^m6x!Vos-Ru~HorE#g#V8ww+*C+1n;kjp?Wo-Liu3ov! z`7@`OC}sHhpMQaEJ2ydWeX2y_=q_3yNuZ6ya|BBZ^Ze_7^=~N<^QGsXp;#=nBbGYZ zJJU_pw5{88;?!-paCwsNzWf^ZKC++Be&q#(AGDm=brOeCy>XL+fBqKN&P+0x4%j|Y zq_?jJkqHQf`{>=d4Yydt@jT1Ew9O2;W6pky<8ivNl8Q8KF z>8C8XY6XWUkyT2e8#P3&21>QHWOqO~x(-_AZfC#bAQ09gP8~mwqoJ5hk?jfad$Kq| zfRU2U+!)j4S7$@hUC%)Vt~Dp6gFsCfRow*1!fCBH8bK-(kV(7LR74ahzz3mQ9nft9 z5z%^UO}1SMhrpNAZ_lyTH$!U2PCos@$GLQEisQ#GQPlyC5QwnK<+CTb|Ivs4FDEh{ zM*pft;7ZBzt=lZj+~(O&evBP^ciTBuiVpk!JHb`w`)>hBd!6FA&h+KW9C`f>f*@dg z^EP4xt1Bz?XS4JV43Ns@s8{RgsKHbBKXli1J_h2jv9U>R0IZGiR~?`E>F57S|IYUU zdItwsS%AvwD$=-tk&Y9F-G9J4yA#9T+d@+W}a}ZhVOZJ z(&=DKOsiBVn*dK(mQ70Q9&U`r3w#QLy<~D}qyvtW^o|ZvC}#iZ+uwWXk0x*2ltaB) zMhfF(@+l#E^5|Yk6xL#Gl#rlR9LZS4vDVsk9fu&}P$(5B6^mptd8FfmmLv`fh$Qj^ ztuRrHiDT1^IsA1j7Ws~_zK%6TLv*j|!CZ{Rp z3MdR$*J@~7LLyutF*0#N*HA|i8O0ig+d&SwWg8!Q^eJ+EC6501U6yC3`OyBYyzuD{ z(bLz13RTw}iY9Z01S9ZWm*vGpe(hiW1|w;Q7e4VZ3Ozk7^UxMcl`yBRoXeI~TsuNX zDb3}p*ZK1wyv8F>KFDW2{~W?kp<-o!o+~j?#Qf=tyz=i~W@)aa?&?)M$3e%6Vt*fFd-oIc^jOR! z?fZ^Z42=dNjvx+^QH1Qs!808lE!|y$>u!SNIc-Mzh$KIB`o=7iHy7wlrRXna3GyjK z$_Ll&&|8|;A*FfG3n7u(;JOa3@1p88TiQyrfI`?jOr^DTdPgQDKiggCdLH>)nj15# zL{XyQ>@ZSov1gV#Md=t%_&BbQG=_#Y5JgmP&XDUb(Kj^8&wTkgZe05vmdbTJDbPCN z^668kD2iM+^F@sQRbdo2CnuS^d7EcH{V_K0*ny61jL+Sx51pW@qeHNH1+*awYg|2h zhNEvCrYE0gY;25BYvvbL80aZ5++QM-P2)H|jxYo%i7@cB&whg6d+~=G8{!7Q+8F=I zqw6SH`YV4M-}NZ=4`7h2tt_F1Ksrw1XnmIqvULGuO#;N2_K8;{G`FB54Ar#T_Z`n8 zUo0RUhn2Zy;#!F3dPwPX>pD6CR>E94t_#MRPL_sC^o@^@%4U#BNNsMQcd)c+sCQ!T z_V2v%AHLbRbnL|Eoa(aJyK^gB#)iobmq1FlR$n7h%0X)!DRHDjHk-y3R{ZaIzA!?f zjkFnH>x=+JQ=G1q&Blv0IyTHLtZ?G|q?w77$nM_F$De(U!r%~Rj=asK!-sjSzYpJW ziB#eP)*;|;l?U7ORr|o3SOExa!|+lykj~Ihsuf4nDt!oJn-hd& zYO%sXJtkeuqb1a$y0rt54#p^A;SyoMNJ7=1Dg=$b5lUm58QXg=+DUQz@L@7E_?c%P z=ZS}Q<7HBbaj0kkt7e9qOg3fLu}r|P|Eu4kBo&`~_M`Ol6l}v*v#ni-Zo!!l3D`;m z?ONF&X=S)Hd6RE`?^Pap`~W}w#h(H{Kqrila2#}_&ZXDi<@Fa2LZsQ+mto6TiQ+&n zZhs&7ZDaVoMWpL>l#OV2^O-xJyJk5#Tv_Jo`^T8Oa)p#DkV28lq!`(=hxFix{cWW{ zI1W*xMs0DC+VTqdTpHi8<@fH>&^m^09lAEH`@?i)9ZCc`jyZesA}WmOFXrhj<`990 zOs8zDQtLS)5v!}Vy@Rw)=u!$&nH15=ss);|&89XJ;@77mnxw<9rzb;n6B?myJ}?r5 zZnE!P3FfUP8`t;H2I}>i^;umlQ=41B?;l|Af!%!ilTYx6fBHJc@kt?>TU_Sm)Rfq^ zdw;CMnCq7>F?D&8&wk;j*tutS0vI)P85wudpLY?oxBi+UY}A=Ne~$OwJWR1zWMpI% zg`&K&OevRTVrYO&%4UEHDbcaA8q0dfZ~m|U69-S6VCw2@>svZLsIa+nONckD-8TT% z#`ufJU!x|=b396e17H-(i;Dy)-$sGx?jP5+T{qjSO$QZXx|AVZNU+PT9dKNSd_Irw z`K&IiP+zO#ICgWTrMm(lOj06j3fnbNs2H3WWq9)hZpJzz8|yPAJwwfRg70OwZdG6S zdw+Z3=)pISoqPSYfAseI$MPpmNJfSR*|v2v1B1QfQW;z3GXf!9V(Fl0iYSa&m|ft+%N z@~JeD5vXJ?TFbERG-)(lr9q59Ca&(4M#R~<87?o(GBni7W4pFc@&lAGC}}Vv3DfN& zzC=5#kdBg9N4AZ*n`XJgT(-%o(ZN7SZCQ~U?FPUEI)nE9(uhLRju14Mq|*) zCMk`;Gq4)Qym@_r-p!jy>jrIS+7(6Cmo6H0;UAHy9Qf?N;Nb91;W zm$-j(KOcEyFPq1QY*$#UEfA8Xi}`7)#yr<$Wu?ro{pxR%s+IZD^BK%ZkqxY`$8{A;wZ9BGUv+d%V$n=^2bLh_4m==KR~P^)~Zzk&tr6`pKK=6?%>pMQtox|(-Psi{PNeo z#y|Y0|FU((THp$Bl-bCD-T+t|<1ZSqQ8>sCHdwZgR`b~RlU-EdP#PRW8^PMjk_~E> zmdR>w&+9c;cVV~8-HA5QwP@=k)6OB4P9X*)u2(^YF;ZG~qh-I8un#F^?2%_a_Q>XYcAcL*e)8ej$!RW6O>*+!JM^S| zwvCOjYx_8ZeMMX+Zq1iSNIZmFt%9WX0h~8dy9(Z7> zG%?XTzGL@yjU&&TK6d0Ez4x8(vt39w6>KM+>hMo8cgq*Vw9B;0ZYEkOX3HzQKRwNI zxW@gvH?w^A7L~1Jj2_Qw^>YQdF=jsSiO0Jl}jtQsT6^9@Ei|U zI;_<~=2ptogB;$_C}B{bvRWpHme^lR^QkBHuxm>{xlBN;BOJUAC?#8lxx!8(t}9uq z*7$$^&A+2OdyB7q;aQ43#Uw9K+(9~+L@U!QYg&_At83E;Zp4oMH1~q1JSO_iq6+9 zZzrfPEO7b9M=0O8PTG@r#+JlVgF_UzZHIIg=~{PjsTI>F&T;(kF&=z$7sZ|&j^G1g zR73~AV!91g@AfxEI}7y8*-4hl4R#Nf7#Z$G1_C#g!b@9=1|3JJMjfG)b(3#;awX+F zlVo-Jfp7Cmr3FtyCYMvTS=Mf+F8e3jlS3nkwdv|eZUWU#W?UrNwXhMA@6F?Y;3IqU)G9UN(6T~8CZV^9 z*g1+Lw2s)kYYY9O!$>ayR~>ubFD-Z3_n3jFjvyU7U0b}Jc!L?@LJ+1k)h;2OHq_G9#Bkzo2785&CPM~t;1AqUMK6h1j5Cjkm(HlyLaOc z4WnHT<2XcXHKtCT=ja=Ux$nLmj1Cr&2@5A$wh!x4&rJtY)~T`V%(iR!Y1z41U9EET z%r(*ic8?E|@68dbh#?H z)3#_E@B+KY z-NAUT%ey!0yREu5@D7kA#5%J~%ZPZMi^yjP>kV`qqhkAhq!h$)2tu=I=N1OWMiK{b zMQ0vg)7BvYS2k!@#YBx-95-q=qiX$^wbs8}SzSX0IiCFVC-L(Ib>qfOb@JU~ym{;- z2hUz(IG19}Xg}MxZAJP%)ljjzRx@iW%VKtRp844&>h%V$@6tCo%-%hF*s_10D3$sN zYjtt!`V5Um;z4wqt(5Ose(>RchO?5y05V@$}(3*U|ff( zwKCs+=Li#}0-w2eFB!)nQr6R}wYP{4fSrWzmNmR~^(IcKkIz5%EajP7+_-jw9wW)7 z(`YSNT&*x$t5U;f^RE4PnJj0|p2U?gpWeNR&)>hDOiB{R4MZw!OS+C@1Aql>uPI@< zQZ&>Xb^gPjz08TD$N9xieuyolB3eb@ICMBMv>P&-g|B4Zw6Vsv()C!a)cEGNUd3@F zU-{Y>2y%HM6%!earxn*vp6BI1{x-5wVe4ogBSU?7eR;gm0kWe*__-X?_d3e19a<~8 zh2(ZqmMyD|%B@?RJM=b=)Zqpd~Et?TR+PWabF}F@%;P5MNuy5}c zwr&|k2xnaxSab>{JFfR#0*Tj~e>H7X5L`NcmATuq>>uo7d}06`iI6USZxQV{)ay0Q zoViTFcPVTi$2buJr`_e+Q7UshC+QSUeukvw$tI=fV$eF>+A}%Jl!Z!Ly;0%?C8TWK zf6aN72pkFZT8)N^N%!OlYjxsk4UM5zUL{?xky4tTLWV~l-phsQw+J;j(lz75gOyKy z`k7oRovoNC#Awm#Qtdj18grNI(B!p_sZ}dnyKsTC$4}DR+e>e6AI2zZwJLEOF*-cR z$nX$>=h=v$gt3+gj5H_&)wL=ZK{nHi<9Yn%ul^eM{r(^EPyY9Re)mOwBcpHwU~P<_ zba?GD848WUU?W>Z;&?4Q7x9<4Y@XxMKRm<&nziyOp6?}07F|t6bgDJ(7Ta+r7Q3^f zhNe5V!Eq&C;1UZE(w@*%hdOc^001BWNkl#?8rVoIibul}7BAHgAe2wru|A6B8p3k8B!0^~h6?sEa4h%3ISn zIW;p=d*ugjrW;|nv%jx@ujhM4Hfl2|$GbSRZR=98)a<vNISM9}}S~z9)q{v$nGO z?zO4wEX~caaP9h=;^r(5>M2;aDAdU?kcIYHpmXcs4fiK1+4DCTrJBZvF_S*3Y-OY^9Y4tYITM0z4K#s z^!D&m_v|7qC2NVNmrSl^D?b{`FgOZ~G#tEsi&JYMf9IFJ%1EKWw|?gjh*m2UM}`Sf z0rTZDH!BrHzQi-1`YASzjq>sjeu%3=zI@+SUbuf7e&AAxB7EP&b3F(Gq-R+V%l0Jf z6%whzD8oxH9pcdIhxq(s53qk?l-MYQk(Mdc0;DY~wlR)wm9JW?tI(7yRlfDoJFL_i zeB9 z`1u?rodrL|>g`!BoSWjQ2ltS5ggpRS0;VgjNo$SkNE_Imc#&AifVLsrGI4=#8uGSo zdqFO1!^6Ya;5ZIKXv?7L&VyKD_6j(7#7a|bgcJt)NjDnAjRvvO=vs}&;v(77ATkrM zXYY1K-#p3eOqDo}MaFZ!lF8>*;#mJLj+86H=vH)(ohQpuiKab9^7A!G=cH29)>gTE z{yZ1YoTax^qNk^bQZe;f4HZQU4-POkGTZ`J%4l@r&1SnA4ds<3qR7zOH%O4m;dlYD zGCcS6$3GBYIda2ybOT^*jK9RN53U7R&3eBrMd>K` zE_6*WB@*_;e9(bNtlKP1ccx*Nq_(AZ5lN?@w(m$O98WSZI!NE}0K#1luGWLKgnBp1 z+8}6^guEK+)hc1VPT#i8Y#SVmC6a~txnH??=^~NVl$X|+xi-VX@)A+3*m2-NKK9H< zwhRmp{m*_novK$>e*N874;{XJ{d$a_WoETbdFB?b=isMOM3G|p_N_wW`t{|~;tD(R zS+WRJY|tVpnC7|D zazmT0O6J$x_5SG6San~dR^&)i(4Tw;Axy&DU_X{{b7hIw-ao;v-X1=6?`{IerEXN4 z@kzd|Hm2p;EsbR<&aPGY&h?u-``q(9cF(<>KJqT7j-F(EXqdj<0p`k!%vCBB`^Wj( zfA`!Cml)l(gHVGe z#x!c2{Lx{KeD76Cu4MbzAcdYBPG5oi)=@Hp{dhqNTsIMB>-L6kK0vr=Z28P!n7?w3 zGjF_uyShX%o5pcGlG-LtF2~@#`|*k;>n0yJxOwV4Z@u&en+J>R-?P;MmIQzcr_Bhp z4ospg2=5G9Z+fwH^%INEuWQbqpXBDP6?P8wvuS(~&v%GpNl+{x(is$n#koc1%N2SH zS!)YnWSiYFmicUYc{q-=B|@E)dmVcX>~yr>M`xa*6yT?Pv=M|_BV89mWba9tFvUsf zx0N^{C60zhxq`E49Ix0zs#2v|t`f(Js9Ywll<_^u$mVhO?%B+_+h?gsL3w#4?KtA= zjw63v7_(|xqDiN)4*VuIpLXVJ+T3R~rE9zohY3 zq-+gwvZ*DG-AUmLNQ?~)Iu4=UAjm36<@rfP@0Q8(=^q|q{+6azso?sqWwJU?w@s#_ zGan7Qg5vMaV0FA{L6=LrG!Cc;Aq{=~ee?|uAYAV*;WE4inmI=%E zFflSluD1wiMz&9|w!BPbafL>u#?CDhgi(WZF~|6p?eq_gUd!fkzv{SNy;8}Y+j8GN zl;y#5yS%XpNKTLR#%~O)#UZj}~D> zqY*N-u*AuQX)1Az{bNHsx?>AD-y<|45vg`SQzMd?nxq>^cLe!%WCM1hVAFKPHgq|- zw@8yt+jL_Was$N1FtfD8kB*&SM^BzlJ#a6c=TeDuyFW^RNlH+y4p41D?{y5nbKwg6 z9(;f=Kl=%0uV3L$|NNVz(mA$FY$gnAER@$UL6)EUJHNp0z4vf?c8c$R|NA^Sk>eXr z-a{au8pgP;#B)8IOb$1j!|^-|QkCg2R<#LsC(m8r5C7;z9vvUyllSk%4_v}Hw$=sO z0xHwgY$f5sO)V5M*}#=X#hR0+uW#qt(r7ky=Mq zVT;N#EoM>(SS!~!^!6z{1P6AG(>qWgGzvGH#w!$%o{ed_GC57g5#$0tfgL&_-@^?D5z#S9JfF+Ms%Itb88+r|o`+l?;*X6Kh#Dpwfn zEzwsTM0kNMiwXzV@yJBMUDx!rSS6Bzjcs=WVEvavK>bAH>tFske*543K}QKkp}T@< zbi!mM`eIk&qyysBDrRXJzX&p&{Yza!&++IR8fMJp3TQ^F2yK z12~SG%!naYA;u^?>0@GnGSM*;YXaZrp8M`$+s++CT9HrXaJ3ai-%BI*rqu|8Y-P~3jY)+t%vUPBd*(b_ig`YE;9jKTv!*mE zsVy}*tPY(tS~v)4sJMm~PhG&z75T=eo+Bj<-#K`Y+2tkf-LjobCg9@r%hZhE`7eH+ z2OheQO1;AG|Lz|!>__~<69*_s$y&V*PU7p7&fw&7xW3oYNLfL>6+@$8a%!62`i(zi zciQ7~kL@R&a&75cCLqQ*_8t+=5SkuaU2rMqh~YNj))$+V-}n{1QlI3P*j^2I48CvUK4bch{0$H23KbvK{I$z&1I zp|Muu+O=u=(kVPASuc`&A!CwA0@E&WODQaaMdH5SWU!3E^W2X1*LJwPOjt`R!4p<5 zCrk@uwaT&0Qmhm>Mk16Utkzg7SLhuXAuJSVlq-bQI*mqyMy-lh?8T8Tn6mTb;$t`$&j(S+e<`RuSdpC9Yk*!u0hUHAeb}87%c9+!P2m@q05MBOEvUz%_jXTx|fX zjj?VN^B%wb?|#jGpsH5Xiv|g6QPObG~*lrNCUW3*BvFXwRH6eg83Z-I#z@vYl4=yzvXWzNLa{fF|_hjku0}<)=MkP94C+B{2;{F{q8q`2UW6P)E5!_r!rH;`3VBYPIltE3`%R3mRC9W_A&YJ`5oIS z?Ae0|0!$cEU0vnyYi}`sV}?&W^$5L%9((U7jd3K7<63ug=~(SqhY%ZWS|F>*u(h)< z4PAMpM&JlTSgG>bq4)9qGza!=Ctt`DNsVw^+(M5P^6Qw&(h~D?OKjb{87TzTZY+7` z1lAE$C(pP{g!7I}OlJi`S>{O@YvIri-j*#OsI(?hF@f(V`nzT@JY8kamaT)ZLF~d2 zxN*el!XkySQT%KcCzrzsHBlH7Rcl1G1~QeWr?;0!AKK5UtFzQ9YxSAQ>vD4@n^h{F z$7paI9O+t|D4VQg66d28i*vJFy?Bv@g$1(N9GP?mrD7WOIwn>O4U`xk8%>xig;LhF zUMtn=B3)Wsp;iqU=^vrkQ$o0Dte`HL3n@Sb?WXdL@t@t08&mK%J7(WK%+hLzT8^Q< z2+=Y`D=1Y%>lkff+d0<}ZqYp3#l{f24zkz-j-;`&g07aa-G@K<@EwnVkzs;#mMDt4 zqaZ|QEvoa=clXcXuK%_PmaHJO*jpr*@A(VUcdbE4|y`5VI;^BtAzbA zcQ0satx<7AeQk}!`B^UCp5pBMP3G%m_6+p$%)VVrlu8&Oi4sH0rkNuMgoZd$RBI8n zT1dHEV|A_0@>&c8B^;!1z`#mng||^!iS=Rfx;yiAJ2KYo{m)5rMggS!~<0&4X-v5qat;rsac9)esJ;d-c~!_Eks zDJO+Pxm@9Qf8!5{Z{OyNAG(KP*0p7$L^Gt5`$Q{+Rio)O^v5IiR2B%(qgZF=UhymYcbAKNKg~_KGIQQ*c_rt3QN?`N$2ufX1tmM^-HRe{ycC56{lSZg8oH6JRx@9fKGUikLs z{w0qUBMy}-WJdI|qPk{)!J1iXEqR*LYBtzhTW5K9lWk*Y<{1ypP4dX0LzKM}YVb58 zy52lvX|)n|Yb}HUiJ+BdG>t)plF{mr&&nC-3l#maPInY*x?zjLS>WC0q>w@oA)S^- z2JEFRUf?Y z4=-L~ZhoE*Jo!O7M)TsUukgmD%bc8iQNR{KUsObmBO|3PNMJ@ZFbq<*9Em8ipJg z9d(9)a?I4JBUC5H9ZNs`HKKUZ)%vc;x;gR3?VWB@hIJ)ha zeDKphelQ5bK*}IT=g#FPT{5Z^pwonvo7cH|@e*mCQ>qLR1ra9CNRtGs4O0{2%+JmC znJZ(Q6MBHT+T7fxR%>x|{xHLp2{(4`^#OSyT9^7rI?!z1+c@t5tb4B?=+)LB5_jd( z9IQguo0vuuIWkF5F8S(+fkg)}ZtFn^*Q!n!5Dix_wI*q;MkFPoROW}_lcO+Ze0rMA zwGDKZpyX{N)xHA4yUhrB#|gSC@Rd>!MWGvKxYcpL=EB~AE%O3WKfS;ROOoX5uI!+# zW#Z@pg`pCm5v}-W2CfJXS5qlgo!AXt1c+i*w`xZ!7Ep< ze(m>v^B;56W*n=IlW7NB#6XVH;Lm1_B}-FkJA16GuW)O3n>q$<;L(KxoLpR>P$;;R zciAst?)n35ZrR?cv$NAg7*JT+MzfcuWLOT(PY_E}h1tag9{8~*2;vCSZnJdeHD37AQv_+s)c7c20Ja=6d~}|X*(sz_ z-jHDWx2?T%EG8G5B?}5XJK#*jKW1@sa6$*$b@}t52o#0Z$J)7qBiq;FN_y3*yT1#P_ zap9FS7g=7f@%ZVZ96Pm$2?LOz3IS22gizAUtPC5gTZCAurN}X|rmIWr7axn;V%(&X zpk2bdQ1lEiDhLMd=l%gnSTI?R5(4e>qLk{7$&K{d%q}A=9A9gsW5vh~d998a9wsOj zDGXOhT5YmCCuue)kLOrfKn03Z$L2XWUS@rLt-P^(t3127aGy5$d65A@=p_6)BhM0+ zmzKD(bd4YksZ@svREW_TS<=DePIGl&c7}4HfH5v!$C%t@UKy8zv9nvFw%1@`?hqr@ zNguf*d~dmTRVoF6fH;Dv1am`Jo4xCHErhtYx84I-_g;hwG6kK)lQtoxut;rZi{0%T zlx8O=Opf~k8jG~_TI72vM%I#Hv4JEQDq(63(!D)mYoR>!1AnXnC>2T!4_DdQ+}Iz} zFrcln-L5k1ZK0L>^OXjfC~Ga!7XjyaPOk~)_Mx;Ltne`2A>`P)TpQ9>i@lXitWZoH zT_C8G2~`Maw9)<)-_67U5RYKx448ruvLv96(cD^D;mz+~;LySmj-NP=AizfPUtd{Y z`3s->wSO2)t*!CM#00Sn(fEY+fiW#YB8Kf}wgO2~19zHzJ@%aT* zAtu)Ysn6YF9&6~d6INF@+1Y9kAt}TmHJww_83~r@iD4#&UH!?vLTWoudE&Ew?7(c- zT0wxqDoI1Y_1Z2kUR&lX7q76Ab;z(NKxX+qhpMAf2MEO5@a|i+8dqNWKJS~D=BGaW z2^Nl?WMgxkZ+-hY_7ct5_%!Q{CfluDe*RNG&hf*CX}4OOd**3QjF*_3o_4x748~+2 zASjk7jExhPD+m=}yl~&*bXS2?y#4A0zW#-;^YCnyM^DTXL0y#M0t9WvDsNf5R&`* zpc@Bpw}PqeD6F-!!_sRP*}S$&Yy~ALk6AGigd@3|ng( zJoEMEh=t(C9y>u?4oP*6HWqC(VP%M*P;hY%R`_hDe&;1UaAmyv*7mf0J$85y6dKrG z-{qN?&oePT%KP7YKjmtb42_kBs4|3#V+Ya@2yNKh+NB&xN>PB-xnp>Q?dhY0vHM-f z)^(Md+>JA>1pKJ43$SdjxXO+Cw9(XS4Mr1TOtDfy;MTQHtBtaj>B({CW@jlDVkc(T#%r&%n|2D=-PvPj zyUxOaMaG7w5i;uKZHewtPJ#?9#0p{orVWi2R3rcFcjIkx|IaE=xVr{ODW_w-_qqqL z{-by)pq=|&!54Fts#IkFn>A{eu48mgc;J9v81-Ng+`b$eEr2K>8Xm&zIgk|!@k26M zl2WNcr`RA#Is^8n!NTXeFje+W$|U->bwKButdkHG%Kbbb!5tZ~?nWta+NreNX6xn} zMg&aWH&0M0xJ(@3i>dwME^+QSPqw`&;0+LG5N`6-Znrq|%0*s&`VDR!+(Tr7$wQ0f z-DGR-U;KmLT#S~k^TE*xMr4TYg>~A2a$)N%`Pxvg*H~FuWp!(Voh%^}5LtNe=pu*a z<_N=x+!$wC=w4yr*o#K3$;$FNwcRG62oO?J>uA=}1RDtECPz6sGeN9E&svIJiEQs` zdCR->CtmBvgNT4F0k5yG@YMN>yt1*zdcOY?)3*bw!C!cnR|myT5Oe6E_mW7(w_kjT zufFsmA}G+lv4m~Z_{k4F!ABo|l#rBjXJ4gYQWg%(y898~>YgNoagoyaIEA4qDh&2T zQ3xvuq+ok(li&NDFEAEaKJdU{3Q-Y*aG+Hf$5DBik+Fjemgto@OP>WLq-A+|mDPN;0ny+E?DOC=161-^06jepa$Ls%{_``}524;>_kW2Dh!sixCv^4g29uzquy zk3MvS(czd}J20GOIj8_tE~Dbu=V^5@jOlB3Jh1ArJN@dZ{?E`&xHkq&s(JSLH`#5a zeDcZTEFPYPz>UvD91xZYNF{wKvms3~_G)!z3QqTk^&m$vuvXZfyRy3$NTpnUl@R?n z1}WX=-(__@@Du)d>9jk90`gpwr^W#?ZPDiDyDhXOgjn9dW}YXk0ePc z4>`RR2CNRy001BWNklLX)f|9$eV0Lv=Ny;kUZ0P8=Vm%$4d z2g0R948@S@M1gV?vU2%4+8Uy{c}(}jV(FbEU2~VyKIGO_NvNR`>@}daM`SIwJoE!6 zz6Vi6d1#1MtKA#z4Ne04uAM=~s{h|^$4dPUHc6Hn+RZjmDRoKRcCemf09k zBYV4l7xM`#QBsjB$u{u)wKcwS{vt20uit$=?IvOW&_QjQ$KUgQ9z6XdSC`K5l|Our zwcQ3`lu#3rBcr2y>Z2bcC{}pkweJ(|ZF6#Jn#gEW5MqSISV<5TD2xmdkBlIr7%N<9 zZ8xq<0coqv@BZ^I(AZw%Gf$jiVrZD$xTO1B*j`M9!TrC+m>$E5>K2PTwj|F}wsv;8 zc59Q_qenUY{>KT5CGuvSGym>utX{mrSh37lxl9}egvFTZ{5*xx5wv$H4r<_fsb^k8 zWQ_~O)R|`W`Yo=$b{^GC8HytU@MAqergNI@CWUH+xd%^Coj-sS65UChvoy=OcK#}F zym^@i7N$8gH-WYi@*JIIWY$s~8YU{1L501*P$#bUUuH1QWWVsfe<127&RcD{er=ua zox8@7*>RqH;z7bnk<{d%1W~bw3}bgMS&IbPorHEfB_0~`djQZtFTcGD0(L3-J~h4v z8v14cr4&ZWUS=E83D%WT-iYCVm*_H31{#evp>z)5Mk{4P1xRK0trbSQQ6t^JUjfoe z0wpj>PTom~q8Js1M8zV7xI>m^v>SC)rAk;RV@$^EfpH!>I?uV~oASoh<@<}3awSVU zn@OibW-??HQm9mjeC}78CbVjG0^q>RG&3_Z6yvC8tT0BS^Bga0WOug|nL&vN z1}f)`7wa3FAf5hzf!TRUKz82)OaqC9HE1@v8_xMfNn;@jP{S2;tx3MOhY|vQKqgBd zh)ZSSq8n8f3x!^YUw@)70Is?KWFR8i-o|eA+;2jVCJ9;6Ay5ILn^h#(H$v;4W392| zX-3*kC{~6DD#M6E0U^4+%0Y{~*gvar$MYO3HoEVdX;{0lMtxc<}gP=BB3*N@4uw z)*X5XAuyR{ZDpIal}&UfA(jDwfLflgmZuOZjxWrxI6c;LRSjxtl6KJK_KVeB*1+asKRE zeB#&v?wc9|VbNCiTk5P!Za2o|YnxsJf#}6EIFWRRy}dnd?9>pY68FFVF@`250K@e& zXL;k9mlzL2MoKYCNkpiq%uZ98p6F#ebrZaMIc1(5>yGS&vDE8LZeG60*0oy{^PIR4 zdWWNXz7wNqcUr{5Lo7aYnxUB~$FAi$#<&=V+U71#J@Yandb8h#t0g< zJ+59|VrzHDZHO`;HHIjRIep>?^D~oM}qq)EchfBfTo{F9&ND=$CG|N6Oq3c%;izQx7$ zZT{NNeTw%k&Y}Y?SPNN3V{4b|S8meW+M#SAikzk>_drKmO)E_)4Ocnz zzzHf-<5<&mmbn1lG|l;gr(dE`uk+dWy^l&5ks5*^4hCcxqN+{kALK0j+{7%2&C^Ow5T{ngn_&FtVLOc5{g!% zgEfND(JHbVEb75bcQgyKcb|4!B6{%(J#Z=rf&itIheLk-v#!zzYuT$eD1??c4A|J* z!Dxe20ovCLxaGsTF`?)R-aVr5L~Wj=nB1UZ1xg{KkT8hI)2=W*Lj)lzE--)OAY<|O zSh;q2{NRy;pN``2&4MZ*rSwWSNs=VAYc+({96Y$d%*+gh&}pvRSMHcA?OdGBz^fq8My95LWt8r$Cz=@{C|ShOz=>2F4xT`%n*#K)eR4%lo$WKd{+;MT)t% zBi{p9KTO8zv*BZp=7_(M!0C-%Bjc>au6QBJ9fa+$ z&&|aCenMj{S(aid@O?;%9rL1R4SBS2Yf zXmZ+}y(YUWJB*aeM4>!-bA992YZos3#HUBg;(=03pf#zFgOCh>tK71^waMkHS7|mI zK95Jx$uojNfzv0CGB-Wt7IUq;iYe!g!kzY)uB@`PzC$c5kyI!FwLD`jOHsv``xg&z zU}A_sIW1Lx!s@SEm%YMw^$AFWfTwOQ^G{xRk+qJ!Be>5@ej9cQfRvMwU{+cZ3Ck_Z zwIA?yPRaSlzw_JZOmJg&nba@nixWrq#sB6nb8Yh$zxcoX@}N48=T}zv3;*Zm_)9T;GJ}xXq7-Uy_r6e2c8~n?E@_UTgoDZKqNTC?`s&nH;E!szy7?&Mo z2gcK;_b9o%ghr!*jAJ$%4Ju=!9C`c!3Y9X{8$A1^r)Vy3GFyr$gc6}Fwh%FKc%HCa zbh&A|*U2(KkP3%ege7S-*|@RF_R0n}Ng0yTn-WA1RHR0e>74TTFh}k`L2eU} zN^6Z1g3VZ!Li>02k5K--AOUppb)4aBnTs1_+_JV_>BLB_x$niQUb0p1$%nANjyGE8J zC?QF$p=~s(7<2OEu^zb6S>|(qq>J?wl6tMpl`E_4uJ2G#5c^=oU7fR@r>J7Y$s-HQ zj1D0K+hgKfok-VFXR$&#&@2T?g=|W}|NZ^5{K4hR?+Wg_AHsc+;UgnuJ~Uq8Jz>UV z(jqKMl2CHXWPEwW@_WlE7ybx3R+DgPW69s1PSH_Rqx`ME`8N>7kiYs@{}wiDzk?Oq zw(u)oeugH3|MAcK3~^MzgrOflJ2onif_9SeZ+`D?jsqqR%O03m>1n9-h z#`el`!D--(zfZy-Ak8wCZ>~@n9U%@v8cEKhAA5|6LknOf=U;w}YpJ2=Zji?bNzs+6#Xb~7QxGCVp$SSY zw@E=ts>MRjd||umkZ~-cw7%F`dTpE!8+A~%;fMa7&CM?$Bu3|?jRp$A{QNvqlT(B$B+D`%{^|p5eJ)*nuSUJr zWOizS(UBPf757g8-kf8FaQ&)TMmSnmU=>`m`pu0BIqx zag3@8m$6l^xpxd>R1hLn=(Jz^_GOLEQjE?>+8LMLzCsi|`}kuYc>iw|E7b`h`3Ug4 z+*T1{xwCh@7kM=Zy1FUm1MGtx$i(VuT#&LC-iXnz?APl&jOf;%*xre-dxG|5#s#cs znLKN6-CQv{w^o^*7(agV>djwVe(kN3r*$eGt&UR;0&<-Ly2o58EX{U@x0hGBzH*a5 zYob7r8A~(Eh>HatICY%ascFX)h&Xp@`)b|<`13@GOS_*dZ4qI8y zP<5CS3p0$5mYir;^sj{web&zki;?ccFO=YFr@^m0`x0-}_x{NH?=!=apP3GMVj^H# zY6|HNGRv@4MdDMj4@t>iJft}>J;I+kQ}e@w>@J86H>N|nDi5PM{@^L1V$6U2*M5mh zOG}8bfXzDZJV67${^f6T^xzDC{wF`_JLUX%$6B;Cy!`T6p8n1mK68436SJc}IMTZO zAYsvdG-y4zGX7rC+9kq=N+GaZxOj3N56h?=bJaiB(6go+J`B2t&^+)c{?lju$Y;3W&v4u%96riLpFZSawV=bxH zXkg~xEOSQ=5|s+%dENs$LbaqseoZ*A&F6 z&m{F_{b-Z3xpIrqVni_xe1g6kJ@;f@#(|t}rihCVbAZ6%P{*r zEsTL6NU0WG70~wP9<%c!{W5ucJcF=3jpAVNsueCv&1M;B?V}+Af;dJc9kNb`v{^@$ zhJ62@VRCMYxtU?su3Qd}9Y6lFQCxU3x90hFqah`j*||9;#>a_*0An;+nmNa!56`v2 zvb(!StC=t}xzHaA8aptp5FRY&pmS6;hVc*r{X>lI2U)EE=Onzt=3WNlr-xms@4eSO zfb}E7VBMu8+2}hzC+~QM`d}j(8|9#Y>z9{k8cq4oB0`0rGpEVv0u;aKNNdT3#fFNY zQh-JRttP}88%8LZB2|b`0T$^_VAhf~n{2PFkT*M&qr&vni*JA3S~>dAdmqgz)#`64 zFUaf*;Cso714YqdA9TVvN9y1GiU(WV%;;(`HM4U zK2jExqm;~a$a761B_akTwBL{!CrgI&t#=U=g0qWTLnK3_idb7;=fC^! z|0g!Kmw^nM*}MMl#=B(tC|z3o*r7vEh{=+U3v;xhU%O-lI>~75*4WwDrrm0xfT*W8>Zkf=T9afs zD8=mDEK_rnL~(@4oj~8|_Y~3!5@Yy-XTMK#XOExx&_k5V5jhrhDzj|F`yxah_(bb*&=eua-{aIUt#0Dg13$+ym2Vtj0bkA3V(3d2L>R+DK> zqt;-j(`2$Z>*Y{h!0sbZl2)6WOUuj+4-o`mzjWC+r=(8>c84EnT*A8_T?)UBbz>uh zQiQ&yYVc4hun29b*V^bjrC7FL@{>;%=uX;K4_Z^Odi^r1v&9qp5=jKh|nApEp{Y+1XjfM#l-m z*hNQV8F`*zu`cJ!S~j<~>9jNEW)~T*P7|oOm!0M`SKcIHazvJ*hC-MKAk6Qu8Q6CL zRf7Gk7$bMw+4DAizrj6lbq`?uC^1;?a1r)cE9G_^VfQ&__On2@75qts8XaXpNNy}G zQOk15M-RI!8tt7<{`4lS={D>>jhidfn+=8n2t`20X6&XNWTD8z$B#2VGlLbDJj=ma zlpi$-1FI{WTwU5AO)`d}03ifPo>Ma^4QrU6oMdrooJu*wO6_VxgzX(_yILYQ=8z~6 z5J?eM=m^&FQv?RPG^eu{HYz%QLB@`+Kyq-Y|5c5V=w?$X|8%4FBo<&xgtNEA0) zId?6Z?B1iErpWRfOUmZ@GP&+1YuP{Yu-(WlfBhePjxYb6Ut+viB9xM>)#M-k_7`Y2 z>U{R`_fS@$6L^CcusdL4Y~EvNj1_3_Tyuf6nw9l+*6MXW^`HJ6W79Ky_36(uzPP~T zefMFp+<4_pF28!7Vy+n~#sq;P7naJ*1eKXdw9yD_92n}>w&aGS*`~R@M`N!}t~1w` zKsOrGn-YvA*M?4-6UH$I7G@Zqot13$XpK>CD%?7!A4QrB|-7Q)}^O{?tdA zoE;~(hRnK-ytmI?CY6@i#f4r_sPOlgv6`)|9rm_sJTg1sTzdfn-RyqFk+Sckm0tYb zW9R*$(XSg}6!sV$JFswC3|l)ppfjq~3WZ2AT#8w~wT3b7stYCeSD<6%++9@C_=bS!aO6WL@*gcY;QY(i#s21 z5VxPy2gZYU2x7=_ZbI|JhqByz{eSnmSN8n>$3<1|;v#&9Ro>qHurm0)lmw&096o%6 z@YXiF=PrQS$G$l_{vZO;=uhZGtp}u;HtsB>9 zZf(-8*DH-${c^k28b(T8hzo!J#?__2`P$28sO|3dbO+8F-p{461MbHDzq=E#J2g73 zL(c@&i^38;-M+9Ow9oVD^%}~4&zvy`tr1!yjYb)R!Xk2wN)p7*4#~n&rEZ4W`;r`ataEe93$kb zVY)1c6#`^HASFd>$O8D)=g)FJ@u|(i_bK{UY~at&3jXaQMgH7K$^nsJv?1|bZPJ1= zzK^a~DdRTkGMY6}B@7?Q_tIPbeFFr+7 ztbkB_{oCK=TQ9!M&p&j8W5ZQ)oJ-8=yl1~Rxp$W4Iog-z3Zvb9l4k7f?y{aFJo2ee zGI``a-gx~S+nt0*Km2~;N|E~N8n1r)dqkO`?BgPY6{up7$;Eks$T7ig;op(-f1Y*jzi2R&H3| z*yD?T_yS{rDMrS}onujVe|}eD-TSBR7g<{mBHYiQ zFH0QjudMRGsUtk_@Ns7ruo;2SY~EVox$m4|WO$frwTdvtfi&a6 zflyp}>k2l>87dcCn`ZaV_bgSHDeHc@@@+lPwB^UG7J?ukjKV%J+Lt6@+1S|wn^P%8 z6bk{PLlssx>eTi+$R5DmzhL$@>d#_H?XreQX3PXY@WNm$odcDno=|x6HrjaW0 zN&Al3b7mDuo@eMzo3N~4I&u#%=3SU8!8G~8Fd*y}5{8CYJbWM3-8Q>#UZk_P=hJ!|pt0V*t}$q99CNQExsqho zkmV^^mXW0yS*Jr|Z>4B>LQb$ioE&1X;K2n>&&~FP!qON*A?PF>&RtpJtsBdj zJYz^e1mxDRlXvJ?!vjZ-a^%1qLP?jX?U^en1kzfr-(2I$(h4#+495|Hlr-{`jjV%K zg8OEsIXE#v6gl=nNP$p3jlGu$-u-wbfsn)iq2&u#uJhd14le7)6M0Vc&+!O;qvh2Uwn!4ue`zaix>F)fAx8O>f|^dTbT2y>;`Q##v13!$~^GVUZ~xxQnCo6X*3%w zZS639>Lf?s^Dt?v!`ZXvnLT=#vDry%u6gBK-(&6ERf<_gAyfoPpdv|idW_QOXwN92 zvkaZ4q|Fwsof^%ZU6OVSYc)zbM%pJVSRkb$^<$ zTGQ+#{QK{`&gyQRk34vUkwS#9)-h#!EjG5>l!iwbpPfd8p^Kt08k1{Go;%Q%yX)bA zmH+R#`#sio-#NhLq64s&T0P-gFJGisF7mODzn8ced-;?lZ*+L-E6*}MHq1khorCbz z$^ZZ$07*naRCb4tE*maDNkMaWj~Bo7Jz@lvg2JNrhn5aLv&QZNT0NR;U};lHe;k5(r)LhtnGR&*Fc*7fcvn^W_4?`tiSFi&zuge%l`Sl zgOHH6o9OyBHcydCA(Y_w!zT%p;mX@@t7076N_F_K#?si`p|Q71mgmSYAdCYxHrLtN zZZI`*kg<_jLRA=eJo^2bI#1A@7EvXJnUbr)xhq5UE~9z|GqDud7T6-z*039`eT@U} za@M)`x(BfS_`a0dFSXi_zVd;=xV`(PbSGBJdXQ16Fn{PMQ$|vI^8&TiHBzhHc9^@I zo!n@0t6j#5b$XTzL#{LO)KT17>U0EQu}HBp`eduodS>bE%f)7`PN`Jd9V(Z9H4eh| zP_>GbiknxjbN1y|snu%aek@=HL6UV9HZBa(pH{=zjXH$%qIEk^AotF_+!fF|rpo%w z6$`@nF_m!zwNe{wX0V+UnhCm{5rstx#i66kdTaUluYUU{uD*E2J~2JTV~Y!(y)j<5 zVrev*yz%xDSJt+`SYl%!H)Ptdi=|=V_`!pmSX@LZMVjXQAVUk%T5jH4=kn4m$Z|?T zq7XE)l=VDC7GjPZnB~Cuggazv?LIrDyg0XCcVfG{neWmI6uj0<_@}R3hAi=wFFw`6 zzy~Yve?2nBXNw79yH3j)>AarI1z`5sMSp4i^_944pO?~rUp?Odo4pg)zXN3bhoryD zJFf0E;LrUZ|B!F~;WHc?9_CLx@GvTfXzISzJ=cyI>Vg#GYe%{)r?Qs4dX2Z&w~6Ks z^61BYoVZZp+^cU=Yo(lg@HBCuNd3kdZ+-Jcf+VF-hzL|bh(H7|JU2yDDIr5Q;t@cW zq_pY{TD2NklK8%8G1ydvv6kF8r=U@i(eY8{=4Yr3ReB7uPBTpAfuXjJfyz=|ySBp9 z=dN;kY=qNeqX?}@+ilWzn;WaUexItE??61-5NUAe`{}Rlg+htQmu(gCE`Mnz1>~5H}{yHT4Zu!-f6Ck z=vAOuFSgIp4q2l~Tn-U)B|jE4Kjiaur{C-H*fzIUz&6>_7vFVEFU38ubx(u!$N43C za-nzmJ+d#<67n`+CHh*0+aI`ID5w9vSY-0RBGJYzu3f&#cC*dMfk^@>>Bb;AS53E` zLP8LbXBv|xexY$_Fs@WT+uYsx+ZWE98Q;8li{X(m9)AB5Bh~SVKb0lvZ!{YXhKGmQ zTw7)J=1sKLJo3b2j7>};eCo6qa0B*o+=ZZA8A63ozvo$qe(Lf-(CGe9$G$tU8(RST zV<9XSt24;mC`73cWb{xgY5vXUzxIvd>)&{q$0kO^dygL?3Lt4Fy-`7Zx51USud=yU zLr6iv7^HQ~)gGEXV>moL%L7M_62%4DNz$vU5Ksvou3TMq0j4rQNmqTdl5~)9 z#F5EKrbmWBT8z~w6}aplDg9V*KyTrRO5;DKO>6n}?|hG%>AQoh3K4nEe=()_FAr5X z8nw_#f(Rvn>6iWv4iCDxZEz^9{OI5}Zl-LTyMXy021A*5S<&zE=HFf2Ci(UY{Ou1t z!Auy^&QrfRchi3Q;%w*6>n4OdaNEo>F0HPU4~_HWPkowVb&Q>jbuOR3#Ki0j(}!mf z(s2Ei*XV3)F&01&I6a>giqgzDmH8=DsnDy~K?t&D3!S9sJoik9^qX}*!M*!cD8>v8 zR~V`e5ycVO^udp}IlAlbh1}bX4*%}CGZgYRPtML$0FtDG$uc&#>g;xM4j-FhXmZ3e zb-81sb-xCuUqWNC-SAN34h>G+?#FYvMtjlu(#jsszkY?2M-KAD6Q@v-B1;|c*jd@* zThF}AWwpJxc`xde!E_8 zkQvM9_?&Ehz3X zvhP-u^&>b?l1inFZD)j~0&BaMDOVQme73hBLW#}p1vlISSbrQ}11AM@w`WJE^pHa@ zOv=8KCBEG>()%++e|L)_hNow_FO0c-YlB<6H7fHHR7w@!G9d_j?^hrdHn3z8I$1{A zXb~bP4~-0)Ect8aUU=c;wTl<13=eVNBM99CG&Euy}Q79JabW$H~X^6rQDJ847*139h8JXr3g#sn0XBlgG z0&&dZ^fXhWBN$xqYZwK+JG*SAfAz){&em%^?Hyr2kf!{V#VUVk zvB-pIleaUJbecZdCjgy3teeZ^dt$peTiv{|8!3Ek^)Bo89oPN+Cv4@FR}U_Zv%R&& za{X?5g}5uoI#bK};+2~`_JQ{x+X<<3g7w^J4`PhZ7V6vYrPgHHa%p9qJrVI!KmR8g znHZ;2ud{i5nXN{fN8Wosg^?o3#x56Lc!em-h~p3;719V~DPrdI5lUm@t`E@o&`Tjn z8x6vO3xv#bKep;dF!*X7sU%S$q+Bjhsg#gXk|&89)BC!go_9sKGUCK&o<4htwcR=o zj|?$gt&nSjusM5sZI;%X49`z8y)ffIKzAIf-Iwt1>;1yM1JHQUzjqCK=13b4ytHAr zq50bP-lkG2^D{s3B!%G;xwd3RlQ!FY@oUc!R0}-$(Z|suaJ}+gqb7vpJKuVdfB(B* z$96hPx#EJV{7v zyM)Ce6iWzU8JikrZfclI=PxD)4;}f*xLEv77=&8Lh-Rb3^71l6)o~^!4ibh1zXtZt zix$W`Es}bjq0utIp{i%D>>Zz}ABw#)-QsSa`K2sI2S`z(QY;b$(cRDY*4z`l-y0GB zG1p+d+a27^pStUuT-<#mFT{R_p^%^=N~7bPSUA9Bu2_5f2Ddg>XeV{rjUAfpJ+i!m z#Skb-7)n&^n(1|tvAep`JoD7ohi|^|dQ=VtBNJmh`oZ_r$7d#gZKzawW@KcTiMd%0 zojk^g2Oi{!4?fAM<0n|Xc7rp|Jx6nQ3xq@?-RaS%ZD)B#RH_h0aUU)TpM!G;F|-)` zfxD88ZkO8adJli1cgffwR1jIK|K8cBpMLD=fA)K&56q17p$8Tj9a0FJp_7zOt-K`6*|hExI-4zgDR5fgM}X)eJ&4g+3XU*?~jd!x^E1|j7v z<$pg};V;iCMs16{(*dJE2VIX~pS==&gKC%6GTlIK&#qq2p>ZcY`rWb(Z$Bdd#{&2V zpFO}&9uEoCJ8xxy{UZe+|Ki*=p5EMcnLv5w%o(a|-C!-4w_AalX;g{y2`zD`9-f*_!)`X8DZqcSmt3?r8m@5`p8a&<0o zsle#u2qTjtgmD2YWKYYef`CG?NU1VJp;UDDz1KRKt~Sa#&||Dc8%wHFuH0PbrMIs$ z90r`89H9~gNNs8Cwz+a^8xu$tj?586A$gv=2!J$oxpbbP?F!g!p1Xg~yzZ(ee$SnV zI@jo2lV^q(-&kU0YnxAf=m8GiH|q+r5ooRX?(?s3b9J3heDeL2tHWN>;GVr8ki2pB z0>A$|U*f}av;4%-S*oGCe#QelGazv90>0jeuorT=f4Oo*x@WJXybXXmChq3?CQ3>s zs>MFI6);gL&}wzKwXy>u*bfry9mZ$=z+z0-B#>dU6q7kUR-QYv4#uFfl&sYtukT@! z1Y@CGDRAWQtgh8|)XLh%pDR`>e`j=Jnq22xU%Ek{N=(fhqFf$Ec5~Z|!I~UvHA$yQ zb8nYowM1}a%xkXfJ0})*{CbzuyI%v1H#j(k(>>(ESd>l?S^YN6%D~=qkG#6aVEstH z^3>s4VfR09_AV~2!O_{Bm@FYdDWYPDW3#gq*VcLc?OSY(#SB%4NHa~PRA#a~#%#HQ z4F!dAgh&e{os{dBF8&W|Z@zJAHdf?vh;(EmBQtNw-25XDHjn{IK_DeTq$pQQ42=vS zh2_;VFOww+@BhTd7@IzT(3afmP{K-?!f@3UBX{d=u4rQ;juHCh4Zz*xL(w0k0iWDVM6h$!;BtU`*85m5!#GcSO z-S~zR{NcPe^vr;yKZ=^SibY_$F}M5PH$4C6|AZ$$|A+rRxOZxVx8Amgp-SL8$C8Zt z;yTwZ&a=FsvzJ8{q9J8IATD8jK_uj+E&f|ab|#j)MC zjhnS{{K#8|=AS=V- zPeytTUayyqZd$oyt&?%)+9Lbzy_-XK-&p`Ku#F~HFI{ENp&g8m)-a6@=U;k_NC-rr zKv<0Gg_3AMP+`rIliF$g6I%l7 z3oTt&d*6W%gD%KPuT4VQOVTU)2PcZMKRSd)TNMXi^^q54_u z(iZn7S>Uxo!zO%Ks#JoY7&Z#{R17+xjb(0bmGNqc+E9u1daHLW)XEXo!uiWf96hoV z5eOf4+UIV2;jq1-tXtY`4c24`Tu(37tr)@>gwg0uo2#Z`oe*uHZwBegN45Z>@(iabC8dD5n}zD8}VO6lNa!CV<`p1gj; zjr}H|iXMyKi!QO<{5=2nx`NS@LPFNKaUvR2n%sH{-vU@awTGgoVHoI{`N{NMgI7s! za`V=HoJ9n%eQE-Q;(OQ5aWQQZgb`;J=ZWNHerDGmc1%v8;&4P(BeQgF<)1ITapoT# zo){&vn$?XCkqgYi$+PR#Qe|!O@F8Rn5lVpzEkPI%M=_;JnRmaxMjQqF@Vn3Otw-csEU&-KY|?JH9wq3ChmI}QAHoTQ40h=} z`NYf5JoCV3e&_dj+w>@p9KVgxYD}JK^0sE}+6q_B&9l0;L7Jt-1nzZdLO>!Nm(vc$ zSe`h3jEUL^X_{knhS7#Fj0mHUb|>M~$v2?UX1L2P2}i@_)U9E9e4Oo5Qv`t^O)`YA zgi%Zo7J-VJ9O*uV-$@9BB(j=znDhTU|0;7LDB=$SD$epx_LTYPs3pu(Or9aFU&lCM z``Kf~8nA2n4g_ddzqtA|L%p8{Nua!W1Q(}OGI~dUT9kCNy6)wE>oVK z;K3)~MO+>t)fyoz>q{%NlZ0*iX9z+;YiW(yQx|+*lky4lDo{j~GOc=p{Ompn!WV2tZ&&pckRJ+RvE9xQZM z&h%tC%PVc3dgVOvP?>i0*hn&(ot){>A(WJu+~LqTZeo=#G?q>bGFZX`DIt8+g;E1i3dOZA zq2BDUwo>Qd^f1EdJ{M{Yl}J#D6c?_pl4XWS^)vOlT?0K1%MEtX+n(F9`$ad9d~dBO zoSBBC(L}~&!g2-U6t&?phj&eg=Pxa)xw)&>3Z#&M-MVcXwb~dGMV9%#L*e`?Gs_Y- z*47vv9cJk848+ol9(#*?Zo*>y==EUN8EAX#)qpVu>zQ%q%etlH#zut!^32^5x!(d< zKixX5vI!T^2nAADwoQ-o4hdhMeVwIBgO0Vlc;z&g&%ePF zcizLk>FJ|ub1TQ@W-tEQq3zR*OF?sGg_47EyRn>{e*Lp2&Y$JzyWh?Bx4sP-mT)Hb z(@_whqJXem=80c?A48MFy!h-7c;>6$Z_B77Z*VkQko`KPGa9 zgX|$w66*vx@Wz!znqkOCe(A#uPfU5? zva?_{^H=7Pv0!4VhA^;t;VShj%M9n5pd1oNNfgC6DM&jB^>zyp283}$9ET_+$&(bT zbHcDhV`ZIaq|DHcEzI1pmz8RX^^0>Dt;w_0>$#-Y(}_YOCJOe#Q49fA^V;PlzIS1s z9iieKJ0{s$8$wu1v)$(6Vx2?^jvn4gT#3*|7n-x;dN9TLUobO3A!2uZhQ|68mjz6b z;9hU$eB-stET`~ukKW0_LwmfoC|p#q1wF9)Lb2z8^#n?g=nRR`Ef49y`7T`)s(T7A1 zL{SjET5pl+oSEqgh~(v0duP@TFs2ORKhw_q{EwDy_9FL`ZKI^4l4wr za1M6$q5gY3qi2itG*%PDF>z%WQ7RDxaB$x?zWdq>T)cR7=fsw+_fAgFyjH7?cASAu zCn*Z1J#cKbn=CKRF*-HM#L?|g?iMq5OY7~8_x66L*qeYWinxx=@ilbKONd@TtN+~$ zQn;IL7ZKbvll0bc3t;{9=(E}lWKbS(3W}RE2V$T`X3$l5J-8ABI~NgviXni}u^Nx& zhxp35msyTdW+ujX;gvId@tGfR$H>I9ca2Z|{l^aMiMLgzSidq)CrNQS&1j^bjHLV| z9m%P`{0nTBv-`2f5h_Bs98fqZQ8>n@xALwJf0%uD9AmBCV9U-O)W$}M$`!11eF>^` zJ#CgO*ejeU9AaG+QD46%21Zd==&)>&Y2#Zrg8i8M4`2S)H;#SkcYmLU_U+`6dyi2r zhlTAuXKikkxeJS|tv5)M1gpKxUl!A{k&cxtWwF`jt;Y^?V0s#iUm+Af&Jcx)C=Ls!)FxR-KVPWmQxjM1ZSHbILlu3k3b1U5JxB(&`KRQ^ri`z&otj&Sz`b0t$xEKJ+Kz|K-anT23P8B zp8V+h*tU08*hs7i*%gYN)ZJ(lA8>QW9GkfARF3C`VKsbXc(!-6z2Z>sP%QAE) zC07chXGKG$2%W;TQdZ7i!w8tzvyClBc2ORxQlDKQZ8dv(qprKKt2gsy=6=;Rzr4b? zUph%eYkp??D7(k2M1i8y%s9JHXE94TaC9f*Q?+izQ^B$ndYVmoXmXE8-T zW0q=OxUkIml{QEBZ0F(o@1RtzV1z^`^+ry{KN;PC9JOZ>Z! z{V96`!3Xa>%xERR3WHLDc9O7RV5riq=d#x+8vN%jw7?396c(XG4;Z=vc*!1nd2$r_zn6qJ5@xHkGP0~vo_YeSyr zKFn7tT1k^@S1vQTeTwb(9Dr(EFjs^3`I|6TgYIw!!ItYWaMoJC`n^7rTJ+V9q8L7f z^9A&8I->`-K-MjQ^;3KpBfupdu+Z)25}U%|#NhhR{TL=|kn?rIPfIFI$aD?E4p%Jhx{2mjvG@uMV3PVOv8o|sVT*M;MAQbik47~rxFXFvBT zq)plR=sTfY_76VW*V`+h*mvh~a-)lkCm(ms ze5NOw2K#qy<1PF45IRAoH9F4-gTRaPgz>v_?i^aEu402 zSj(!`9NDv*k!qD(`)C0rB~cWif+DHf*@5uYu1=?L0a?q6Gkolar`X6d%FdFiVRlN* zKR!Ifj(myCIE3@D0k&YSB)!-yaBxY$r#B)#dDU^UZ6Pc}Xu)P+HMxZa8L^OBUcRLH z#{4Y5e*X|Zdu%s<^ow_L*UJn1Z{J>IU8o;<2mYNW;O)~6n|ZyCb1Arh@i=0~P{7${ zmzS`;+%|zE3SvSPVT>Wsx)6oPP5Py8TwLToKC~B^>Ox=QgIqgVn-|YrX8&96Ky0nWXbl9YLeyf7Mr(rz6;T)w zMgdX<{^w4ib9tFnZ5Y`;#psSn;!@1Uz;`yaU*84aVg#pO8{PM+g) zpZOXGt7YDO`#y$iWpaFpwnNa&a#SrQ5DwF671}4!YjN=ZAbV>oTihI^>Vpa?eZ-0E zRcyH)I28hnbzHc3nXNMul!nT*vJOF{$aIQQj!+p!${{(L3s)D}w{O@3W9N&Nx=jc^ zLA?M`-QleG`H+J>sCYe=);Qw@@Xi?0rXz0+5yc@wmME75JGYFwYt4?xQggS?^oNY@ zeA{TV<}8FVV7;-<#f#_Id1x>D-+rtBS9(B=_$JI1+&DN7tOc!g7~|Kswur(Jg;}UK zU?C8N9;-`ic1yHxV6blXque@f0j$5x$6tKw;>#uhI;ut1JjHZMTESApadns*w$$t8L3h~^CBnmga`NRF7Feh}7NYVy#1C4XOYD5SjWOUft^j|&y)N-4qM*sjI07*naR8#l= z>F@r2`O(|<@y`42Ar1s4Nzh3~V{x6ia|>*2cyMLRpa|5zPjzd!)T}c(Hp;_C4pIvO z(mY3-9HkUt9D#&RyTi$oXK~t43L_ujDJ+SErB;U>Tc(*A8^bxsbqYA*I3|oDRMA`4 z6ZsAla=QWN z39(p2kmGa;*K3}PPir^8(Ksj#_-HELA*g{qe0$Du%;jC zni|dN3m3V(w!v!R7?N;!dYF+)1()Y6v=f%noG=jV-7`a}T)~(k)5_?+8_aF?#C5Z` zx*dbE09vMCVytn@Z?t*p&;c!qC&`5ATy$HPbVFg_ghK##KyCj*v}gpsitfdHFjpt`+^fU^b$JxI10OQ-&a2u;6nWmi^V(EPFA^p-;F}M~S z9JP2ePyO|k@?F~5uRRDHT4zLoHZgQI1Q)LviU~ zDF{hYjn1G{s*>wACqDBD!kGzX?t0KaLY;3?*=A{!Yu;|Db#+W^FMy>8(+Wo(;xez=#ks^^WKN=BMv25vqRoasV@NiKmxz4 zGk0#0`bL{9(`4FWoG7#z5^Wt#W4WAmkWtJdw;f{3aE&z2(7E>YH_9^}4#&Ckv$X0h zszKQMC~^S{Nr!5+!tO0w2!nt$&wS!^xkMb7P)hZJQa4SJZlD&#<~+Z+%oi_RMIZ?f z1VM%St1Uh}9Z*R+;g z;ZVrraOl4vCC>OHYk_~rI%AMQnW1=?TxT>p4%;h<_7Bkw3*Wu6!oFksi(+rZ<)t;w zZ!~$|fAM}s$0vO(K><(&fU&I1FEKe*qZ*f>t+{gQEJ-J0s0+rd>aVH_jgzx}7$8(c zaYPiw1Vt5&D=K@O^vr6g0`ywK(kmCqcXX(ZjSz$}f$E^0^qEQ)q*AFofvrKn+xP8aq*lR7ixdT*Gk%@gGL};4yZfBA0~n#}^rUz1v4vt?hJhatj(t&ae#eyh=#kB@$n)L<`u*J~Q4U8;JUn`X3OpeJDY@Q*UA#2si+jZhfh>AkC&D1c@T{hP)UYP5&>u02r z&2n`J6$EUotT4TAH}|~voe-7^a3$EB)XsI=h&S26cNsV9!Lw(sHf@TF8l%m!66g9+ ziVlGc)SEL{w=9IW0M<{_apm+CB0Ee}3dyVhlhJH7Fb=AfxG0b9$2Eu_S4GA3j24?H z_x{Q-@CL)E1bNC>b%g5nLw}iUtlwFR17x7kg=p8`7=?{X6*V+Gtc0_olQ*5k;4H){ zCL<#h-5ZCz>5mq8RZ$@;80aVzqUQ>8vKS_H=JR}z#G*)b?#3__x&v3#%Y;!J>a_ib zKlsv@-u~G?{$ugT(S5w!Xq&RrEUU7^tu zD48=9H3`!W#unH*kYuSP%^)5M2&7`H>;`^s(Zl<$sAsa;Qi@}SOErvvPTs;guThcG zhim@Dsf&E@;5LRL#pU$|-#K%EyB~d!gLmBKh0xA)XLP?-%5xekYwXxDO&|nGv(42D z^JptEXdKa|Q`PI74BBXHAgM%UD#d5kEg}}hl2a6HOrB*(p)lIAa&eyK!Wu!OkO=ZL z^O;d)?-LVslnkfRW6v=hE{VwMZbO^)AjfOkIe7NT;5cA9cIV($DLfBluGx%2M* zy!CB&5d?~jxp{v0wdZ){yDzgfaJ+TTR>o>2g20;~28Dy#8rG5yao|wh_kSQrvx*ICm16IX~T@3cM2w9}i!)tHMvTe&GQxi4PwN_8(CMXKV1(X9zs0{TqrIX~u zL5P)XVz9aZ%bFq_)|npLF9vR|zKGd)aHTV{EW_m~)*6yli@eju=EJB^GBaJH99gxz zbj{Yr%C4iOaDha$a<_fg4EKNFNhnnYuCs1f$;~oQ-u&pf1qZ>^Kyd9~y4>|%Cm(}T ze9gQGgC%bu6FNVQfNUWtovo6Gjt+VdNcUy=$B;|F_-ZIwz3I62(Ip&F|)>{tGG)C}EL8A{7LIVnr#EAj<2hZdJnUsuoiy6j%v)p3>@QhT;-&82Q8i z+n+w21*KHAU0?s#-}}Z#>(sxoo0qJuE?*l zKGakQtcBbO)-5a<&D~q4xog*sqRJwJLN6AofG7-ET3P4(g)0QwTi0#ja?GqDF`9|7 zF(yVw&_vuy-?%eDJJskQ+m$Q-V-YDwU}kgEkqd z@ppP#L{GfdNO<<19i7WbR9${jcZ2 zL8L-fJ1xHQ{TDcJ+hOiFbcoOY*iuzcYXU;E^jxqNDtJwp}lKfISpt%M52rdldds0~c6S#P$OoEjmN0%7vL08?5Y zc-tE!r2j0HuaoLl)<`LP0Be(T+IbLGZ)YqlZg9^X2M}>UtDRs|O(3L)C_*5FA`Su~ zXKA!rG#UxRC4mUNd125=(r2)Wd@yXUzCapk_@zLE0Z}DlV|9&Wb&Zk9Nvcsv zV4ahG6dsXMMXmbk?|tPzeDXujedEi#@A0?rz+3JhFqXX8Mz>PxSJzm-xI(AlJLJ4z z*kOv*iYZvE4Xc^YJM5}edGO#KDy0%hp7}L_HK@>+0jGJ!>!;6>Y_upV)g${2kU2*X zhD?l)p#n*g<)}as#UW7~4fHY=(`F$C7GZ!k87nI*T$#VZT&Kw!orE?TiNjd9O(p#5 zmXIOcBpuiQ*rIkxDIclxQUm_sDY#&(K(ky^}dW@`=|k^YZHnBTs8PQXINQSAW(wZP!*?hRu&fN zbP_BUC6!~i0YekRy#KwA^XhZ2vNpHEfuSqN0ugh&WJ_t#(L z^85n-;J^7*M#f59JMjj8{U5$e{o*`#Zy)9E{W}>Rs*(p1p$f^glm+(jEL2B3I@(f> zLWJ{dtrOkV+~x=pTdd=)QN8m~6yo-t2CK(G7Fa_HuFS8{>7?x3vlRkpuGa~**%Zwp zgbyekiY05AVPn0`^hAg>;G8TNEP9`y?G0%KKovJR`JKi&4bEV!CChS*aYSiK-fm*k z6j$gecWx{n7H|6+-0x?e(ijV!MX*o{!Sc+5A49DDM_P&lrd5U)M{HFtp`(N)AEAaF9kI{v7>y7pveqked7e5e#iZ>(0r5vYLa zu`xE*HblLiK44n<$!aC|<8Wd+(IP^plI=(CB|3c9PxSj5`h}~#Y@FULpty5L@3=Ch zkkZsJS6@EO+R_Sp_w99YR7M~^V4OK|>D-y$`qIZg@qyO4)BN(!KFsZh_aZV)+RDf` zTC}dNur|9&*3Nykigo11VH{Wru`+@U?YOElhN6JSZa>0UZG_xtoYk0I6T}fg5TdQ& z{jrl?dZ7-LaN5k(PE9QVp`U9pn%50?B6&~DFfm12`+9^11hk=%71%>jCGT|dH z`YXx7Ld-W=Q-~s=$Av7X8LwV~BfCJB90;#1>fMYD*c9Xjq$@GH-9g71r5wU_sKkaK zfLzO>Gq0aoZLA@4dGF=B>(neyFxvD=fF+if*6Nf3_+S3Zk1)Oe02uATj&r?oaET?) z5?1CH8Lw7}RDkJZEY2;`=(GuyXH9ShB|WQ{XPQQ$~b(Xf0{8t}&A8E72(_jBCqj3qB<*cu5P_NgqA%xY4%5aHx zt3kCC5tU-f)gdw=$deQ>7}K}rJE6TupRNwCFCw=BPRy?I-PvVCtoXSH@8alz-2`Fm zHH;=>_R>|JdiGV``|gL?yL+5VFTTQ8{^Hvt%Lxx2+RMSM!$jo>r+kN?m4ZENq_zccySX_O(wv znCEhEp21R1lcgzPXwi9!PFkQdgf&FcFg0GK-nwAdn(b`+NX^a7UE%nH_dpaDt7~zi z=4um|x-p@>3#__^g&uSDCUg7^^K>`n0^bNJiVQIIqg;fy7_3_W>u)uwbsZ(#+OVYU zdFstobf$4a5yUl0NT5QLlw7;E$mR1_iG?H-vbbSew6(N!&QP_=#MmexFwQ62hjD}og5Gc1 z)znEL2vkTb@9@Ui6D(i5LZwtfgm8AF%}VjUWH_eCd1BWDWtPz077yXqauO&BSkUlK z&%g;=20BRL4Fx#sMOkj}{2SOWxa(CL{S`$aSs@G+Sz6+$^En@W*!0h{B2mF=&?)3~ zNEbl26oC-F-&a8p7=|NDFqrVrb)gjo2jeuEaoEhDokmGXs6uj^VXSA8G8`4)u}6+` z&tngGZIUp(8NSbcIMQa5`pOEUwPAFYqm5%@eWQ5jdj=>_zM{xl%X%vztd$s_7(*%H zE&4a)F<$5P+H}qN^mZjAQb-zU%5�b8T&vkACE5*)=oH=RfsjI?XmCp~6~2tL@`8 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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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%0DI_T5v-ZP|iT?5q!61-RNkom%to4`UPZVOR5 zEdgqNT7~w-bEkrKL2&;#z8_l)A3o*OC}9uG=MqC2v#zAnm?3$fGxt^I4imQ7)k_ZJ zPr{*qb)q?YTzOOq5$Ub?)cy%QqeWj`_1#i zGkkB3Ep3(6%lbJ{dTXm*3M4fs#k5$d!*)NG8ZYDx{6u891mp+InU{2lnElXg1hHZM zf@5(lrd`;I^6Mo=qR6^I3{>VML3j}b1z2}`7EoR^_gfm3Z|wc5|1Oh#V(tQ6s~}p| z!anQbOLbn*w~EH(nj33sdiqX0+yAc^0$ee&Q|31iKj@=EDHkpow(!Lc;N78DUjVkxW>g@Nu=MuXdNigKM_zK5+lIM1v%CjJ|dpKjkkL z$c6K81hgO_oYLn0Z|0n8b67~IWiF}f$>SGEz<$VatRZkj#&E|Oy4ns+%Px@jdp}Z> z^WH4paUkAjk2TK+|9&8q@Nl}wCp)DI`2?h)(|hx1d|*U)lwCX2 zjE1nS?8m4a&l74>E8y+oy zVC-l<5U;hW%VO2d55StFO52bi!_R;)bxRh>qDxM-MNo@_NfgNq>}PeIe!b-vN)fdx zLXCohY$i!qzIbwML_$#NW3rK8M@~2OR7}~ql2S_&GZ5cK3T5+czJMoGo6--1Piw9b1#~I4Y3VwRj zQ+bC>$TR9~ysW<)&WOzJlqCywuh3}#7pE6jz_H7G>U2gbn& z3|s(zEYe!@?k?Ww7D@9I10Z{U$d0Jzwl zcptJEZ57Bbetf88E&+m~u!WobMfT0(NIoS=wxJrS;kWB10oMMlp>3W3)^zN~?R!c|L0QI8#Wy;FHRQU-4xN90YeK~gZNdk4dSt$z>kQMPsE%4ey| zCI4FE)PlS9U(TYi!*0=1qCG?OmK}glJT_zF%RyS&^mup{ZBx8Bd6JNNBO^ zJ!N~Sn^^g^Y^cd-LBh-)h>DP@K;OymIEB{ynYv=@2>B7WW<(<%0A(=%_Ee&q%$AZl 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 AX8-^I diff --git a/images/brand/64.png b/images/brand/64.png index 0309ab76852582d2b5c7d4bc6e5806581f42d5ab..4fc9b10dc429bc97779bac5379f835dc8d822971 100644 GIT binary patch literal 12055 zcmV+yFX+&TP)uJ@VVD_UC<6{NG_fI~0ue<-1QkJoA_k0xBC#Thg@9ne9*`iQ#9$Or zQF$}6R&?d%y_c8YA7_1QpS|}zXYYO1x&V;8{kgn!SPFnNo`4_X6{c}T{8k*B#$jdxfFg<9uYy1K45IaYvHg`_dOZM)Sy63ve6hvv z1)yUy0P^?0*fb9UASvow`@mQCp^4`uNg&9uGcn1|&Nk+9SjOUl{-OWr@Hh0;_l(8q z{wNRKos+;6rV8ldy0Owz(}jF`W(JeRp&R{qi2rfmU!TJ;gp(Kmm5I1s5m_f-n#TRsj}B0%?E`vOzxB2#P=n*a3EfYETOrKoe*ICqM@{4K9Go;5xVgZi5G4 z1dM~{UdP6d+Yd3o?MrAqM0Kc|iV92owdyL5UC#5<>aVCa44|hpM4E zs0sQWIt5*Tu0n&*J!lk~f_{hI!w5`*sjxDv4V%CW*ah~3!{C*0BD@;TgA3v9a1~q+ zAA{TB3-ERLHar49hi4Ih5D^-ph8Q6X#0?2VqLBoIkE}zAkxHZUgRb+f=nat zP#6>iMMoK->`~sRLq)(kHo*Vn{;LcG6+edD1=7D>9j^O?D{Qg|tCDK{ym)H7&wDr6*;uGTJg8GHjVbnL{!cWyUB7MT6o-VNo_w8Yq`2<5Ub)hw4L3rj}5@qxMs0 zWMyP6Wy582WNT#4$d1qunl{acmP#w5ouJ*Jy_Zv#bCKi7ZIf$}8d zZdVy&)LYdbX%I9R8VMQ|8r>Q*nyQ)sn)#Z|n)kKvS`4iu ztvy=3T65Yu+7a4Yv^%sXb>ww?bn(=Yu(!=O6^iuTp>)p_Y^{w=i z^lS773}6Fm1Fpe-gF!>Ip{*g$u-szvGhed;vo5pW&GpS$<~8QGEXWp~7V9lKEnZq0SaK{6Sl+dwSOr*Z zvFf(^Xl-N7w{EeXveC4Ov)N}e%%C!Y7^RFWwrE>d+x51mZQt2h+X?JW*!^a2WS?Sx z)P8cQ&Qi|OhNWW;>JChYI)@QQx?`Nj^#uJBl~d&PK+RZLOLos~K(b5>qmrMN0})tOkySZ3_W zICNY@+|jrX%s^&6b2i>5eqa0y%Z;^%^_=a@u3%4b9605ii3Ep)@`TAmhs0fpQ%O!q zl}XcFH*PieWwLj2ZSq`7V9Mc?h17`D)-+sNT-qs~3@?S(ldh7UlRlVXkWrK|vf6I- z?$tAVKYn8-l({mqQ$Q8{O!WzMg`0(=S&msXS#Pt$vrpzo=kRj+a`kh!z=6$;c zwT88(J6|n-WB%w`m$h~4pmp)YIh_ z3ETV2tjiAU!0h1dxU-n=E9e!)6|Z;4?!H=SSy{V>ut&IOq{_dl zbFb#!9eY1iCsp6Bajj|Hr?hX|zPbJE{X++w546-O*Ot`2Kgd0Jx6Z4syT zu9enWavU5N9)I?I-1m1*_?_rJ$vD~agVqoG+9++s?NEDe`%Fht$4F;X=in*dQ{7$m zU2Q)a|9JSc+Uc4zvS-T963!N$T{xF_ZuWe}`RNOZ7sk3{yB}PPym+f8xTpV;-=!;; zJuhGEb?H5K#o@~7t9DmUU1MD9xNd#Dz0azz?I)|B+WM{g+Xrk0I&awC=o(x)cy`EX z=)z6+o0o6-+`4{y+3mqQ%kSJBju{@g%f35#FZJHb`&swrA8dGtepviS>QUumrN{L@ z>;2q1Vm)$Z)P1z?N$8UYW2~{~zhwUMVZ87u`Dx{Z>O|9|`Q+&->FRy-Sjp7DHs zy69KwU-!MxeeuI@&cF4|M9z%AfP?@5 z`Tzg`fam}Kbua(`>RI+y?e7jT@qQ9J+u00v@9M??Vs0RI60puMM)00009a7bBm z000XU000XU0RWnu7ytkO2XskIMF-&v1QRDSk_!r(001BWNkl-RnH`#qxj z(ckc{f+h>}0tZ3t1icm5fKc7M#g}18c5OLG$AT%K1)%@x zf6Tx73#~y2lm@gwp%6kMqj6$Qt@u@k@?Z~sJ_|yQ{%a+$7FYvxYOT4ORuj&&`MDSc6!;lCe{;GI#^he^&l%*9~ zD-fWy4m9h#Ai?QBg4279k-c3hA&k!euK~{jw*Z}J@D;e8i%{ckDL}az&+&*v%wPdR z2bw{lQQ>Dg!+Sj&-up8^>h#VknASO&z1zDnO!=0)Mr!2)BJh|uiUKJq=gJ5n&{YqC zK>KjyiTf${A4>tZ3n6=fZ-fgTuVPZ^I77Wzv{d6A0Ie0ZO=$*?=Rj+;5@@Z_N`_bH zfT3}OrZap-FL3+m6j3czscXtmEI4mLe1U8_cQs}_7ApIV6ad|Odl5pQJRdDZ@Oxk1 zq=h^Pd;y4`{t=MM#OOWHe{vXvpVM_ovVB>EG!W7Up#m=xfft1S7AR(Hjq&iA2Z5VT zdYXjR_C~sQ_aF^(4oOxpwjBD{&hLnswUu0XxP+O*bnTI=s>t)B$u2VtXeN-pWT z1Wv^P{l=+KYmLkw+y_bp>NtJ)b7X%%l~NVcHgL+`4O;1~z=bEI5u6m?uMadZsy@Y1 z@kIQeP=Nt$vnJ8CX>XWd1%?(VrP0bqD-V1h?S-B(hA!g;xlk#!70nGwX@nt|($T{H zoyR~b-pswFKm@naAWr(>Sm5pJI?ROT229iVmTAc!RZ10kLJ0jPXcH<&xthkQjSTMX zLx~`;#i-UaIF%|xeIp=6;E1MyRzddEN~4rU`96N7it^Qo^^RIG@DweTQa?iJZzZ(yar$~r4Ey1D5Tc)i+iLBHI1JgL{I4cYX}+@a{MiilkKikf?>Ii(Syf9Yorti(?EGXzUPC|Xr&NJ)&itvow&lPHgTAQ(M!m}S)TsAu zHFf ztXK@gG=hBKEAV}k7ledYc5urTkfY^OfEfOmEZ0>l?zhlj+15!(bgU2(K`3ewrV1(0 zjthtz5c-yCp-O66J@`JEx+KwLluS*G?6F*U!!U`CT6>&2MVO?76qNEsny1z?Y4%L^ zKeHWaNPJC*_dYTbp){DK^PCROzi27X{ca2SYzbd^nDHn`iB<}wR8X0QO3;o=C096+ z=0-(SS_joD`j*)Xrrz+{<~;!<>QhlbkE=pMLu${R58`fVNaE=jNKjf3Z%pI+3da{X z#R8FtJ?@cW>|F#&M+=Pz1@!V2GA$|2Ub~c?n+{VcI)O_EH4nm9nDH1%Q*yj8!biUF zZie?}8R^TRmBvoQ@O%~ICtsnIAGo#h(Y{9|JA&u>0g{Bq5J=D(&++a?i}}(FvxW{J zRcrP79N_ytTB|#S6w)*eBJl`#3Rypi=?iDlv%e1^C6*zmRLVhlD@HGG9DD{4s?neY zRoA6?avgj3_Hfg$ZehctTd|`SC=nz$6m}v`b)>+QxsANKZYSrye+B78nx1`q2%(9l z5)AZ=gbUSZ*GKspd=OgT6$(^~6$FYv4Jw3pEjt&s-PyHqFG_i1&MNdNFepI#+$7*a zEd^R>WGn(gqoQ%_j(Jo{4pux$OKUC1_8kX*bTg%clrqkhw3a7g+^_o7H)a9_ z>8shZ^u(ZahxEETsbrYUKy8u3*ky@S%j=eyanqZ`{Wf?T3O`zF59 zQ1zhfQW_dTxgJ75Q&Y{&hxYfREJGqZFg|tNl|dyX#HY~e?b|o*q-jDOW->x=aWMbn^0+J*1sHS_J=TXsqS67k4vt)dCLhI>KPKNZ+A894k#r+hnF+ zcm~5qk2BPLlx1h1!z-J2<0^;7rZleSBSHhH(5rOC!L9m8Q(6-y)eZ03*8O~KLmE@~ z3N0ll{XuCM^dBCkJf}*+Fp0!tbe_8~sKu=WB3c{t2(=V_L-A$T#slB z?zsM&L=q7UL!t!WddP@L&BP`auUtUe>T`JJ`IlI^d;x{QBCl;ZiXHdpnBGFU;sp6Y zgF+xwmCA7kZCf9V#H>3?#S#)NQCh7O%I`ESgG4+|-?3q&5SS4Q92a7>5Upc;C2_`O ztLZ*6Li410Ha)%_%Q6FO_7r$N+6(Ymdmgw7sT6k9V&ygGvvvJ;NM{h134S#wY z{yp@S1P|Z;9C#k{7EGsY)+D51P$*O>6+KE-O}XIFx2KzRzkQmYef|gR*>#kydyf*G zmZFSDeceQMbq#Xx;2@>4M>J-GRw2ZL^!(1Iq`mUcuA>;YTyr^Ei%)693M*pLzw0=j zR!o{X0ml06rj`^p?sWTg{JyiVs!~b)x%7t(9#&gNgZZ41VTt!XH2J(X(p11aQO{y zXUEI?kzviQJcahc9h~puRa~@N1>5HBe}6U4Kd}Xjpo`|@Mrf!@^U!}hOgs@|)vX`k zsxN<%|MT6iv8mT!VoRDC?P(@YNYFYd#>{C+W_Bc*KBI|T&cR9~NoJC~^#7iMwxuW; zr>3=*mSwG+cga%Bh{=l83z@cf8U-OZnk{jpKffmDYNS?bGFo?P1(jljbbXShNexsh z4zW}Wj7ZQiI^F)iT4mAIYw07GloTVRn7cHG_cutAF-a%~Brb$Fd9I24nUna5F!P;FSbHpXmoukf* zQYv~_QzlTYxIFXQKOkFXp=+j~OH~?YP2kAB<0v68Y?GGOCfaAVGHveU&MDIxCmT0k zz4j8V^}DrJlnP}Unwt=rDEXcN>Sj(Op2~n3Ic-Np1E!F;(kC8|P?JfKIcp`a{9zp} z4GFwzP#Wv7zZWqi<#?1#CeDO;i`n<;b{d+RS^vnB~cZ4I`4={DjO7`#TWAC+h z;725#Z(oG3e53?niJ)O>h!;$){Y9hYyOVcFccw{1B%5D+jrK$m%;<@xhIoBb8Bi>~ zd<~m^^8iaPTTQLDx#Y{A<=J~5!06u3l(r@+UQl+%Q*p8f`YitYiudGi>Z>kOs>4spN*y~evg(?5G4JCyus7>5 z+&_$M8aPgs>R=XXYtf!h$NWxERi1og1DT&a!;FXiLf=J;h&}f2%=^ZT%wF2a`%t41|;%JXpZA1Pz%8v(Icoo;iui?H}fee3i2Ab5^cI#y0UkaT%Zd@yF>s zIs{S&mbV1b5MYSerq;TRaKoT|jf_OVilL)X>{t?%1Y5A-fj`sNwUbmPL8>lEZ8AaM z-X5%J(^z)(+o4op){-R@vRRVJ6e!3f;#}~Fk8r%FmmPn296LKm+_oqd6p5(BNX7a2 z<)5Yf&zmS-vznTZzMZ!v(sa(6$BX3x!T`%OgH4htF%Tp(F(NTZso)ZcL{J78w#0KP zq+&K5w_i{7^FQU7uWA0~C%N>xx53Ch3WYMwK?i^QtIgC(wP=EUx%PapZM3IJv`j?A zA|OnD^s(F6`q=ZtQgJ-jC6P`saBPsaX)RoL^Hg4W^idYBej8CV+De|>W2#nZXvnbY zhU=*miwx}O;^3~m%v`pVAI`a!w!UHb^=(}D&hr`Q?WbYF6mVh6oY}losgkNsBNb?& zN!Q27XB~=pMO{l05izh$18EzWi5OBy&ba6t_UoV0`kvLS{@_}0UBC>gSn;}nX^TYZ z8sUTVgZ34|2W*<>%m5Gm>;L{cFFp1=p5qg3tRtVTaA;pIPQ~Nsq2t{C@jGds+m17I z41|FY^5ou(0FlZtTq-b>FHmlnz}!{maQoc%usl~n-E|A+uRa?pWg0SRaKn|gPlQ;4 zNYuoLni!VByrnI~Gd8hgl-Y}#G2<3S)Idwg%%x|LsY_7G6-Xh8bhbgU5TKWmSdj?U z>zkuBCR#_82or-+Xvc$yMbnfPasvbW>1PjNTQ={%?{Q?K1ymZM~a|SeohO{uTAknO@f=Qe;4F(p{6w2 zFCdHrlmxR@T|nXROE_gWn9sSAYd>-Z@q|t9K#~4|9Ktp^)OD2q^QBLbi77VyZaqmn zYF5k*;M?=i+Q5ng<+SUbq=2X?q=8gE+7KuK`R-xT%ce7-rj}=a`zR)oWj9>T%JWvy zx#Kauu=s7%H8!KQ#xNw&h-9Fuz*z>|h7QgF4j=Z#ug4UFdyTq%CM$h1htN)FcI$CL3 zyNU(xSP_t2DGUIknQ zhJip}#cZn1>n6q(^U$8&Y(sq_-rUwqOKT&K{^B`4dBqauUbdE=-G}Hq+>c=y{M*$Z zBYUJ9|G3KxPqJEC*rp_h!C*#GkRFk20r}bQbN@enkMo++bd3~P{^F16XrBzO2f2~a zP6-Qdyo&wz{DDnRY-Z`wMdR|u=%}j#*vwtJ1h<%DppvH{(gtoFLPj7W`0qO&;oV>S z0?}#(OnCh3_b~|&AHEp!BS8V@n`o^_#^aQSM~J1O;}wvViKx-Lf8!p?*&-rhlGF}^ zhlgNfm_-+#g;Ok1_I<>m9BcaFqcTb|W$`Q1;U`g_`;%3U&rf16&#>$_U&i>u*Qv85 z<+4M~UANLPsRcamHFL4x_A)(T=4CvE>3 z;Jf$n!&`6X!&5J&CK4fK0KUVHZF@=o=QHeD*1_@>GXvN698exYND`?Sg^}E;9?&bR zh=Z1G(!Z|Lc@zvi zP6><~R%obJIb+3A49{oxGtV)tJ;Qe%{1U6Lyol`30I%*kMAv)oV1FXYbwB!h$mfDE zQW{i{2a>TULj(C!hu{b0$}7&=x$8hbZS$uvaOfC)Wen1fa|G)p0voARpqj?(f+9-A+LdU{JCbVU+;wBvn8Zi=G!Z6LSp5Y{KDwQjEzK;m!lHq!`ny%+>m^Ob3p5q~-lwu38B38Zcr^JTuFq zKkq?T9aP^CUOEnz1vn?Wv^bf6ftD~*;(h<}_q=@hd3ay=0WD8&!CLn+ulkBXL*mGw z4_T)mr8VW681YNap!ue?teVk6d0+q$i3Ys~5p0~0p(jWa@W>72Xr5d@-WXj#x2csT zm5M#?=JK7kElH)LC@IOcq$q6P0jpL86_r%rjnFZ74o7#aq`hqb;uAr(qWvfvo_>~< zYcE0=2Kv|la!SKEN#!_7$0-d3F%}MsXHR4CpYOscIXt)iW%6BzsT?>&+Ev8j5zMA~ zGOOmZa_Jl>SD;vgsv?%k1R-OLqxuL5Ml^Vj#V8C8(oi1{a&fSR#{o>=^N7Y{zZ*V2 zd`l!|v*OxCJoNGrCJvX``^*MrthpG<<)Ek#6(~8Jw{{I1|M*9WT@_|8ok-vQZuYF- z#)^wBghZ6%zgv%e?gGe{P9_kYc6=npG$DaPg$QuriUqK20a#`@`&MCRNv52F zI@!nZ2~M6{%K$=ENRbIg@dzVKa$%esR&dF%!7A->-&tUo^d31*CSl=tdQ<^jqpVZu zS6zgZ_^yAC6hXrv6E`@xxS0z|f?s^^zrmJ4d?^onFDNi71v)xfSaanHe)RlzXl_VD zO^j_H`36VlOl8)RId8~3du!Yx{f49~kfC)A5TUR}Bv61E2|owIBWwoD z6@AYMdW{eYx9Z?&?#8f6LI{Qj3oM;KnIo5VGW!R==TDFP1>~qToPZ|l`v|RBzWOY1 zES#De?p=8k4Sj>0cmKb_;K=`;;HTlXoyhQp5xBWw1_{rwg70MbeJaNA!98qy^$<06 zNraFk;O^1ZfUGzQuT;epB5#=7seMgus7!KVnsYCo&-SyXQN8|a-1E)K|@TbcnwNSMI|Y=$0#W(e?)1a+MX=E~of`cM$*B zzjODmo?_C6E+X3AhAxA3crWOaj|ee3&hjKJYakit zKR$mu`!{SPHJIa!#uV|0P1Te)rt4zcVDvmfK)2Jr#<=BuSCVN+a&%iag?tG+7QwPj zVu{EW&}psIipJUmyY~$;wY@b^f{-B=69_)4fKfOSGJv7KdSJ&6yvOzbhHx0I8b$C7 zX+XrLsy)8_kxw&2IxK3fVX#=`;6RDj4h?<#sTX#CbS$lF@lQ_@ z-_y;4TRt4r_HSxU<+vGv6*1o({EXIY9EWPvs5*<^*mf0I=YFuY%+*Z~QlDY|#+4lH4yl2+a z(Ar8vdpj%6nn!be2G{W(bbbH5h6L9VluIFvPI>sQi&t@wqg-L%^_efk){pY1+Q7o$v9=6C0Q_r;Q`|GKE5g$xWHSbG7)k z?|L6KO#{Oa?CI-g+O!rt&qIflz-q3DVOtCh6)^tsy;tD*8s&O)ES$nCFCU_9S_5uX zVOaqaWZabNPulf+8e8h#5iJjjNACVLIbYMZXkOqgUbr6@e=FvP0_)cku1C)=V3|Dj zlY4M?ZDYkbb8%xPyX`cWb^lcd000CQNklkTK1_)#W-^KIL1G|?@AC7zgWc6l4Meuu zp@Y+WXtf0)9x@DL*3>Dr?TOM58y|d_zQa8@QV_4J569{<*hZi3JkofA|LcT4<-k%r z#-1%Nvge71x!~McOq@OeD;~wRO%4|oixw`%Emxju?U=SUpRReV+Ax`YyZ6w$zlY&! ziMGl00a?or_B_>W4qsU8+uw`piU0V^qKH5UsudT@1bm&+p8q-Rb2I3;_I;ER7${6>P{|fK)}LjtSjCSfh_$v- z(>|4n9qr7T*@0OX5~y7TrU9Ju3pYxHJh$o$GPCr71#}RV+ueB zyx{@|4)!vA!8CSnKTK;w3`5A^OrRe&G?hRI*t55f)(JJ(h5*k;`8tL2Z&X@;P$<=@ zG)Q0IIc|8&B>*8S{W&R8;?V+W3q1`MT$rIHMfX z5rW$0dYn=PA;7R~(Bn>zYpt=OQJi9qWh*-P=^ejk?$Q}--h7ya3tNLM8iKkIl0u=( zkwZhYw>N})e|6%Rlva8@==I9?Z`E2)M(O2Bvk0wbOGMktJ;N=X=PaqQt6ucE&AXV; zn6A{Uxh&g0aZ2wWHa>VT-?eXNqpEJjvNzyVg5&ctYR^^cLvwf-Bk;_K6{RLw z6ChRz+`$1-^OqnEgQ(UZ1x}%i6e2j6==&f9{~sB%^oC3E^Zoz;002ovPDHLkV1m&> BAk+W= literal 11111 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 diff --git a/images/brand/64.webp b/images/brand/64.webp index 6cac4dc64ed8fa08132e0b837a612bafa6eb256f..e2b69b4789a50ea2cdbb499e6a4a5f31b204de23 100644 GIT binary patch literal 3604 zcmV+v4(st!Nk&Et4gdgGMM6+kP&il$0000G0000#002J#06|PpNbUgu01aQFpluuJ zn(Q9_h`*|u%l*8T72 zpU-=r_nw_VME?`ru28_^cG}E5AJ_9~x4ED+9E)1{WwtrpoMFq$ zI%cR$CSLqIL2W#d;-4m}lff!~=By~MXaoN67ZK=n_xx@nVQQ86RYE4>>+r)`MX27? z_^u&k&o}t8W)Uk7HQoi}Ea@Ka41$>vuP~Bc$C?_i{?f0$ZHuza>;8E zLvq1u?eHRZyzw4okQ?#UOIwio81tnK%Sg5P(a778x%c^@HS5UK`PK17kXRk~U2d6B z|5@RWeYc1D0)Gy)3aVTDQw=6jJKgeMDiKHJe8&HYR}yv88G2H|3aVOL^i{Q<2^F6O zbPu|%2*y_x4D8Fkx*cQp`T=8|X1;7e|3|xzVXZMtXT0b;j>ag_dLu_H>gB)Va&bf5 zda=|(0gu~hGxL00&!^o6-2ebqP&gp?3jhF+Dgd1UDnI~006rNCgFzt$!zEP&0AVdn z8tI?#4jk7Mr_*0qe?9)U`my~l{%2=@*}oNejOY(QFTsC_|0MiU{;9Cu z0RNAFFaD?f-|wTz2lnqUPTKzI`2q9+{73l@^iS(YB+dsSiU;i!n|L+I$5A5IH|GoYDeTDup{HOZ=_7Cv?=s$h`-hIG(G5*T?1%Ckk zUqTh2UI6_o&IH-t)C({lz&`{(NVPyefPMgesDCrt0R92|gYZvq1NevG7Vro155YaK zAHY9>e+c{)davE0CB!N9td zoH@nny$2}25php6&Ekkz&Xt+Up?9WH19gCh*)sCx(m8*>JRB8l*gRGBa8%R=Bq`sA z*MwMS)puL}^Hh)3B=3{fm5)cigmO_{7ozN8yUi@#vZw~jy0Ea9zktDij{rMJP zLvUA$igZRJJKl&t7jTGgL`O;hHQ=pcn&`!qY2>5Bj;OcwavdJqr!3I!vXn61l7)x;`tex^UzSg5|;TT zVr-;8d?ocZ^KVQMP`A6Dv7qe#HLlP^ z=j~a@OiiQ*$jp)qlQ$93V%GkPLAJzLOsu12uJVFb^XKSlzu;CsUx0bOS#~tOK~I5BKrc_(s@d5&GFJsV3SdUgwaBtFiwj4E4n)PYE#CJiPoo@YZNi=;T&H~#M!Ad3 zNrNY5$tf`^)vPJm4*nPay$Xn1L&gx@9dRBe9dB=7pO-5Rdy-8fX1WS@c$zZf$giEA zaeb@XcskV4x1)4l@!P#?{T|!%%7i&a^_G_ah*08(f^~G%I{HWWhDSpO`==NYKMBx__@ctpusPOpr}0MAZdz2 zPMiPFRPr1BB{TF>sOe<#KSTOJ)DP1Hy`vAUp!k|Jqfq)qG&eUgqyI9x0q<|LAF?oC z#W_Y9(jfFvPO#skggA<%`8Z{_#wrT%|KGt=mY&hxEZR9XDYQ{!y$L#`6#NmPb|_VJ1Za>`H&;KNmQNu;k@~rTG(oO>Xk{3&H$=`!vv`j3n zz>TXRrbbGHzqDk`{c1>3BNFve_qbbywUTBpz?W-_IZBl8b3V@uLCiG_by!FfyEvE zy)zKphl7L!l6ZiuFPQRH2!>Q1xv#qyS205s{fN%&Md0=|?ibrg-F3TGUX%dHCk&_l zCv{b$qsiE^J^7}N|5@^jQG(Eq7-(tu_Cm6=>6{0ygWo2E559QwRW?0v2+X$;W(Dk< zh(5T0ps%mZa-<3FHFfTZo{{&=Nb#6H5{s;@qAc!`g=(|d{Zx>{lT0~E{_j5SK$wl< ziw>WZR@CNna4>$l3VdHx!W{F(`vdW8+$baZ2_Zs1g-8rXu4_2eNa%28!6wPRCKgi|DH#) zuwHEMV00sOIqQECj5W#MZ%;<~=iM@QiEgSu_kl z&TfC)4sCNMsI+db@^=5>!ujUg?7vEm!Mm?4eVX3O?@v9)(W1aUQ0XZNkJm8Ad<1RK zqCR={Wz@48ObxucC@&?p1oRiYL^+_@qx%juzof&rk*!))9&&I}Q1_&Yt1%YTrl_Rf z3v%nJmeu+73*1@ukFhrs5MUkIPzpP#Y=jc9y(u#2Rq!#3J0+L8278k5GA{TFNB$p? zoJkHCqD=X&244}L(E+i0z0sohaiU@B&>H!>=i0jTJnN*WJhYyr2;=OkBNFxC<`No* zgn&a{PjzxsS6WVouJOC;S$jhWrIu&KTl`$&Tu)yHG$HJtdSe9rM|MbTd($Z54}rBZ z@RWq4)?fbiv&by@eK&eqf)~?yp^{UFpu8gl<`(L`2pa8sS+zaOrRzyE@@&D)u#P6 zy?<*-$AI>WBPIgn3Jzpk+fG!{j?Gj8Ny%a3Fk+zm2kl$jT{cfjkowuhc_zuigv&@l a!7uZjE#lXz;!BF0vC^fA!hQlQ0000$<1=0W literal 3888 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 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)