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