Improved soundtrack editing

This commit is contained in:
Eduard Urbach 2017-10-12 17:52:46 +02:00
parent 6d0b2ccdf6
commit 37a9e6cbf4
11 changed files with 142 additions and 68 deletions

View File

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

View File

@ -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 + " "
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")

View File

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

View File

@ -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(`<div class="widget-form">`)
b.WriteString(`<div class="widget" data-api="/api/` + lowerCaseTypeName + `/` + id.String() + `">`)
b.WriteString(`<div class="widget" data-api="` + endpoint + `">`)
b.WriteString(`<h1>`)
b.WriteString(title)
b.WriteString(`</h1>`)
@ -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(`<div class="widget-section">`)
b.WriteString(`<div class="widget-title">` + strconv.Itoa(sliceIndex+1) + ". " + field.Name + `</div>`)
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(`<div class="buttons"><button class="action" data-action="arrayRemove" data-trigger="click" data-field="` + field.Name + `" data-index="`)
b.WriteString(strconv.Itoa(sliceIndex))
b.WriteString(`">` + utils.RawIcon("trash") + `</button></div>`)
b.WriteString(`</div>`)
}
b.WriteString(`<div class="buttons"><button class="action" data-action="arrayAppend" data-trigger="click" data-field="` + field.Name + `">` + utils.Icon("plus") + `Add ` + field.Name + `</button></div>`)

View File

@ -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
.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")

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
iframe
min-height 200px
.video-container
width 100%
.video
width 100%
height calc(100vh - nav-height)
height 100vh

View File

@ -1,6 +1,6 @@
// pack:ignore
const CACHE = "v-2"
const CACHE = "v-3"
const RELOADS = new Map<string, Promise<Response>>()
const ETAGS = new Map<string, string>()
const CACHEREFRESH = new Map<string, Promise<void>>()