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) .tags(id=id)
for index, tag := range value for index, tag := range value
.tag .tag
span.tag-title= tag span.tag-title.action(contenteditable="true", data-trigger="focusout", data-action="save", data-field=id + "[" + strconv.Itoa(index) + "]")= tag
.tag-remove.action(data-action="arrayRemove", data-trigger="click", data-field=id, data-index=index) x 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) button.tag-add.action(data-action="arrayAppend", data-trigger="click", data-field=id)
RawIcon("plus") RawIcon("plus")

View File

@ -7,13 +7,25 @@ component SoundTrackAllMedia(track *arn.SoundTrack)
component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia) component SoundTrackMedia(track *arn.SoundTrack, media *arn.ExternalMedia)
.sound-track.mountable(id=track.ID) .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) 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(data-src=track.MainAnime().Image.Small, alt=track.MainAnime().Title.Canonical, title=track.MainAnime().Title.Canonical)
iframe.lazy(data-src=media.EmbedLink(), allowfullscreen="allowfullscreen") ExternalMedia(media)
.sound-track-footer
a.ajax(href=track.Link()) component SoundTrackFooter(track *arn.SoundTrack)
Icon("music") .sound-track-footer
span posted by if track.Title == ""
a.ajax(href=track.CreatedByUser().Link())= track.CreatedByUser().Nick + " " 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 .widget-section > button,
button, .widget-section > .button
.button margin-bottom 1rem
margin-bottom 1rem
.avatar-preview .avatar-preview
margin 0 auto margin 0 auto

View File

@ -27,14 +27,17 @@ func Edit(ctx *aero.Context) string {
ctx.Data = &arn.OpenGraph{ ctx.Data = &arn.OpenGraph{
Tags: map[string]string{ Tags: map[string]string{
"og:title": track.Title, "og:title": track.Title,
"og:image": track.MainAnime().Image.Large,
"og:url": "https://" + ctx.App.Config.Domain + track.Link(), "og:url": "https://" + ctx.App.Config.Domain + track.Link(),
"og:site_name": "notify.moe", "og:site_name": "notify.moe",
"og:type": "music.song", "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 ... // EditForm ...
@ -43,10 +46,11 @@ func EditForm(obj interface{}, title string) string {
v := reflect.ValueOf(obj).Elem() v := reflect.ValueOf(obj).Elem()
id := reflect.Indirect(v.FieldByName("ID")) id := reflect.Indirect(v.FieldByName("ID"))
lowerCaseTypeName := strings.ToLower(t.Name()) lowerCaseTypeName := strings.ToLower(t.Name())
endpoint := `/api/` + lowerCaseTypeName + `/` + id.String()
var b bytes.Buffer var b bytes.Buffer
b.WriteString(`<div class="widget-form">`) 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(`<h1>`)
b.WriteString(title) b.WriteString(title)
b.WriteString(`</h1>`) 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)) b.WriteString(components.InputTags(idPrefix+field.Name, fieldValue.Interface().([]string), field.Name))
case "[]*arn.ExternalMedia": case "[]*arn.ExternalMedia":
for sliceIndex := 0; sliceIndex < fieldValue.Len(); sliceIndex++ { 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() arrayObj := fieldValue.Index(sliceIndex).Interface()
arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex) arrayIDPrefix := fmt.Sprintf("%s[%d].", field.Name, sliceIndex)
RenderObject(b, arrayObj, arrayIDPrefix) RenderObject(b, arrayObj, arrayIDPrefix)
// Preview
b.WriteString(components.ExternalMedia(fieldValue.Index(sliceIndex).Interface().(*arn.ExternalMedia)))
// Remove button // Remove button
b.WriteString(`<div class="buttons"><button class="action" data-action="arrayRemove" data-trigger="click" data-field="` + field.Name + `" data-index="`) 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(strconv.Itoa(sliceIndex))
b.WriteString(`">` + utils.RawIcon("trash") + `</button></div>`) 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>`) 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) component Track(track *arn.SoundTrack)
h1= track.Title SoundTrackTabs(track)
if track.Title == ""
h1 untitled
else
h1= track.Title
.sound-tracks .sound-tracks
SoundTrackAllMedia(track) SoundTrackAllMedia(track)
p .footer.text-center
a.ajax(href=track.Link() + "/edit") Edit 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 mainPageLoaded: boolean
lastReloadContentPath: string lastReloadContentPath: string
imageFound: MutationQueue elementFound: MutationQueue
imageNotFound: MutationQueue elementNotFound: MutationQueue
unmount: MutationQueue unmount: MutationQueue
constructor(app: Application) { constructor(app: Application) {
@ -34,13 +34,13 @@ export class AnimeNotifier {
this.user = null this.user = null
this.title = "Anime Notifier" this.title = "Anime Notifier"
this.imageFound = new MutationQueue(elem => elem.classList.add("image-found")) this.elementFound = new MutationQueue(elem => elem.classList.add("element-found"))
this.imageNotFound = new MutationQueue(elem => elem.classList.add("image-not-found")) this.elementNotFound = new MutationQueue(elem => elem.classList.add("element-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 // These classes will never be removed on DOM diffs
Diff.persistentClasses.add("mounted") Diff.persistentClasses.add("mounted")
Diff.persistentClasses.add("image-found") Diff.persistentClasses.add("element-found")
// Never remove src property on diffs // Never remove src property on diffs
Diff.persistentAttributes.add("src") Diff.persistentAttributes.add("src")
@ -134,7 +134,7 @@ export class AnimeNotifier {
this.contentLoadedActions = Promise.all([ this.contentLoadedActions = Promise.all([
Promise.resolve().then(() => this.mountMountables()), Promise.resolve().then(() => this.mountMountables()),
Promise.resolve().then(() => this.lazyLoadImages()), Promise.resolve().then(() => this.lazyLoad()),
Promise.resolve().then(() => this.displayLocalDates()), Promise.resolve().then(() => this.displayLocalDates()),
Promise.resolve().then(() => this.setSelectBoxValue()), Promise.resolve().then(() => this.setSelectBoxValue()),
Promise.resolve().then(() => this.assignActions()), Promise.resolve().then(() => this.assignActions()),
@ -500,37 +500,59 @@ export class AnimeNotifier {
} }
} }
lazyLoadImages() { lazyLoad() {
for(let element of findAll("lazy")) { 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 // Once the image becomes visible, load it
img["became visible"] = () => { element["became visible"] = () => {
// Replace URL with WebP if supported // Replace URL with WebP if supported
if(this.webpEnabled && img.dataset.webp) { if(this.webpEnabled && element.dataset.webp) {
let dot = img.dataset.src.lastIndexOf(".") let dot = element.dataset.src.lastIndexOf(".")
img.src = img.dataset.src.substring(0, dot) + ".webp" element.src = element.dataset.src.substring(0, dot) + ".webp"
} else { } else {
img.src = img.dataset.src element.src = element.dataset.src
} }
if(img.naturalWidth === 0) { if(element.naturalWidth === 0) {
img.onload = () => { element.onload = () => {
this.imageFound.queue(img) this.elementFound.queue(element)
} }
img.onerror = () => { element.onerror = () => {
this.imageNotFound.queue(img) this.elementNotFound.queue(element)
} }
} else { } 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() { mountMountables() {
@ -752,14 +774,18 @@ export class AnimeNotifier {
return return
} }
// Disallow Enter key in contenteditables // Ignore hotkeys on contentEditable elements
if(activeElement.getAttribute("contenteditable") === "true" && e.keyCode == 13) { if(activeElement.getAttribute("contenteditable") === "true") {
if("blur" in activeElement) { // Disallow Enter key in contenteditables and make it blur the element instead
activeElement["blur"]() if(e.keyCode == 13) {
if("blur" in activeElement) {
activeElement["blur"]()
}
e.preventDefault()
e.stopPropagation()
} }
e.preventDefault()
e.stopPropagation()
return return
} }

View File

@ -2,10 +2,10 @@
visibility hidden visibility hidden
opacity 0 opacity 0
.image-found .element-found
visibility visible visibility visible
opacity 1 !important opacity 1 !important
.image-not-found .element-not-found
visibility hidden visibility hidden
opacity 0 !important opacity 0 !important

View File

@ -62,8 +62,8 @@ content-padding = 1.6rem
content-padding-top = 1.6rem content-padding-top = 1.6rem
content-line-height = 1.7em content-line-height = 1.7em
hover-line-size = 3px hover-line-size = 3px
nav-height = 3.11rem
typography-margin = 0.4rem typography-margin = 0.4rem
// nav-height = 3.11rem
// Timings // Timings
fade-speed = 250ms fade-speed = 250ms

View File

@ -1,24 +1,24 @@
mixin tag-dimensions
padding 0.4rem 0.8rem
margin 0.4rem
height 40px
.tags .tags
horizontal-wrap horizontal-wrap
.tag .tag
ui-element ui-element
padding 0.4rem 0.8rem tag-dimensions
margin 0.4rem margin-right 0
border-right none
.tag-input border-top-right-radius 0
horizontal border-bottom-right-radius 0
button
margin-left 0.8rem
.tag-remove .tag-remove
display inline-block tag-dimensions
margin-left 0.4rem margin-left 0
opacity 0.5 border-top-left-radius 0
border-bottom-left-radius 0
:hover
cursor pointer
.tag-add .tag-add
margin 0.4rem !important tag-dimensions

View File

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

View File

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