1 Commits

Author SHA1 Message Date
4927a26497 Videojs: Show endscreen with links 2018-09-14 02:59:21 +02:00
37 changed files with 1889 additions and 1572 deletions

2176
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -94,31 +94,13 @@ def share_node(node_id):
@blueprint.route('/tagged/<tag>')
def tagged(tag=''):
"""Return all tagged nodes of public projects as JSON."""
from pillar.auth import current_user
# We explicitly register the tagless endpoint to raise a 404, otherwise the PATCH
# handler on /api/nodes/<node_id> will return a 405 Method Not Allowed.
if not tag:
raise wz_exceptions.NotFound()
# Build the (cached) list of tagged nodes
agg_list = _tagged(tag)
# If the user is anonymous, no more information is needed and we return
if current_user.is_anonymous:
return jsonify(agg_list)
# If the user is authenticated, attach view_progress for video assets
view_progress = current_user.nodes['view_progress']
for node in agg_list:
node_id = str(node['_id'])
# View progress should be added only for nodes of type 'asset' and
# with content_type 'video', only if the video was already in the watched
# list for the current user.
if node_id in view_progress:
node['view_progress'] = view_progress[node_id]
return jsonify(agg_list)
return _tagged(tag)
def _tagged(tag: str):
@@ -147,8 +129,7 @@ def _tagged(tag: str):
{'$sort': {'_created': -1}}
])
return list(agg)
return jsonify(list(agg))
def generate_and_store_short_code(node):

View File

@@ -61,10 +61,16 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
post.picture = get_file(post.picture, api=api)
post.url = url_for_node(node=post)
# Use the *_main_project.html template for the main blog
is_main_project = project_id == current_app.config['MAIN_PROJECT_ID']
main_project_template = '_main_project' if is_main_project else ''
main_project_template = '_main_project'
index_arch = 'archive' if archive else 'index'
template_path = f'nodes/custom/blog/{index_arch}.html',
template_path = f'nodes/custom/blog/{index_arch}{main_project_template}.html',
if url:
template_path = f'nodes/custom/post/view{main_project_template}.html',
post = Node.find_one({
'where': {'parent': blog._id, 'properties.url': url},
'embedded': {'node_type': 1, 'user': 1},
@@ -89,7 +95,6 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
can_create_blog_posts = project.node_type_has_method('post', 'POST', api=api)
# Use functools.partial so we can later pass page=X.
is_main_project = project_id == current_app.config['MAIN_PROJECT_ID']
if is_main_project:
url_func = functools.partial(url_for, 'main.main_blog_archive')
else:
@@ -116,7 +121,7 @@ def posts_view(project_id=None, project_url=None, url=None, *, archive=False, pa
return render_template(
template_path,
blog=blog,
node=post, # node is used by the generic comments rendering (see custom/_scripts.pug)
node=post,
posts=posts._items,
posts_meta=pmeta,
more_posts_available=pmeta['total'] > pmeta['max_results'],

View File

@@ -0,0 +1,109 @@
(function(vjs) {
"use strict";
var
extend = function(obj) {
var arg, i, k;
for (i = 1; i < arguments.length; i++) {
arg = arguments[i];
for (k in arg) {
if (arg.hasOwnProperty(k)) {
obj[k] = arg[k];
}
}
}
return obj;
},
defaults = {
count: 10,
counter: "counter",
countdown: "countdown",
countdown_text: "Next video in:",
endcard: "player-endcard",
related: "related-content",
next: "next-video",
getRelatedContent: function(callback){callback();},
getNextVid: function(callback){callback();}
},
endcard = function(options) {
var player = this;
var el = this.el();
var settings = extend({}, defaults, options || {});
// set background
var card = document.createElement('div');
card.id = settings.endcard;
card.style.display = 'none';
el.appendChild(card);
settings.getRelatedContent(function(content) {
if (content instanceof Array) {
var related_content_div = document.createElement('div');
related_content_div.id = settings.related;
for (var i = 0; i < content.length; i++) {
related_content_div.appendChild(content[i]);
}
card.appendChild(related_content_div);
}
else {
throw new TypeError("options.getRelatedContent must return an array");
}
});
settings.getNextVid(function(next) {
if (typeof next !== "undefined") {
var next_div = document.createElement('div');
var counter = document.createElement('span');
var countdown = document.createElement('div');
counter.id = settings.counter;
countdown.id = settings.countdown;
next_div.id = settings.next;
countdown.innerHTML = settings.countdown_text;
countdown.appendChild(counter);
next_div.appendChild(countdown);
next_div.appendChild(next);
card.appendChild(next_div);
}
});
var counter_started = 0;
player.on('ended', function() {
card.style.display = 'block';
var next = document.getElementById(settings.next);
if (next !== null) {
var href = next.getElementsByTagName("a")[0].href;
var count = settings.count;
counter.innerHTML = count;
var interval = setInterval(function(){
count--;
if (count <= 0) {
clearInterval(interval);
window.location = href;
return;
}
counter.innerHTML = count;
}, 1000);
}
if (counter_started === 0) {
counter_started++;
player.on('playing', function() {
card.style.display = 'none';
clearInterval(interval);
});
}
});
};
vjs.plugin('endcard', endcard);
})(window.videojs);

View File

@@ -403,6 +403,7 @@ nav.sidebar
$loader-bar-width: 100px
$loader-bar-height: 2px
.loader-bar
background-color: $color-background
bottom: 0
content: ''
display: none
@@ -411,12 +412,10 @@ $loader-bar-height: 2px
position: absolute
visibility: hidden
width: 100%
z-index: 20
&:before
animation: none
background-color: $primary
background-image: linear-gradient(to right, $primary-accent, $primary)
content: ''
display: block
height: $loader-bar-height
@@ -454,4 +453,3 @@ $loader-bar-height: 2px
.progress-bar
background-color: $primary
background-image: linear-gradient(to right, $primary-accent, $primary)

View File

@@ -1,7 +1,6 @@
$comments-width-max: 710px
.comments-container
max-width: $comments-width-max
position: relative
#comments-reload
@@ -315,6 +314,9 @@ $comments-width-max: 710px
color: $color-success
.comment-reply
&-container
background-color: $color-background
/* Little gravatar icon on the left */
&-avatar
img
@@ -331,7 +333,7 @@ $comments-width-max: 710px
width: 100%
&-field
background-color: $color-background-light
background-color: $color-background-dark
border-radius: 3px
box-shadow: inset 0 0 2px 0 rgba(darken($color-background-dark, 20%), .5)
display: flex
@@ -340,7 +342,6 @@ $comments-width-max: 710px
textarea
+node-details-description
background-color: $color-background-light
border-bottom-right-radius: 0
border-top-right-radius: 0
border: none
@@ -375,6 +376,7 @@ $comments-width-max: 710px
&.filled
textarea
background-color: $color-background-light
border-bottom: thin solid $color-background
&:focus

View File

@@ -30,7 +30,6 @@ $color-primary: #009eff !default
$color-primary-light: hsl(hue($color-primary), 30%, 90%) !default
$color-primary-dark: hsl(hue($color-primary), 80%, 30%) !default
$color-primary-accent: hsl(hue($color-primary), 100%, 50%) !default
$primary-accent: #0bd
$color-secondary: #f42942 !default
$color-secondary-light: hsl(hue($color-secondary), 30%, 90%) !default
@@ -157,5 +156,3 @@ $tooltip-max-width: auto
$tooltip-opacity: 1
$nav-link-height: 37px
$navbar-padding-x: 0
$navbar-padding-y: 0

View File

@@ -24,16 +24,13 @@
color: $color-secondary
#notifications-toggle
color: $color-text
cursor: pointer
font-size: 1.5em
position: relative
user-select: none
> i:before
content: '\e815'
font-size: 1.3em
position: relative
top: 2px
&.has-notifications
> i:before

View File

@@ -77,8 +77,6 @@ body.workshops
a
color: $primary
i
+active-gradient
a
align-items: center

View File

@@ -95,7 +95,7 @@ $search-hit-width_grid: 100px
.search-list
width: 30%
.card-deck.card-deck-vertical
.card-deck.card-deck-horizontal
.card .embed-responsive
max-width: 80px

View File

@@ -67,6 +67,131 @@
&:hover
background-color: lighten($provider-color-google, 7%)
#settings
#settings-sidebar
+media-xs
width: 100%
+container-box
background-color: $color-background-light
color: $color-text
margin-right: 15px
width: 30%
.settings-content
padding: 0
ul
list-style: none
margin: 0
padding: 0
a
&:hover
text-decoration: none
li
background-color: lighten($color-background, 5%)
li
border-bottom: thin solid $color-background
border-left: thick solid transparent
margin: 0
padding: 25px
transition: all 100ms ease-in-out
i
font-size: 1.1em
padding-right: 15px
.active
li
background-color: lighten($color-background, 5%)
border-left: thick solid $color-info
#settings-container
+media-xs
width: 100%
+container-box
background-color: $color-background-light
width: 70%
.settings-header
background-color: $color-background
border-top-left-radius: 3px
border-top-right-radius: 3px
.settings-title
font:
size: 1.5em
weight: 300
padding: 10px 15px 10px 25px
.settings-content
padding: 25px
.settings-billing-info
font-size: 1.2em
.subscription-active
color: $color-success
padding-bottom: 20px
.subscription-demo
color: $color-info
margin-top: 0
.subscription-missing
color: $color-danger
margin-top: 0
.button-submit
clear: both
display: block
min-width: 200px
margin: 0 auto
+button($color-primary, 3px, true)
#settings-container
#settings-form
width: 100%
.settings-form
align-items: center
display: flex
justify-content: center
.left, .right
padding: 25px 0
.left
width: 60%
float: left
.right
width: 40%
float: right
text-align: center
label
color: $color-text
display: block
.settings-avatar
img
border-radius: 3px
span
display: block
padding: 15px 0
font:
size: .9em
.settings-password
color: $color-text-dark-primary
#user-edit-container
padding: 15px

View File

@@ -659,9 +659,6 @@
.user-select-none
user-select: none
.pointer-events-none
pointer-events: none
// Bootstrap has .img-fluid, a class to limit the width of an image to 100%.
// .imgs-fluid below is to be applied on a parent container when we can't add
// classes to the images themselves. e.g. the blog.
@@ -672,15 +669,3 @@
.overflow-hidden
overflow: hidden
=text-gradient($color_from, $color_to)
background: linear-gradient(to right, $color_from, $color_to)
background-clip: text
-webkit-background-clip: text
-webkit-text-fill-color: transparent
=active-gradient
+text-gradient($primary-accent, $primary)
&:before
+text-gradient($primary-accent, $primary)

View File

@@ -15,37 +15,77 @@
@import "../../node_modules/bootstrap/scss/code"
@import "../../node_modules/bootstrap/scss/grid"
@import "../../node_modules/bootstrap/scss/tables"
@import "../../node_modules/bootstrap/scss/forms"
@import "../../node_modules/bootstrap/scss/buttons"
@import "../../node_modules/bootstrap/scss/transitions"
@import "../../node_modules/bootstrap/scss/dropdown"
@import "../../node_modules/bootstrap/scss/button-group"
@import "../../node_modules/bootstrap/scss/input-group"
@import "../../node_modules/bootstrap/scss/custom-forms"
@import "../../node_modules/bootstrap/scss/nav"
@import "../../node_modules/bootstrap/scss/navbar"
@import "../../node_modules/bootstrap/scss/card"
@import "../../node_modules/bootstrap/scss/breadcrumb"
@import "../../node_modules/bootstrap/scss/pagination"
@import "../../node_modules/bootstrap/scss/badge"
@import "../../node_modules/bootstrap/scss/jumbotron"
@import "../../node_modules/bootstrap/scss/alert"
@import "../../node_modules/bootstrap/scss/progress"
@import "../../node_modules/bootstrap/scss/media"
@import "../../node_modules/bootstrap/scss/list-group"
@import "../../node_modules/bootstrap/scss/close"
@import "../../node_modules/bootstrap/scss/modal"
@import "../../node_modules/bootstrap/scss/tooltip"
@import "../../node_modules/bootstrap/scss/popover"
@import "../../node_modules/bootstrap/scss/carousel"
@import "../../node_modules/bootstrap/scss/utilities"
@import "../../node_modules/bootstrap/scss/print"
// Pillar components.
@import "apps_base"
@import "components/base"
@import "components/jumbotron"
@import "components/alerts"
@import "components/navbar"
@import "components/dropdown"
@import "components/footer"
@import "components/shortcode"
@import "components/statusbar"
@import "components/search"
@import "components/flyout"
@import "components/forms"
@import "components/inputs"
@import "components/buttons"
@import "components/popover"
@import "components/tooltip"
@import "components/checkbox"
@import "components/overlay"
@import "components/card"
@import _comments
@import _notifications
@import _error
@import _search
@import components/base
@import components/alerts
@import components/navbar
@import components/footer
@import components/shortcode
@import components/statusbar
@import components/search
@import components/flyout
@import components/forms
@import components/inputs
@import components/buttons
@import components/popover
@import components/tooltip
@import components/checkbox
@import components/overlay
#blog_container
+media-xs
@@ -482,3 +522,29 @@
.blog-archive-navigation
margin-left: 35px
// Used on the blog.
.comments-compact
.comments-list
border: none
padding: 0 0 15px 0
.comments-container
max-width: 680px
margin: 0 auto
.comment-reply-container
background-color: transparent
.comment-reply-field
textarea, .comment-reply-meta
background-color: $color-background-light
&.filled
.comment-reply-meta
background-color: $color-success
.comment-reply-form
+media-xs
padding:
left: 0

View File

@@ -21,7 +21,7 @@
flex: 1 0 20%
max-width: 20%
&.card-deck-vertical
&.card-deck-horizontal
@extend .flex-column
flex-wrap: initial
@@ -98,7 +98,6 @@
i
opacity: .2
/* Tiny label for cards. e.g. 'WATCHED' on videos. */
.card-label
background-color: rgba($black, .5)
border-radius: 3px
@@ -106,7 +105,7 @@
display: block
font-size: $font-size-xxs
left: 5px
top: -27px // enough to be above the progress-bar
top: -25px
position: absolute
padding: 1px 5px
z-index: 1

View File

@@ -24,21 +24,3 @@ ul.dropdown-menu
nav .dropdown:hover
ul.dropdown-menu
display: block
nav .dropdown.large:hover
.dropdown-menu
@extend .d-flex
.dropdown.large.show
@extend .d-flex
.dropdown-menu.show
@extend .d-flex
.dropdown-menu-tab
display: none
min-width: 100px
&.show // .dropdown-menu-tab.show
@extend .d-flex

View File

@@ -2,7 +2,8 @@
.navbar
box-shadow: inset 0 -2px $color-background
.nav
.navbar,
nav.sidebar
border: none
color: $color-text-dark-secondary
padding: 0
@@ -18,6 +19,29 @@
margin: 0
width: 100%
.navbar-item
align-items: center
display: flex
user-select: none
color: inherit
+media-sm
padding-left: 10px
padding-right: 10px
&:hover, &:focus
color: $primary
background-color: transparent
box-shadow: inset 0 -3px 0 $primary
text-decoration: none
&:focus
box-shadow: inset 0 -3px 0 $primary
&.active
color: $primary
box-shadow: inset 0 -3px 0 $primary
li
user-select: none
position: relative
@@ -56,74 +80,49 @@
i
+position-center-translate
.dropdown
min-width: 50px // navbar avatar size
.dropdown
min-width: 50px // navbar avatar size
.navbar-item
&:hover
box-shadow: none // Remove the blue underline usually on navbar, from dropdown items.
span.fa-stack
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
ul.dropdown-menu
li
a
white-space: nowrap
ul.dropdown-menu
li
a
white-space: nowrap
.subitem // e.g. "Not Sintel? Log out"
font-size: .8em
text-transform: initial
&:hover
box-shadow: none // removes underline
i
width: 30px
.subitem // e.g. "Not Sintel? Log out"
font-size: .8em
text-transform: initial
&.subscription-status
a, a:hover
color: $white
i
width: 30px
&.none
background-color: $color-danger
&.subscription-status
a, a:hover
color: $white
&.subscriber
background-color: $color-success
&.none
background-color: $color-danger
&.demo
background-color: $color-info
&.subscriber
background-color: $color-success
span.info
display: block
&.demo
background-color: $color-info
span.renew
span.info
display: block
font-size: .9em
.nav-link
@extend .d-flex
.nav-title
white-space: nowrap
.navbar-item
align-items: center
display: flex
user-select: none
color: inherit
+media-sm
padding-left: 10px
padding-right: 10px
&:hover, &:focus
color: $primary
background-color: transparent
box-shadow: inset 0 -3px 0 $primary
text-decoration: none
&:focus
box-shadow: inset 0 -3px 0 $primary
&.active
color: $primary
box-shadow: inset 0 -3px 0 $primary
span.renew
display: block
font-size: .9em
/* Secondary navigation. */
@@ -133,36 +132,19 @@ $nav-secondary-bar-size: -2px
box-shadow: inset 0 $nav-secondary-bar-size 0 0 $color-background
.nav-link
box-shadow: inset 0 $nav-secondary-bar-size 0 0 $color-background
color: $color-text
cursor: pointer
transition: color 150ms ease-in-out
&:after
background-color: transparent
bottom: 0
content: ''
height: 2px
position: absolute
right: 0
left: 0
width: 0
transition: width 150ms ease-in-out
transition: box-shadow 150ms ease-in-out
.nav-link:hover,
.nav-link.active,
.nav-item.dropdown.show > .nav-link
.nav-item.dropdown.show .nav-link
// Blue bar on the bottom.
&:after
background-color: $primary-accent
background-image: linear-gradient(to right, $primary-accent 70%, $primary)
height: 2px
width: 100%
span
+active-gradient
box-shadow: inset 0 $nav-secondary-bar-size 0 0 $primary
i
color: $primary-accent
color: $primary
&.nav-secondary-vertical
align-items: flex-start
@@ -174,31 +156,19 @@ $nav-secondary-bar-size: -2px
// Blue bar on the side.
.nav-link
box-shadow: inset 0 -1px 0 0 $color-background, inset -1px 0 0 0 $color-background
&:hover,
&.active
color: $primary
@extend .bg-white
box-shadow: inset 0 -1px 0 0 $color-background, inset ($nav-secondary-bar-size * 1.5) 0 0 0 $primary
&:after
background-image: linear-gradient($primary-accent 70%, $primary)
height: 100%
left: initial
top: 0
width: 3px
// Big navigation dropdown.
.nav-main
.nav-secondary
&.nav-main
.nav-link
@extend .pr-5
box-shadow: none
color: $color-text-dark-secondary
&.nav-see-more
color: $primary
font-size: $font-size-xxs
&:hover
color: $body-color
i, span
+active-gradient
.navbar-overlay
+media-lg
@@ -218,6 +188,15 @@ $nav-secondary-bar-size: -2px
background-color: $color-background-nav
text-shadow: none
.navbar-brand
color: inherit
padding: 0 0 0 3px
position: relative
top: -2px
&:hover
color: $primary
nav.navbar
.navbar-collapse
> ul > li > .navbar-item

View File

@@ -1365,3 +1365,28 @@ video::-webkit-media-text-track-display
position: absolute
bottom: 3em
left: 0.5em
#player-endcard
background-color: rgba($black, .5)
bottom: 0
font-size: 1.5em
left: 0
position: absolute
right: 0
top: 0
z-index: 1
#related-content,
.end-card-content
@extend .align-items-center
@extend .d-flex
@extend .flex-column
@extend .h-100
@extend .justify-content-center
.btn-video-overlay
border: thin solid $white
&:hover
background-color: rgba($white, .5)

View File

@@ -0,0 +1,82 @@
// Bootstrap variables and utilities.
@import "../../node_modules/bootstrap/scss/functions"
@import "../../node_modules/bootstrap/scss/variables"
@import "../../node_modules/bootstrap/scss/mixins"
@import _config
@import _utils
// Bootstrap components.
@import "../../node_modules/bootstrap/scss/root"
@import "../../node_modules/bootstrap/scss/reboot"
@import "../../node_modules/bootstrap/scss/type"
@import "../../node_modules/bootstrap/scss/images"
@import "../../node_modules/bootstrap/scss/code"
@import "../../node_modules/bootstrap/scss/grid"
@import "../../node_modules/bootstrap/scss/tables"
@import "../../node_modules/bootstrap/scss/forms"
@import "../../node_modules/bootstrap/scss/buttons"
@import "../../node_modules/bootstrap/scss/transitions"
@import "../../node_modules/bootstrap/scss/dropdown"
@import "../../node_modules/bootstrap/scss/button-group"
@import "../../node_modules/bootstrap/scss/input-group"
@import "../../node_modules/bootstrap/scss/custom-forms"
@import "../../node_modules/bootstrap/scss/nav"
@import "../../node_modules/bootstrap/scss/navbar"
@import "../../node_modules/bootstrap/scss/card"
@import "../../node_modules/bootstrap/scss/breadcrumb"
@import "../../node_modules/bootstrap/scss/pagination"
@import "../../node_modules/bootstrap/scss/badge"
@import "../../node_modules/bootstrap/scss/jumbotron"
@import "../../node_modules/bootstrap/scss/alert"
@import "../../node_modules/bootstrap/scss/progress"
@import "../../node_modules/bootstrap/scss/media"
@import "../../node_modules/bootstrap/scss/list-group"
@import "../../node_modules/bootstrap/scss/close"
@import "../../node_modules/bootstrap/scss/modal"
@import "../../node_modules/bootstrap/scss/tooltip"
@import "../../node_modules/bootstrap/scss/popover"
@import "../../node_modules/bootstrap/scss/carousel"
@import "../../node_modules/bootstrap/scss/utilities"
@import "../../node_modules/bootstrap/scss/print"
// Pillar components.
@import "apps_base"
@import "components/base"
@import "components/jumbotron"
@import "components/alerts"
@import "components/navbar"
@import "components/dropdown"
@import "components/footer"
@import "components/shortcode"
@import "components/statusbar"
@import "components/search"
@import "components/flyout"
@import "components/forms"
@import "components/inputs"
@import "components/buttons"
@import "components/popover"
@import "components/tooltip"
@import "components/checkbox"
@import "components/overlay"
@import "components/card"
@import _notifications
@import _comments
@import _project
@import _project-sharing
@import _project-dashboard
@import _error
@import _search
@import plugins/_jstree
@import plugins/_js_select2

View File

@@ -43,6 +43,7 @@ html(lang="en")
| {% block css %}
link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css') }}", rel="stylesheet")
link(href="{{ url_for('static_pillar', filename='assets/css/base.css') }}", rel="stylesheet")
| {% if title == 'blog' %}
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
| {% else %}

View File

@@ -1,11 +1,10 @@
| {% if current_user.is_authenticated %}
li.nav-notifications.nav-item
a.nav-link.px-2(
id="notifications-toggle",
title="Notifications",
data-toggle="tooltip",
data-placement="bottom")
li.nav-notifications
a.navbar-item#notifications-toggle.px-0(
title="Notifications",
data-toggle="tooltip",
data-placement="bottom")
i.pi-notifications-none.nav-notifications-icon
span#notifications-count
span

View File

@@ -12,6 +12,25 @@ li.dropdown
ul.dropdown-menu.dropdown-menu-right
| {% if not current_user.has_role('protected') %}
| {% block menu_list %}
li
a.navbar-item.px-2(
href="{{ url_for('projects.home_project') }}"
title="Home")
| #[i.pi-home] Home
li
a.navbar-item.px-2(
href="{{ url_for('projects.index') }}"
title="My Projects")
| #[i.pi-star] My Projects
| {% if current_user.has_organizations() %}
li
a.navbar-item.px-2(
href="{{ url_for('pillar.web.organizations.index') }}"
title="My Organizations")
| #[i.pi-users] My Organizations
| {% endif %}
li
a.navbar-item.px-2(

View File

@@ -35,8 +35,8 @@ mixin jumbotron(title, text, image, url)
mixin nav-secondary(title)
ul.nav.nav-secondary&attributes(attributes)
if title
li.nav-item
span.nav-title.nav-link.font-weight-bold.pointer-events-none= title
li.font-weight-bold.px-2
=title
if block
block

View File

@@ -91,9 +91,32 @@ script(type="text/javascript").
'fetch_progress_url': fetch_progress_url,
});
this.endcard({
getRelatedContent: getRelatedContent
});
{% endif %}
});
function getRelatedContent(callback) {
var el = document.createElement('div');
el.className = 'end-card-content';
el.innerHTML = '<h4><button class="js-video-play my-3 px-5 btn btn-video-overlay"><i class="pi-play"></i> Play Again</button></h4>';
el.innerHTML += '<button class="js-video-loop px-4 btn btn-sm btn-video-overlay"><i class="pi-back"></i> Loop</button>';
setTimeout(function(){
callback([el])
}, 0);
}
$('body').on('click', '.js-video-play', function(){
videoPlayer.play();
});
$('body').on('click', '.js-video-loop', function(){
videoPlayerToggleLoop(videoPlayer, videoPlayerLoopButton);
videoPlayer.play();
});
// Generic utility to add-buttons to the player.
function addVideoPlayerButton(data) {

View File

@@ -1,11 +1,8 @@
| {% extends 'nodes/custom/blog/index.html' %}
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
| {% block body %}
.container
.pt-4
h2.text-uppercase.font-weight-bold
| Blog Archive
| {{ blogmacros.render_archive(project, posts, posts_meta) }}
| {% endblock body %}
| {% block project_context %}
#blog_container
#blog_index-container.expand-image-links
| {{ blogmacros.render_archive(project, posts, posts_meta) }}
| {% endblock project_context%}

View File

@@ -0,0 +1,9 @@
| {% extends 'nodes/custom/blog/index_main_project.html' %}
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
| {% block body %}
.container
h3 Blog Archive
| {{ blogmacros.render_archive(project, posts, posts_meta) }}
| {% endblock body %}

View File

@@ -1,44 +1,55 @@
| {% extends 'layout.html' %}
| {% extends 'projects/view.html' %}
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
| {% from 'projects/_macros.html' import render_secondary_navigation %}
| {% set title = 'blog' %}
| {% block page_title %}Blog{% endblock%}
| {% block navigation_tabs %}
| {{ render_secondary_navigation(project, navigation_links, title) }}
| {% endblock navigation_tabs %}
| {% block css %}
| {{ super() }}
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
| {% endblock %}
| {% block body %}
| {% if node %}
| {{ blogmacros.render_blog_post(node, project=project) }}
| {% else %}
| {{ blogmacros.render_blog_index(project, posts, can_create_blog_posts, api, more_posts_available, posts_meta, pages=pages) }}
| {% endif %}
| {% block project_context %}
| {{ blogmacros.render_blog_index(project, posts, can_create_blog_posts, api, more_posts_available, posts_meta) }}
| {% endblock %}
| {% block project_tree %}
#project_tree.jstree.jstree-default.blog
ul.jstree-container-ul.jstree-children
li.jstree-node(data-node-type="page")
a.jstree-anchor(
href="{{ url_for('projects.view', project_url=project.url) }}")
| Browse Project
li.jstree-node(data-node-type="page")
a.jstree-anchor.jstree-clicked(
href="{{ url_for('main.project_blog', project_url=project.url) }}") Blog
| {% for post in posts %}
li.jstree-node
a.jstree-anchor.tree-item.post(
href="{{ node.url }}")
.tree-item-thumbnail
| {% if post.picture %}
img(src="{{ post.picture.thumbnail('s', api=api) }}")
| {% else %}
i.pi-document-text
| {% endif %}
span.tree-item-title {{ post.name }}
span.tree-item-info {{ post._created | pretty_date }}
| {% endfor %}
| {% endblock %}
| {% block footer_scripts %}
include ../_scripts
script.
hopToTop(); // Display jump to top button
/* UI Stuff */
var project_container = document.getElementById('project-container');
/* Expand images when their link points to a jpg/png/gif */
/* TODO: De-duplicate code from view post */
var page_overlay = document.getElementById('page-overlay');
$('.item-content a img').on('click', function(e){
e.preventDefault();
$(window).on("load resize",function(){
containerResizeY($(window).height());
var href = $(this).parent().attr('href');
var src = $(this).attr('src');
if (href.match("jpg$") || href.match("png$") || href.match("gif$")) {
$(page_overlay)
.addClass('active')
.html('<img src="' + src + '"/>');
} else {
window.location.href = href;
if ($(window).width() > 480) {
project_container.style.height = (window.innerHeight - project_container.offsetTop) + "px";
}
});

View File

@@ -0,0 +1,40 @@
| {% extends 'layout.html' %}
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
| {% set title = 'blog' %}
| {% block page_title %}Blog{% endblock%}
| {% block css %}
| {{ super() }}
link(href="{{ url_for('static_cloud', filename='assets/css/project-landing.css') }}", rel="stylesheet")
| {% endblock css %}
| {% block body %}
| {{ blogmacros.render_blog_index(project, posts, can_create_blog_posts, api, more_posts_available, posts_meta, pages=pages) }}
| {% endblock %}
| {% block footer_scripts %}
include ../_scripts
script.
hopToTop(); // Display jump to top button
/* Expand images when their link points to a jpg/png/gif */
/* TODO: De-duplicate code from view post */
var page_overlay = document.getElementById('page-overlay');
$('.item-content a img').on('click', function(e){
e.preventDefault();
var href = $(this).parent().attr('href');
var src = $(this).attr('src');
if (href.match("jpg$") || href.match("png$") || href.match("gif$")) {
$(page_overlay)
.addClass('active')
.html('<img src="' + src + '"/>');
} else {
window.location.href = href;
}
});
| {% endblock %}

View File

@@ -23,8 +23,60 @@ include ../../../mixins/components
i.pi-list
+card-deck(class="px-2")
| {% for child in children %}
| {% for child in children %}
| {# Browse type: List #}
a(
href="{{ url_for_node(node=child) }}",
data-node_id="{{ child._id }}",
class="js-item-open list-node-children-item browse-list")
.list-node-children-item-thumbnail
| {% if child.picture %}
img(
src="{{ child.picture.thumbnail('t', api=api)}} ")
| {% else %}
.cloud-logo
i.pi-blender-cloud
| {% endif %}
| {% if child.permissions.world %}
.list-node-children-item-ribbon
span free
| {% endif %}
.list-node-children-item-thumbnail-icon
| {% if child.properties.content_type and child.properties.content_type == 'video' %}
i.pi-play
| {% elif child.properties.content_type and child.properties.content_type == 'image' %}
i.pi-image
| {% elif child.properties.content_type and child.properties.content_type == 'file' %}
i.pi-file-archive
| {% else %}
i.pi-folder
| {% endif %}
.list-node-children-item-name {{ child.name }}
.list-node-children-item-meta
| {% if child.properties.status != 'published' %}
span.status {{ child.properties.status }}
| {% endif %}
span.type
| {% if child.properties.content_type %}
| {{ child.properties.content_type | undertitle }} ·
| {% elif child.node_type == 'group' %}
| Folder ·
| {% else %}
| {{ child.node_type | undertitle }} ·
| {% endif %}
span(title="Created on {{ child._created }}") {{ child._created | pretty_date }}
| {# Browse type: Icon #}
| {{ asset_list_item(child, current_user) }}
| {% endfor %}
| {% else %}
.list-node-children-container
@@ -63,12 +115,14 @@ include ../../../mixins/components
// Browse type: icon or list
function projectBrowseTypeIcon() {
$(".card-deck").removeClass('card-deck-vertical');
$(".list-node-children-item.browse-list").hide();
$(".list-node-children-item.browse-icon").show();
$(".js-btn-browsetoggle").html('<i class="pi-list"></i> List View');
};
function projectBrowseTypeList() {
$(".card-deck").addClass('card-deck-vertical');
$(".list-node-children-item.browse-list").show();
$(".list-node-children-item.browse-icon").hide();
$(".js-btn-browsetoggle").html('<i class="pi-layout"></i> Grid View');
};

View File

@@ -5,7 +5,9 @@
header
img.header(src="{{ node.picture.thumbnail('h', api=api) }}")
| {% endif %}
| {% block navbar_secondary %}
| {{ super() }}
| {% endblock navbar_secondary %}
#node-container
#node-overlay

View File

@@ -0,0 +1,73 @@
| {% extends 'projects/view.html' %}
| {% set title = 'blog' %}
| {% block og %}
meta(property="og:title", content="{{ node.name }}")
meta(property="og:url", content="{{ url_for('main.project_blog', project_url=project.url, url=node.properties.url, _external=True)}}")
meta(property="og:type", content="website")
| {% if node.picture %}
meta(property="og:image", content="{{ node.picture.thumbnail('l', api=api) }}")
| {% endif %}
meta(property="og:description", content="Blender Cloud is a web based service developed by Blender Institute that allows people to access the training videos and all the data from the open projects.")
meta(name="twitter:title", content="{{ node.name }}")
meta(name="twitter:description", content="Blender Cloud is a web based service developed by Blender Institute that allows people to access the training videos and all the data from the open projects.")
| {% if node.picture %}
meta(property="og:image", content="{{ node.picture.thumbnail('l', api=api) }}")
| {% endif %}
| {% endblock %}
| {% block page_title %}{{node.name}} - Blog{% endblock%}
| {% block css %}
| {{ super() }}
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
| {% endblock %}
| {% block project_context %}
| {% include 'nodes/custom/post/view_embed.html' %}
| {% endblock %}
| {% block project_tree %}
#project_tree.jstree.jstree-default.blog
ul.jstree-container-ul.jstree-children
li.jstree-node(data-node-type="page")
a.jstree-anchor(
href="{{ url_for('projects.view', project_url=project.url) }}")
| Browse Project
li.jstree-node(data-node-type="page")
a.jstree-anchor(
href="{{ url_for('main.project_blog', project_url=project.url) }}") Blog
| {% for post in posts %}
li.jstree-node
a.jstree-anchor.tree-item.post(
href="{{ url_for_node(node=post) }}",
class="{% if post._id == node._id %}jstree-clicked{% endif %}")
.tree-item-thumbnail
| {% if post.picture %}
img(src="{{ post.picture.thumbnail('s', api=api) }}")
| {% else %}
i.pi-document-text
| {% endif %}
span.tree-item-title {{ post.name }}
span.tree-item-info {{ post._created | pretty_date }}
| {% endfor %}
| {% endblock %}
| {% block footer_scripts %}
script.
ProjectUtils.setProjectAttributes({projectId: "{{project._id}}", isProject: false, nodeId: '{{node._id}}'});
/* UI Stuff */
var project_container = document.getElementById('project-container');
$(window).on("load resize",function(){
containerResizeY($(window).height());
if ($(window).width() > 480) {
project_container.style.height = (window.innerHeight - project_container.offsetTop) + "px";
}
});
| {% endblock footer_scripts %}

View File

@@ -0,0 +1,9 @@
| {% import 'nodes/custom/blog/_macros.html' as blogmacros %}
| {{ blogmacros.render_blog_post(node, project=project) }}
#comments-embed.comments-compact
.comments-list-loading
i.pi-spin
include ../_scripts

View File

@@ -18,6 +18,11 @@ meta(property="og:image", content="{{ node.picture.thumbnail('l', api=api) }}")
| {% block page_title %}{{node.name}} - Blog{% endblock%}
| {% block css %}
| {{ super() }}
link(href="{{ url_for('static_cloud', filename='assets/css/project-landing.css') }}", rel="stylesheet")
| {% endblock css %}
| {% set title = 'blog' %}
| {% block body %}

View File

@@ -68,7 +68,7 @@ script.
#stats.search-list-stats
+card-deck()(id='hits', class="h-100 m-0 pt-3 pr-2 card-deck-vertical")
+card-deck()(id='hits', class="h-100 m-0 pt-3 pr-2 card-deck-horizontal")
#search-details.border-left.search-details
#search-error

View File

@@ -1,48 +1,41 @@
include ../mixins/components
| {% macro render_secondary_navigation(project, navigation_links, title) %}
| {% if project.category == 'course' %}
| {% set category_url = url_for('cloud.courses') %}
| {% elif project.category == 'workshop' %}
| {% set category_url = url_for('cloud.workshops') %}
| {% elif project.category == 'film' %}
| {% set category_url = url_for('cloud.open_projects') %}
| {% else %}
| {% set category_url = url_for('main.homepage') %}
| {% endif %}
+nav-secondary()
| {% if project.url != 'blender-cloud' %}
li.text-capitalize
a.nav-link.text-muted.px-0(href="{{ category_url }}")
span {{ project.category }}
li.px-1
i.pi-angle-right
+nav-secondary-link(
class="px-1 font-weight-bold",
href="{{url_for('projects.view', project_url=project.url, _external=True)}}")
span {{ project.name }}
| {% endif %}
| {% if project.nodes_featured and (title !='project') %}
| {# In some cases featured_nodes might might be embedded #}
| {% if '_id' in project.nodes_featured[0] %}
| {% set featured_node_id=project.nodes_featured[0]._id %}
| {% else %}
| {% set featured_node_id=project.nodes_featured[0] %}
| {% endif %}
+nav-secondary-link(
href="{{ url_for('projects.view_node', project_url=project.url, node_id=featured_node_id) }}",
title="Explore {{ project.name }}",
class="{% if title == 'project' %}active{% endif %}")
span Explore
| {% endif %}
| {% for link in navigation_links %}
+nav-secondary-link(href="{{ link['url'] }}")
| {{ link['label'] }}
| {% endfor %}
| {% macro render_secondary_navigation(project, pages=None) %}
nav.navbar-secondary
nav.collapse.navbar-collapse
ul.navbar-nav.navbar-right
li
a.navbar-item(
href="{{ url_for('projects.view', project_url=project.url) }}",
title="{{ project.name }} Homepage")
span
b {{ project.name }}
li
a.navbar-item(
href="{{ url_for('main.project_blog', project_url=project.url) }}",
title="Project Blog",
class="{% if category == 'blog' %}active{% endif %}")
span Blog
| {% if pages %}
| {% for p in pages %}
li
a.navbar-item(
href="{{ url_for('projects.view_node', project_url=project.url, node_id=p._id) }}",
title="{{ p.name }}",
class="{% if category == 'page' %}active{% endif %}")
span {{ p.name }}
| {% endfor %}
| {% endif %}
| {% if project.nodes_featured %}
| {# In some cases featured_nodes might might be embedded #}
| {% if '_id' in project.nodes_featured[0] %}
| {% set featured_node_id=project.nodes_featured[0]._id %}
| {% else %}
| {% set featured_node_id=project.nodes_featured[0] %}
| {% endif %}
li
a.navbar-item(
href="{{ url_for('projects.view_node', project_url=project.url, node_id=featured_node_id) }}",
title="Explore {{ project.name }}",
class="{% if category == 'blog' %}active{% endif %}")
span Explore
| {% endif %}
| {% endmacro %}

View File

@@ -1,34 +1,36 @@
| {% extends 'layout.html' %}
include ../../mixins/components
//- Don't extend this base file directly. Instead, extend page.html so that Pillar extensions
//- can provide overrides.
| {% block body %}
.container.py-4
.row
.col-md-3
.container
#settings.d-flex.py-4.flex-xs-column
#settings-sidebar
| {% block settings_sidebar %}
+nav-secondary('Settings')(class="nav-secondary-vertical bg-light")
| {% block settings_sidebar_menu %}
+nav-secondary-link(
class="{% if title == 'profile' %}active{% endif %} border-top",
href="{{ url_for('settings.profile') }}")
i.pr-3.pi-vcard
span Profile
| {% endblock settings_sidebar_menu %}
| {% block settings_sidebar_menu_bottom %}
+nav-secondary-link(
class="{% if title == 'roles' %}active{% endif %}",
href="{{ url_for('settings.roles') }}")
i.pr-3.pi-cog
span Roles &amp; Capabilities
| {% endblock settings_sidebar_menu_bottom %}
.settings-header
.settings-title Settings
.settings-content
ul
| {% block settings_sidebar_menu %}
a(class="{% if title == 'profile' %}active{% endif %}",
href="{{ url_for('settings.profile') }}")
li
i.pi-vcard
| Profile
| {% endblock settings_sidebar_menu %}
| {% block settings_sidebar_menu_bottom %}
a(class="{% if title == 'roles' %}active{% endif %}",
href="{{ url_for('settings.roles') }}")
li
i.pi-cog
| Roles &amp; Capabilities
| {% endblock settings_sidebar_menu_bottom %}
| {% endblock %}
.col-md-9
h3.py-1 {% block settings_page_title %}{{ _("Title not set") }}{% endblock %}
#settings-container
.settings-header
.settings-title {% block settings_page_title %}{{ _("Title not set") }}{% endblock %}
| {% block settings_page_content %}No content set, update your template.{% endblock %}
.settings-content
| {% block settings_page_content %}No content set, update your template.{% endblock %}
| {% endblock %}

View File

@@ -21,7 +21,7 @@ style.
| {% block settings_page_content %}
.settings-form
form#settings-form(method='POST', action="{{url_for('settings.profile')}}")
.pb-3
.left
.form-group
| {{ form.username.label }}
| {{ form.username(size=20, class='form-control') }}
@@ -45,13 +45,14 @@ style.
| {{ current_user.badges_html|safe }}
p.hint-text Note that updates to these badges may take a few minutes to be visible here.
| {% endif %}
.py-3
a(href="https://gravatar.com/")
img.rounded-circle(src="{{ current_user.gravatar }}")
span.p-3 {{ _("Change Gravatar") }}
.right
.settings-avatar
a(href="https://gravatar.com/")
img(src="{{ current_user.gravatar }}")
span {{ _("Change Gravatar") }}
.py-3
button.btn.btn-outline-success.px-5.button-submit(type='submit')
i.pi-check.pr-2
.buttons
button.btn.btn-outline-success.button-submit(type='submit')
i.pi-check
| {{ _("Save Changes") }}
| {% endblock %}

View File

@@ -479,65 +479,61 @@ class TextureSortFilesTest(AbstractPillarTest):
class TaggedNodesTest(AbstractPillarTest):
def setUp(self, **kwargs):
super().setUp(**kwargs)
self.pid, _ = self.ensure_project_exists()
self.file_id, _ = self.ensure_file_exists()
self.uid = self.create_user()
from pillar.api.utils import utcnow
self.fake_now = utcnow()
def test_tagged_nodes_api(self):
from pillar.api.utils import utcnow
from datetime import timedelta
pid, _ = self.ensure_project_exists()
file_id, _ = self.ensure_file_exists()
uid = self.create_user()
now = utcnow()
base_node = {
'name': 'Just a node name',
'project': self.pid,
'project': pid,
'description': '',
'node_type': 'asset',
'user': self.uid,
'user': uid,
}
base_props = {'status': 'published',
'file': self.file_id,
'file': file_id,
'content_type': 'video',
'order': 0}
# No tags, should never be returned.
self.create_node({
'_created': self.fake_now,
'_created': now,
'properties': base_props,
**base_node})
# Empty tag list, should never be returned.
self.create_node({
'_created': self.fake_now + timedelta(seconds=1),
'_created': now + timedelta(seconds=1),
'properties': {'tags': [], **base_props},
**base_node})
# Empty string as tag, should never be returned.
self.create_node({
'_created': self.fake_now + timedelta(seconds=1),
'_created': now + timedelta(seconds=1),
'properties': {'tags': [''], **base_props},
**base_node})
nid_single_tag = self.create_node({
'_created': self.fake_now + timedelta(seconds=2),
'_created': now + timedelta(seconds=2),
# 'एनिमेशन' is 'animation' in Hindi.
'properties': {'tags': ['एनिमेशन'], **base_props},
**base_node,
})
nid_double_tag = self.create_node({
'_created': self.fake_now + timedelta(hours=3),
'_created': now + timedelta(hours=3),
'properties': {'tags': ['एनिमेशन', 'rigging'], **base_props},
**base_node,
})
nid_other_tag = self.create_node({
'_deleted': False,
'_created': self.fake_now + timedelta(days=4),
'_created': now + timedelta(days=4),
'properties': {'tags': ['producción'], **base_props},
**base_node,
})
# Matching tag but deleted node, should never be returned.
self.create_node({
'_created': self.fake_now + timedelta(seconds=1),
'_created': now + timedelta(seconds=1),
'_deleted': True,
'properties': {'tags': ['एनिमेशन'], **base_props},
**base_node})
@@ -560,76 +556,3 @@ class TaggedNodesTest(AbstractPillarTest):
with self.app.app_context():
invalid_url = flask.url_for('nodes_api.tagged', tag='')
self.get(invalid_url, expected_status=404)
def test_tagged_nodes_asset_video_with_progress_api(self):
from datetime import timedelta
from pillar.auth import current_user
base_node = {
'name': 'Spring hair rig setup',
'project': self.pid,
'description': '',
'node_type': 'asset',
'user': self.uid,
}
base_props = {'status': 'published',
'file': self.file_id,
'content_type': 'video',
'order': 0}
# Create one node of type asset video
nid_single_tag = self.create_node({
'_created': self.fake_now + timedelta(seconds=2),
# 'एनिमेशन' is 'animation' in Hindi.
'properties': {'tags': ['एनिमेशन'], **base_props},
**base_node,
})
# Create another node
self.create_node({
'_created': self.fake_now + timedelta(seconds=2),
# 'एनिमेशन' is 'animation' in Hindi.
'properties': {'tags': ['एनिमेशन'], **base_props},
**base_node,
})
# Add video watch progress for the self.uid user
with self.app.app_context():
users_coll = self.app.db('users')
# Define video progress
progress_in_sec = 333
video_progress = {
'progress_in_sec': progress_in_sec,
'progress_in_percent': 70,
'done': False,
'last_watched': self.fake_now + timedelta(seconds=2),
}
users_coll.update_one(
{'_id': self.uid},
{'$set': {f'nodes.view_progress.{nid_single_tag}': video_progress}})
# Utility to fetch tagged nodes and return them as JSON list
def do_query():
animation_tags_url = flask.url_for('nodes_api.tagged', tag='एनिमेशन')
return self.get(animation_tags_url).json
# Ensure that anonymous users get videos with no view_progress info
with self.app.app_context():
resp = do_query()
for node in resp:
self.assertNotIn('view_progress', node)
# Ensure that an authenticated user gets view_progress info if the video was watched
with self.login_as(self.uid):
resp = do_query()
for node in resp:
if node['_id'] in current_user.nodes['view_progress']:
self.assertIn('view_progress', node)
self.assertEqual(progress_in_sec, node['view_progress']['progress_in_sec'])
# Ensure that another user with no view progress does not get any view progress info
other_user = self.create_user(user_id=ObjectId())
with self.login_as(other_user):
resp = do_query()
for node in resp:
self.assertNotIn('view_progress', node)