diff --git a/app/settings.py b/app/settings.py
index 9a45592..c55fcd8 100755
--- a/app/settings.py
+++ b/app/settings.py
@@ -146,6 +146,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
GEOIP_PATH = BASE_DIR / 'geo'
DEFAULT_PANDORA_API = "https://archive.njp.ma/api/"
+TIMELINE_QUERY = {'conditions': []}
URL_PREFIX = ''
diff --git a/app/static/css/partials/_film.scss b/app/static/css/partials/_film.scss
index a0807e4..acdd2e9 100755
--- a/app/static/css/partials/_film.scss
+++ b/app/static/css/partials/_film.scss
@@ -81,12 +81,12 @@ main > .film {
}
}
- .videos {
+ .items {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
- .video {
+ .item {
width: calc(100% / 4 - 16px);
a {
color: #000;
@@ -102,7 +102,7 @@ main > .film {
}
}
@media screen and (max-width: 799px) {
- .video {
+ .item {
width: 100%;
}
}
@@ -147,7 +147,7 @@ main > .film {
}
main > .film {
- .info-meta, .bio-block, .play, .summary-block, .texts {
+ .info-meta, .bio-block, .play, .summary-block, .texts, .items {
background: rgba(0, 0, 0, 0.55);
border-radius: 2px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.6);
diff --git a/app/static/js/ascroll.js b/app/static/js/ascroll.js
index f2e35cc..623c0cc 100755
--- a/app/static/js/ascroll.js
+++ b/app/static/js/ascroll.js
@@ -1,11 +1,10 @@
var cache = cache || {}
var layer = 'descriptions'
-var baseURL = 'https://pad.ma'
var imageResolution = 480
var videoExtension
async function pandoraAPI(action, data) {
- var url = baseURL + '/api/'
+ var url = pandoraURL + '/api/'
//var url = '/pandoraAPI/'
var key = JSON.stringify([action, data])
if (!cache[key]) {
@@ -233,13 +232,13 @@ function renderAnnotation(config, video, ascroll, annotation) {
var director = annotation.director[0]
}
var txt = `${title} (${director})`
- figcaption = `${txt}`
+ figcaption = `${txt}`
}
- annotation.value = annotation.value.replace(/src="\//g, `src="${baseURL}/`).replace(/href="\//g, `href="${baseURL}/`)
+ annotation.value = annotation.value.replace(/src="\//g, `src="${pandoraURL}/`).replace(/href="\//g, `href="${pandoraURL}/`)
div.innerHTML = `
@@ -250,7 +249,7 @@ function renderAnnotation(config, video, ascroll, annotation) {
document.addEventListener('scroll', onVisibilityChange(div.querySelector('.frame'), function(visible) {
var src
if (config.edit) {
- src = `${baseURL}/${annotation.id.split('/')[0]}/480p.webm`
+ src = `${pandoraURL}/${annotation.id.split('/')[0]}/480p.webm`
}
if (config.loaded && visible) {
updatePlayer(video, frame, annotation['in'], annotation['out'], src)
@@ -270,7 +269,7 @@ function renderAnnotations(config) {
video.WebKitPlaysInline = true
video.muted = true
if (config.item) {
- setVideoSrc(video, `${baseURL}/${config.item}/480p.webm`)
+ setVideoSrc(video, `${pandoraURL}/${config.item}/480p.webm`)
}
video.addEventListener('timeupdate', timeupdate)
video.addEventListener('touchstart', showOverlay)
@@ -290,7 +289,7 @@ function renderAnnotations(config) {
if (frame) {
var src
if (config.edit) {
- src = `${baseURL}/${config.first.id.split('/')[0]}/480p.webm`
+ src = `${pandoraURL}/${config.first.id.split('/')[0]}/480p.webm`
}
updatePlayer(video, frame, config.first['in'], config.first['out'], src)
}
diff --git a/app/static/js/play.js b/app/static/js/play.js
new file mode 100644
index 0000000..1cbd416
--- /dev/null
+++ b/app/static/js/play.js
@@ -0,0 +1,439 @@
+var cache = cache || {}
+var layer = 'descriptions'
+var imageResolution = 480
+var videoExtension
+
+async function pandoraAPI(action, data) {
+ var url = pandoraURL + '/api/'
+ //var url = '/pandoraAPI/'
+ var key = JSON.stringify([action, data])
+ if (!cache[key]) {
+ var response = await fetch(url, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ action: action,
+ data: data
+ })
+ })
+ cache[key] = await response.json()
+ }
+ return cache[key]
+}
+
+function setVideoSrc(video, src) {
+ var ext
+ if (!videoExtension) {
+ [
+ ['video/mp4; codecs="avc1.42E01E, mp4a.40.2"', '.mp4'],
+ ['video/webm; codecs="vp8, vorbis"', '.webm'],
+ ].forEach(opt => {
+ if (videoExtension) { return }
+ if (video.canPlayType(opt[0]).replace('no', '')) {
+ videoExtension = opt[1]
+ }
+ })
+ }
+ src = src.replace('.webm', videoExtension)
+ if (src != video.src) {
+ video.src = src
+ }
+}
+
+function resize() {
+ var video = document.querySelector('video')
+ if (video && video._frame) {
+ var rect = video._frame.getBoundingClientRect();
+ video.style.top = (rect.top + window.scrollY) + 'px'
+ }
+}
+
+function updatePlayer(video, frame, currentTime, out, src) {
+ var rect = frame.getBoundingClientRect();
+ video.style.opacity = 0
+ video.style.top = (rect.top + window.scrollY) + 'px'
+ video.style.display = 'block';
+ if (src) {
+ setVideoSrc(video, src)
+ }
+ //video.poster = frame.querySelector('img').src
+ var muted = video.muted
+ // video.muted = true
+ video.currentTime = currentTime
+ video.dataset.in = currentTime
+ video.dataset.out = out
+ video._frame = frame
+ const show = event => {
+ video.style.opacity = 1
+ video.muted = muted
+ video.removeEventListener('seeked', show)
+ }
+ video.addEventListener('seeked', show)
+ video.play()
+}
+
+function isElementInViewport (el) {
+ var rect = el.getBoundingClientRect();
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
+ Math.floor(rect.right) <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
+ );
+}
+
+function onVisibilityChange(el, callback) {
+ var old_visible;
+ return function () {
+ var visible = isElementInViewport(el);
+ if (visible != old_visible) {
+ old_visible = visible;
+ if (visible) {
+ el.classList.add('visible')
+ } else {
+ el.classList.remove('visible')
+ }
+ if (typeof callback == 'function') {
+ callback(visible);
+ }
+ }
+ }
+}
+
+function scrollTo(element) {
+ var delta = element.offsetTop - document.scrollingElement.scrollTop,
+ duration = 1000, t = 40, n = duration / t,
+ step = delta / n;
+
+ function scroll() {
+ if (document.scrollingElement.scrollTop + step > element.offsetTop) {
+ document.scrollingElement.scrollTop = element.offsetTop
+ n = 0
+ } else {
+ document.scrollingElement.scrollTop += step
+ }
+ n--
+ if (n) setTimeout(scroll, t)
+ }
+ scroll()
+}
+
+function timeupdate(event) {
+ if (event.target.dataset.out && event.target.dataset.in) {
+ var in_= parseFloat(event.target.dataset.in)
+ var out_= parseFloat(event.target.dataset.out)
+ if (event.target.currentTime >= out_) {
+ /*
+ var next
+ if (event.target._frame) {
+ next = event.target._frame.parentElement.nextSibling
+ if (next) {
+ next = next.querySelector('.frame')
+ }
+ }
+ if (next) {
+ scrollTo(next)
+ } else {
+ event.target.pause()
+ }
+ */
+ if (config.pause) {
+ event.target.pause()
+ } else {
+ event.target.currentTime = in_
+ }
+ }
+ }
+}
+
+function formatInfo(config, ascroll) {
+ var info = document.createElement('div')
+ var h1 = document.createElement('h1')
+ h1.innerHTML = config.title
+ info.appendChild(h1)
+ var h2 = document.createElement('h2')
+ h2.innerHTML = config.byline
+ info.appendChild(h2)
+ var div = document.createElement('div')
+ div.classList.add('intro')
+ div.innerHTML = config.body
+ info.appendChild(div)
+ ascroll.appendChild(info)
+ return info
+}
+
+function showOverlay(event) {
+ event.stopPropagation()
+ event.preventDefault()
+ document.querySelectorAll('#video-overlay').forEach(element => element.remove())
+ var video = event.target
+ var rect = video.getBoundingClientRect();
+ var overlay = document.createElement('div')
+ overlay.id = 'video-overlay'
+ overlay.style.top = video.style.top
+ overlay.style.width = rect.width + 'px'
+ overlay.style.height = rect.height + 'px'
+ overlay.style.position = 'absolute'
+ overlay.style.display = 'flex'
+ overlay.style.alignItems = 'center'
+ overlay.style.justifyContent = 'center'
+ //overlay.style.fontSize = '45px'
+ video.controls = false
+
+ var off = ``
+ var on = ``
+
+ if (video.muted) {
+ overlay.innerHTML = off
+ } else {
+ overlay.innerHTML = on
+ }
+ overlay.addEventListener('click', event=> {
+ video.muted = !video.muted
+ if (video.muted) {
+ overlay.innerHTML = off
+ } else {
+ overlay.innerHTML = on
+ }
+ })
+ var timeout = setTimeout(() => {
+ video.controls = false
+ overlay.remove()
+ }, 3000)
+ overlay.addEventListener('mousemove', event=> {
+ clearTimeout(timeout)
+ timeout = setTimeout(() => {
+ video.controls = false
+ overlay.remove()
+ }, 500)
+ })
+ video.parentElement.appendChild(overlay)
+
+}
+
+function renderAnnotation(config, video, ascroll, annotation) {
+ var div = document.createElement('div')
+ div.classList.add('annotation')
+
+ var color1 = `hsl(${annotation.color1.hue}, 70%, 75%)`
+ var color2 = `hsl(${annotation.color2.hue}, 70%, 75%)`
+ if (!config.first) {
+ config.first = annotation
+ config.info.style.background = color1
+ }
+ div.style.background = `linear-gradient(to bottom, ${color1}, ${color2})`;
+ var figcaption = ''
+ if (annotation.title) {
+ if (config.language == 'zh') {
+ var title = annotation.title.split('/')[1] || annotation.title
+ var director = annotation.director[1]
+ } else {
+ var title = annotation.title.split('/')[0]
+ var director = annotation.director[0]
+ }
+ var txt = `${title} (${director})`
+ figcaption = `${txt}`
+ }
+ annotation.value = annotation.value.replace(/src="\//g, `src="${pandoraURL}/`).replace(/href="\//g, `href="${pandoraURL}/`)
+ div.innerHTML = `
+
+
+
+ ${annotation.value}
+ `
+ ascroll.appendChild(div)
+ var frame = div.querySelector('.frame')
+ document.addEventListener('scroll', onVisibilityChange(div.querySelector('.frame'), function(visible) {
+ var src
+ if (config.edit) {
+ src = `${pandoraURL}/${annotation.id.split('/')[0]}/480p.webm`
+ }
+ if (config.loaded && visible) {
+ updatePlayer(video, frame, annotation['in'], annotation['out'], src)
+ }
+
+ }))
+}
+
+function renderAnnotations(config) {
+ var ascroll = document.querySelector('#ascroll')
+ config.loaded = false
+ var video = document.createElement('video')
+ video.classList.add('player')
+ video.playsinline = true
+ video.setAttribute('playsinline', 'playsinline')
+ video.setAttribute('webkit-playsinline', 'webkit-playsinline')
+ video.WebKitPlaysInline = true
+ video.muted = true
+ if (config.item) {
+ setVideoSrc(video, `${pandoraURL}/${config.item}/480p.webm`)
+ }
+ video.addEventListener('timeupdate', timeupdate)
+ video.addEventListener('touchstart', showOverlay)
+ video.addEventListener('mouseover', showOverlay)
+ var box = document.createElement('div')
+ box.classList.add('vbox')
+ box.appendChild(video)
+ ascroll.appendChild(box)
+
+ config.info = formatInfo(config, ascroll)
+ config.annotations.forEach(annotation => {
+ renderAnnotation(config, video, ascroll, annotation)
+ })
+ config.loaded = true
+ if (config.first) {
+ let frame = ascroll.querySelector('.annotation .frame')
+ if (frame) {
+ var src
+ if (config.edit) {
+ src = `${pandoraURL}/${config.first.id.split('/')[0]}/480p.webm`
+ }
+ updatePlayer(video, frame, config.first['in'], config.first['out'], src)
+ }
+ }
+ if (config.item_url || config.cited) {
+ var box = document.createElement('div')
+ var color = config.annotations[config.annotations.length - 1].color2
+ box.style.background = `hsl(${color.hue}, 70%, 75%)`
+ var div = document.createElement('div')
+ div.classList.add('related-film')
+ if (config.item_url) {
+ if (config.language == 'zh') {
+ div.innerHTML = `
+ 引用影片: ${config.item_title_zh} (${config.item_director[1]})
+ `
+ } else {
+ div.innerHTML = `
+ Film cited: ${config.item_title} (${config.item_director[0]})
+ `
+ }
+ } else {
+ var html = []
+ config.cited.forEach(film => {
+ var title_en = film.title.split(' / ')[0]
+ var title_zh = film.title.split(' / ')[1] || title_en
+ if (config.language == 'en') {
+ var director = film.director ? ` (${film.director[0]})` : ''
+ html.push(`${title_en}${director}`)
+ } else {
+ var director = film.director ? ` (${film.director[1]})` : ''
+ html.push(`${title_zh}${director}`)
+ }
+ })
+ var films = html.length == 1 ? 'Film' : 'Films'
+ html= html.join(', ')
+ if (config.language == 'zh') {
+ div.innerHTML = ` 引用影片: ${html}`
+ } else {
+ div.innerHTML = `${films} cited: ${html}`
+ }
+ }
+ box.appendChild(div)
+ ascroll.appendChild(box)
+ }
+}
+
+async function loadClips(annotations) {
+ var items = annotations.map(annotation => annotation.id.split('/')[0])
+ items = [...new Set(items)]
+ return pandoraAPI('findClips', {itemsQuery: {
+ conditions: [{key: 'id', operator: '&', value: items}]
+ }, range: [0, 10000], keys: [
+ 'id', 'hue', 'saturation', 'lightness'
+ ]}).then(response => {
+ var colors = {}
+ response.data.items.forEach(clip => {
+ colors[clip.id] = clip
+ })
+ var previous
+ annotations.forEach(annotation => {
+ var clipId = annotation.id.split('/')[0] + '/' + annotation.in.toFixed(3) + '-'+ annotation.out.toFixed(3)
+ annotation.color1 = colors[clipId]
+ if(previous) {
+ previous.color2 = annotation.color1
+ }
+ previous = annotation
+ })
+ if (annotations.length) {
+ annotations[annotations.length - 1].color2 = annotations[0].color1
+ }
+ return annotations
+ })
+}
+
+function loadAnnotations(config) {
+ if (config.item) {
+ pandoraAPI('get', {id: config.item, keys: [
+ 'layers'
+ ]}).then(response => {
+ var annotations = response.data.layers[config.layer].filter(annotation => {
+ return !(config.user && annotation.user != config.user)
+ })
+ loadClips(annotations).then(annotations => {
+ config.annotations = annotations.filter(annotation => {
+ if (config.only_e) {
+ if (annotation.value.slice(0, 2) == 'E:') {
+ annotation.value = annotation.value.slice(2).trim()
+ return true
+ } else {
+ return false
+ }
+ } else {
+ return annotation.value.slice(0, 2) != 'E:'
+ }
+ })
+ renderAnnotations(config)
+ })
+ })
+ } else {
+ var cited = {}
+ pandoraAPI('getEdit', {id: config.edit, keys: []}).then(response => {
+ var annotations = []
+ response.data.clips.forEach(clip => {
+ cited[clip.item] = {
+ title: clip.title,
+ director: clip.director,
+ id: clip.item
+ }
+ clip.layers[config.layer].forEach(annotation => {
+ if (config.user && annotation.user != config.user) {
+ return
+ }
+ ;['title', 'director', 'date'].forEach(key => {
+ annotation[key] = clip[key]
+ })
+ if (config.only_e) {
+ if (annotation.value.slice(0, 2) == 'E:') {
+ annotation.value = annotation.value.slice(2).trim()
+ annotations.push(annotation)
+ }
+ } else {
+ if (annotation.value.slice(0, 2) != 'E:') {
+ annotations.push(annotation)
+ }
+ }
+ })
+ })
+ loadClips(annotations).then(annotations => {
+ config.annotations = annotations
+ config.cited = Object.values(cited)
+ renderAnnotations(config)
+ })
+ })
+ }
+}
+
+config.layer = config.layer || layer
+
+if (config.annotations) {
+ renderAnnotations(config)
+} else {
+ loadAnnotations(config)
+}
+
+
+window.addEventListener('resize', resize, false);
+
diff --git a/app/templates/film.html b/app/templates/film.html
index 56f3b29..a113307 100644
--- a/app/templates/film.html
+++ b/app/templates/film.html
@@ -43,7 +43,7 @@ body {
-
+
-