remove unneeded Ox. prefix from path and file names

This commit is contained in:
rolux 2014-09-26 15:51:50 +02:00
commit 51696562f1
1365 changed files with 43 additions and 43 deletions

View file

@ -0,0 +1,734 @@
'use strict';
/*@
Ox.AnnotationFolder <f> AnnotationFolder Object
options <o> Options object
editable <b|false> If true, annotations can be added
id <s> id
items <a|[]> items
title <s> title
type <s|'text'> panel type
width <n|0>
self <o> Shared private variable
([options[, self]]) -> <o:Ox.CollapsePanel> AnnotationFolder Object
add <!> add
blur <!> blur
change <!> change
edit <!> edit
info <!> info
insert <!> insert
key_* <!> key_*
open <!> open
remove <!> remove
selectnext <!> selectnext
selectprevious <!> selectprevious
selectnone <!> selectnone
select <!> select
submit <!> submit
togglelayer <!> togglelayer
togglewidget <!> togglewidget
@*/
Ox.AnnotationFolder = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
clickLink: null,
collapsed: false,
editable: false,
highlight: '',
id: '',
'in': 0,
item: '',
items: [],
keyboard: '',
languages: 'all',
out: 0,
position: 0,
range: 'all',
selected: '',
showInfo: false,
showWidget: false,
sort: 'position',
title: '',
type: 'text',
users: 'all',
widgetSize: 256,
width: 0
})
.options(options || {})
.update(function(key, value) {
if (key == 'highlight') {
self.$annotations.options({highlight: value});
}
if (['in', 'out'].indexOf(key) > -1 && self.editing) {
var item = Ox.getObjectById(self.options.items, self.options.selected);
item[key] = value;
item.duration = self.options.out - self.options['in'];
self.points = getPoints();
}
if (key == 'in') {
//fixme: array editable should support item updates while editing
self.options.range == 'selection' && updateAnnotations();
} else if (key == 'out') {
self.options.range == 'selection' && updateAnnotations();
} else if (key == 'position') {
if (self.options.range == 'position') {
crossesPoint() && updateAnnotations();
self.position = self.options.position;
}
} else if (key == 'languages') {
updateAnnotations();
} else if (key == 'range') {
updateAnnotations();
self.$annotations.options({placeholder: getPlaceholder()});
} else if (key == 'selected') {
if (value === '') {
self.editing = false;
}
if (value && self.options.collapsed) {
self.$panel.options({animate: false});
self.$panel.options({collapsed: false});
self.$panel.options({animate: true});
}
self.$annotations.options({selected: value});
} else if (key == 'sort') {
self.sort = getSort();
self.$annotations.options({sort: self.sort});
showWarnings();
} else if (key == 'users') {
updateAnnotations();
} else if (key == 'width') {
if (self.widget) {
self.$outer.options({width: self.options.width});
self.$inner.options({width: self.options.width});
self.$widget.options({width: self.options.width});
}
self.$annotations.options({
width: self.options.type == 'text'
? self.options.width + 8
: self.options.width
});
}
});
if (self.options.selected) {
self.options.collapsed = false;
}
self.annotations = getAnnotations();
self.points = getPoints();
self.position = self.options.position;
self.sort = getSort();
self.widget = self.options.type == 'event' ? 'Calendar'
: self.options.type == 'place' ? 'Map' : '';
self.$addButton = Ox.Button({
id: 'add',
style: 'symbol',
title: 'add',
tooltip: Ox._('Add {0}', [self.options.item])
+ (self.options.keyboard ? ' [' + self.options.keyboard + ']' : ''),
type: 'image'
})
.bindEvent({
click: function() {
that.triggerEvent('add', {value: ''});
}
});
self.$infoButton = Ox.Button({
style: 'symbol',
title: 'info',
type: 'image'
})
.bindEvent({
click: function() {
that.triggerEvent('info');
}
});
self.$panel = Ox.CollapsePanel({
collapsed: self.options.collapsed,
extras: [self.options.editable ? self.$addButton : self.$infoButton],
size: 16,
title: self.options.title
})
.addClass('OxAnnotationFolder')
.bindEvent({
toggle: toggleLayer
});
that.setElement(self.$panel);
that.$content = self.$panel.$content;
if (self.widget) {
self.widgetSize = self.options.showWidget * self.options.widgetSize;
self.$outer = Ox.Element()
.css({
display: 'table-cell',
width: self.options.width + 'px'
})
.appendTo(that.$content);
self.$inner = Ox.Element()
.css({
height: self.widgetSize + 'px',
overflow: 'hidden'
})
.appendTo(self.$outer);
if (options.type == 'event') {
self.$widget = self.$calendar = Ox.Calendar({
events: getEvents(),
height: self.widgetSize,
showZoombar: true,
width: self.options.width,
zoomOnlyWhenFocused: true
})
.css({
width: self.options.width + 'px',
height: self.widgetSize + 'px'
})
.bindEvent({
select: function(data) {
if (
!data.id && self.options.selected
&& isDefined(Ox.getObjectById(self.options.items, self.options.selected))
) {
// only deselect annotation if the event deselect was not
// caused by switching to an annotation without event
self.$annotations.options({selected: ''});
} else if (
data.annotationIds
&& data.annotationIds.indexOf(self.options.selected) == -1
) {
// only select a new annotation if the currently selected
// annotation does not match the selected event
self.$annotations.options({selected: data.annotationIds[0]});
}
}
})
.appendTo(self.$inner);
} else { // place
self.$widget = self.$map = Ox.Map({
places: getPlaces(),
showTypes: true,
// FIXME: should be showZoombar
zoombar: true,
zoomOnlyWhenFocused: true
// showLabels: true
})
.css({
width: self.options.width + 'px',
height: self.widgetSize + 'px'
})
.bindEvent({
// FIXME: duplicated!
select: function(data) {
if (
!data.id && self.options.selected
&& isDefined(Ox.getObjectById(self.options.items, self.options.selected))
) {
// only deselect annotation if the place deselect was not
// caused by switching to an annotation without place
self.$annotations.options({selected: ''});
} else if (
data.annotationIds
&& data.annotationIds.indexOf(self.options.selected) == -1
) {
// only select a new annotation if the currently selected
// annotation does not match the selected place
self.$annotations.options({selected: data.annotationIds[0]});
}
}
})
.appendTo(self.$inner);
}
self.$resizebar = Ox.Element({
tooltip: Ox._('Drag to resize or click to toggle map') // fixme: update as w/ splitpanels
})
.addClass('OxResizebar OxHorizontal')
.css({
position: 'absolute',
top: self.widgetSize + 'px',
cursor: 'ns-resize'
})
.append($('<div>').addClass('OxSpace'))
.append($('<div>').addClass('OxLine'))
.append($('<div>').addClass('OxSpace'))
.bindEvent({
anyclick: toggleWidget,
dragstart: dragstart,
drag: drag,
dragend: dragend
})
.appendTo(self.$outer);
}
self.$annotations = Ox.ArrayEditable({
clickLink: self.options.clickLink,
editable: self.options.editable,
getSortValue: self.options.type == 'text'
? function(value) {
return Ox.stripTags(value);
}
: null,
globalAttributes: ['lang'],
highlight: self.options.highlight,
placeholder: Ox._('Loading...'),
separator: ';',
sort: self.sort,
submitOnBlur: false,
tooltipText: self.options.showInfo ? function(item) {
return Ox.encodeHTMLEntities(item.user) + ', '
+ Ox.formatDate(item.modified.slice(0, 10), '%B %e, %Y');
} : '',
width: self.options.width,
maxHeight: self.options.type == 'text' ? Infinity : void 0,
type: self.options.type == 'text' ? 'textarea' : 'input'
})
.bindEvent({
add: function(data) {
if (self.editing) {
// FIXME: changed value will not be saved!
}
that.triggerEvent('add', {value: data.value || ''});
},
blur: function() {
// the video editor will, if it has not received focus,
// call blurItem
that.triggerEvent('blur');
},
change: changeAnnotation,
'delete': removeAnnotation,
edit: function() {
self.editing = true;
that.triggerEvent('edit');
},
insert: function(data) {
that.triggerEvent('insert', data);
},
open: function() {
that.triggerEvent('open');
},
select: selectAnnotation,
selectnext: function() {
that.triggerEvent('selectnext');
},
selectprevious: function() {
that.triggerEvent('selectprevious');
},
selectnone: function() {
that.triggerEvent('selectnone');
},
submit: submitAnnotation
})
.appendTo(
['event', 'place'].indexOf(self.options.type) > -1
? self.$outer : that.$content
);
setTimeout(function() {
self.$annotations.options({
items: self.annotations,
placeholder: getPlaceholder(),
selected: self.options.selected
});
});
[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'b', 'backslash', 'closebracket', 'comma', 'dot',
'equal', 'f', 'g', 'h', 'i', 'minus', 'n', 'o',
'openbracket', 'p', 'shift_0', 'shift_equal',
'shift_g', 'shift_i', 'shift_minus', 'shift_o',
'slash', 'space'
].forEach(function(key) {
key = 'key_' + key;
self.$annotations.bindEvent(key, function() {
that.triggerEvent(key);
});
});
self.options.selected && setTimeout(function() {
// need timeout in order to trigger events
if (self.options.collapsed) {
self.$panel.options({collapsed: false});
}
selectAnnotation({id: self.options.selected});
}, 0);
showWarnings();
function changeAnnotation(data) {
var item = Ox.getObjectById(self.options.items, data.id);
item.value = data.value;
that.triggerEvent('change', item);
}
function crossesPoint() {
var positions = [self.position, self.options.position].sort();
return self.points.some(function(point) {
return point >= positions[0] && point <= positions[1];
});
}
function dragstart() {
if (self.options.showWidget) {
Ox.$body.addClass('OxDragging');
self.drag = {
startSize: self.options.widgetSize
};
}
}
function drag(e) {
if (self.options.showWidget) {
self.options.widgetSize = Ox.limit(
self.drag.startSize + e.clientDY, 128, 384
);
if (self.options.widgetSize >= 248 && self.options.widgetSize <= 264) {
self.options.widgetSize = 256;
}
self.$resizebar.css({top: self.options.widgetSize + 'px'});
self.$inner.css({height: self.options.widgetSize + 'px'});
self.$widget.options({height: self.options.widgetSize});
}
}
function dragend(e) {
if (self.options.showWidget) {
Ox.$body.removeClass('OxDragging');
self.options.type == 'event'
? self.$calendar.resizeCalendar()
: self.$map.resizeMap();
that.triggerEvent('resizewidget', {size: self.options.widgetSize});
}
}
function getAnnotations() {
return Ox.filter(self.options.items, function(item) {
return self.editing && item.id == self.options.selected || (
(
self.options.range == 'all' || (
self.options.range == 'selection'
&& item['in'] <= self.options.out
&& item.out >= self.options['in']
) || (
self.options.range == 'position'
&& item['in'] <= self.options.position
&& item.out >= self.options.position
)
) && (
self.options.languages == 'all'
|| self.options.languages.some(function(language) {
return item.languages.indexOf(language) > -1;
})
) && (
self.options.users == 'all'
|| self.options.users.indexOf(item.user) > -1
)
);
});
}
function getEvents() {
var events = [];
self.annotations.filter(function(item) {
return isDefined(item);
}).forEach(function(item) {
var index = Ox.getIndexById(events, item.event.id);
if (index == -1) {
events.push(Ox.extend({
annotationIds: [item.id]
}, item.event))
} else {
events[index].annotationIds.push(item.id);
}
});
return events;
}
function getPlaceholder() {
return 'No ' + self.options.title.toLowerCase() + (
self.options.range == 'position' ? ' at current position'
: self.options.range == 'selection' ? ' in current selection'
: ''
);
}
function getPlaces() {
var places = [];
self.annotations.filter(function(item) {
return isDefined(item);
}).forEach(function(item) {
var index = Ox.getIndexById(places, item.place.id);
if (index == -1) {
places.push(Ox.extend({
annotationIds: [item.id]
}, item.place));
} else {
places[index].annotationIds.push(item.id);
}
});
return places;
}
function getPoints() {
return Ox.unique(Ox.flatten(
self.options.items.map(function(item) {
return [item['in'], item.out];
})
));
}
function getSort() {
return ({
duration: ['-duration', '+in', self.options.type == 'text' ? '+created' : '+value'],
position: ['+in', '+duration', self.options.type == 'text' ? '+created' : '+value'],
text: ['+value', '+in', '+duration']
})[self.options.sort];
}
function isDefined(item) {
return !!item[self.options.type]
&& !!item[self.options.type].type;
}
function removeAnnotation(data) {
var item;
self.editing = false;
if (self.widget) {
item = Ox.getObjectById(self.options.items, data.id);
if (isDefined(item)) {
if (self.options.type == 'event') {
self.$calendar.options({selected: ''})
.options({events: getEvents()});
} else {
self.$map.options({selected: ''})
.options({places: getPlaces()});
}
}
}
showWarnings();
that.triggerEvent('remove', {id: data.id});
}
function selectAnnotation(data) {
var item = Ox.getObjectById(self.options.items, data.id);
self.options.selected = item ? data.id : '';
if (self.widget) {
if (self.options.type == 'event') {
self.$calendar.options({
selected: item && isDefined(item) ? item.event.id : ''
})
.panToEvent();
} else {
self.$map.options({
selected: item && isDefined(item) ? item.place.id : ''
})
.panToPlace();
}
}
that.triggerEvent('select', Ox.extend(data, item ? {
'in': item['in'],
out: item.out,
layer: self.options.id
} : {}));
}
function showWarnings() {
if (self.widget && self.options.items.length) {
self.$annotations.find('.OxEditableElement').each(function() {
var $element = $(this);
// We don't want to catch an eventual placeholder,
// which is an EditableElement without .data('id')
if (
$element.data('id')
&& !isDefined(
Ox.getObjectById(self.options.items, $element.data('id'))
)
) {
$element.addClass('OxWarning');
} else {
$element.removeClass('OxWarning');
}
});
}
}
function submitAnnotation(data) {
var item = Ox.getObjectById(self.options.items, data.id);
item.value = data.value;
self.editing = false;
self.options.sort == 'text' && self.$annotations.reloadItems();
that.triggerEvent('submit', item);
}
function toggleLayer() {
self.options.collapsed = !self.options.collapsed;
if (
!self.options.collapsed
&& self.options.type == 'place'
&& self.options.showWidget
) {
self.$map.resizeMap();
}
if (self.options.collapsed) {
self.editing && that.blurItem();
self.$annotations.loseFocus();
}
that.triggerEvent('togglelayer', {collapsed: self.options.collapsed});
}
function toggleWidget() {
self.options.showWidget = !self.options.showWidget;
self.widgetSize = self.options.showWidget * self.options.widgetSize;
self.$resizebar.animate({top: self.widgetSize + 'px'}, 250);
self.$inner.animate({height: self.widgetSize + 'px'}, 250);
self.$widget.animate({height: self.widgetSize + 'px'}, 250, function() {
self.$widget.options({height: self.widgetSize});
});
that.triggerEvent('togglewidget', {collapsed: !self.options.showWidget});
}
function updateAnnotations() {
self.annotations = getAnnotations();
self.$annotations.options({items: self.annotations});
showWarnings();
if (self.widget) {
self.options.type == 'event'
? self.$calendar.options({events: getEvents()})
: self.$map.options({places: getPlaces()});
}
}
/*@
addItem <f> addItem
@*/
that.addItem = function(item) {
var pos = 0;
self.options.items.splice(pos, 0, item);
self.$panel.options({collapsed: false});
self.$annotations
.addItem(pos, item)
.options({selected: item.id})
.editItem();
showWarnings();
self.points = getPoints();
return that;
};
/*@
blurItem <f> blur item
() -> <o> blur selected item
@*/
that.blurItem = function() {
self.editing = false;
self.$annotations.blurItem();
return that;
};
/*@
editItem <f> edit item
() -> <o> edit selected item
@*/
that.editItem = function() {
self.editing = true;
self.$panel.options({collapsed: false});
self.$annotations.editItem();
return that;
};
/*@
gainFocus <f> gain focus
() -> <o> gain focus
@*/
that.gainFocus = function() {
self.$annotations.gainFocus();
return that;
};
that.getCurrentAnnotations = function() {
return getAnnotations();
};
/*@
removeItem <f> remove item
() -> <o> remove selected item
@*/
that.removeItem = function() {
self.$annotations.removeItem();
};
/*@
selectItem <f> select item
(position) -> <o> select item at position
@*/
that.selectItem = function(position) {
// selects the first (0) or last (-1) visible annotation
if (self.annotations.length) {
that.options({selected: self.annotations[
position == 0 ? 0 : self.annotations.length - 1
].id});
self.$annotations.gainFocus();
} else {
that.triggerEvent(
position == 0 ? 'selectnext' : 'selectprevious'
);
}
};
/*@
updateItem <f> update item
(id, data) -> <o> update item
@*/
that.updateItem = function(id, data) {
var item = Ox.getObjectById(self.options.items, id);
Ox.forEach(data, function(value, key) {
item[key] = value;
});
if (id != item.id) {
self.$annotations.find('.OxEditableElement').each(function() {
var $element = $(this);
if ($element.data('id') == self.options.selected) {
$element.data({id: item.id});
}
});
self.options.selected = item.id;
}
if (self.$widget) {
// update may have made the item match,
// or no longer match, an event or place
if (isDefined(item)) {
self.$widget.options(
self.options.type == 'event'
? {events: getEvents()}
: {places: getPlaces()}
)
.options({
selected: item[self.options.type].id
});
self.$widget[
self.options.type == 'event' ? 'panToEvent' : 'panToPlace'
]();
} else {
self.$widget.options({
selected: ''
})
.options(
self.options.type == 'event'
? {events: getEvents()}
: {places: getPlaces()}
);
}
}
if (id != item.id) {
self.$annotations.options({selected: self.options.selected});
}
showWarnings();
return that;
};
return that;
};

View file

@ -0,0 +1,728 @@
'use strict';
// FIXME: should be Ox.AnnotationFolders
/*@
Ox.AnnotationPanel <f> Video Annotation Panel
options <o> Options object
calendarSize <n|256> calendar size
clickLink <f|null> click link callback
editable <b|false> if true, annotations can be edited
highlight <s|''> highlight given string in annotations
layers <a|[]> array with annotation objects
mapSize <n|256> map size
range <s|'all'> all, position, selection
selected <s|''> selected annotation
showCalendar <b|false> if true, calendar is shown
showLayers <o|{}> object with layers to show
showMap <b|false> if true, show map
showUsers <b|false> if true show user
sort <s|'position'> position, start, text
width <n|256> panel width
self <o> Shared private variable
([options[, self]]) -> <o:Ox.SplitPanel> AnnotationPanel Object
add <!> add
annotationsID <!> annotationsID
blur <!> blur
change <!> change
define <!> define
edit <!> edit
findannotations <!> findannotations
find <!> find
focus <!> focus
info <!> info
open <!> open
remove <!> remove
resize <!> resize
submit <!> submit
togglelayer <!> togglelayer
toggle* <!> toggle*
@*/
Ox.AnnotationPanel = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
calendarSize: 256,
clickLink: null,
editable: false,
enableExport: false,
enableImport: false,
highlight: '',
itemName: {singular: 'video', plural: 'videos'},
layers: [],
mapSize: 256,
range: 'all',
selected: '',
showCalendar: false,
showLayers: {},
showMap: false,
showUsers: false,
sort: 'position',
width: 256
})
.options(options || {})
.update(function(key, value) {
if (key == 'highlight') {
self.$folder.forEach(function($folder) {
$folder.options({highlight: value});
});
} else if (['in', 'out', 'position'].indexOf(key) > -1) {
self.$folder.forEach(function($folder) {
$folder.options(key, value);
});
} else if (key == 'layers') {
renderFolders();
} else if (key == 'selected') {
self.options.editable && updateEditMenu();
if (value) {
getFolder(value).options({selected: value});
} else {
self.$folder.forEach(function($folder) {
$folder.options({selected: ''});
});
}
} else if (key == 'width') {
self.$folder.forEach(function($folder) {
$folder.options({width: self.options.width - Ox.UI.SCROLLBAR_SIZE});
});
}
})
.addClass('OxAnnotationPanel');
self.editing = false;
self.languages = getLanguages();
self.enabledLanguages = self.languages.map(function(language) {
return language.code;
});
if (self.options.showUsers) {
self.users = getUsers();
self.enabledUsers = self.users;
} else {
self.enabledUsers = 'all';
}
self.$menubar = Ox.Bar({
size: 16
})
.addClass('OxVideoPlayer')
.bindEvent({
doubleclick: function(e) {
if ($(e.target).is('.OxBar')) {
self.$folders.animate({scrollTop: 0}, 250);
}
}
});
self.$folders = Ox.Element().css({overflowY: 'scroll'});
self.$folder = [];
renderFolders();
renderOptionsMenu();
self.options.editable && renderEditMenu();
that.setElement(
self.$panel = Ox.SplitPanel({
elements: [
{
element: self.$menubar,
size: 16
},
{
element: self.$folders
}
],
orientation: 'vertical'
})
);
self.options.selected && getFolder(self.options.selected) && scrollToSelected(
getFolder(self.options.selected).options('type')
);
function getAnnotation(annotationId) {
var found = false, annotation;
Ox.forEach(self.options.layers, function(layer, i) {
Ox.forEach(layer.items, function(item) {
if (item.id == annotationId) {
annotation = item;
found = true;
return false; // break
}
});
if (found) {
return false; // break
}
});
return annotation;
}
function getFolder(annotationId) {
var found = false, folder;
Ox.forEach(self.options.layers, function(layer, i) {
Ox.forEach(layer.items, function(item) {
if (item.id == annotationId) {
folder = self.$folder[i];
found = true;
return false; // break
}
});
if (found) {
return false; // break
}
});
return folder;
}
function getLanguages() {
return Ox.sortBy(Ox.map(Ox.unique(Ox.flatten(
self.options.layers.map(function(layer) {
return layer.items.map(function(item) {
return item.languages;
});
})
)), function(language) {
return {
code: language,
name: Ox.getLanguageNameByCode(language)
};
}), 'name');
}
function getUsers() {
return Ox.sort(Ox.unique(Ox.flatten(
self.options.layers.map(function(layer) {
return layer.items.map(function(item) {
return item.user;
});
})
)));
}
function insert(data) {
var id = data.id;
Ox.InsertHTMLDialog(Ox.extend({
callback: function(data) {
Ox.$elements[id]
.value(data.value)
.focusInput(data.position)
.triggerEvent('change', data.value);
}
}, data)).open();
}
function renderEditMenu() {
var annotation, annotationTitle, folder, hasManualCalendarOrMap,
isDefined, isEditable, isEvent, isEventOrPlace, isPlace, isString,
key, manageTitle, type, value;
if (self.options.selected) {
annotation = getAnnotation(self.options.selected);
folder = getFolder(self.options.selected);
if (annotation && folder) {
key = folder.options('id');
type = folder.options('type');
value = annotation.value;
isEditable = annotation.editable;
isEvent = type == 'event';
isPlace = type == 'place';
isEventOrPlace = isEvent || isPlace;
isString = type != 'text';
// fixme: absence of annotation[type] may be an error
isDefined = isEventOrPlace && !!annotation[type] && !!annotation[type].type;
annotationTitle = folder.options('item') + ': "' + value + '"';
}
}
hasManualCalendarOrMap = self.options.layers.some(function(layer) {
return layer.type == 'event' || layer.type == 'place';
});
manageTitle = Ox._((isDefined ? 'Edit' : 'Define') + ' '
+ (isPlace ? 'Place' : isEvent ? 'Event' : 'Place or Event') + '...');
self.$editMenuButton && self.$editMenuButton.remove();
self.$editMenuButton = Ox.MenuButton({
items: [].concat(
self.options.layers.map(function(layer, i) {
return {id: 'add' + layer.id, title: Ox._('Add {0}', [layer.item]), keyboard: i + 1 + ''}
}),
[
{},
{id: 'deselect', title: Ox._('Deselect Annotation'), disabled: !self.options.selected || self.editing, keyboard: 'escape'},
{id: 'edit', title: Ox._('Edit Annotation'), disabled: !self.options.selected || !isEditable || self.editing, keyboard: 'return'},
{id: 'delete', title: Ox._('Delete Annotation'), disabled: !self.options.selected || !isEditable, keyboard: 'delete'},
{},
{id: 'insert', title: Ox._('Insert...'), disabled: isString || !self.editing, keyboard: 'control i'},
{id: 'undo', title: Ox._('Undo Changes'), disabled: !self.editing, keyboard: 'escape'},
{id: 'save', title: Ox._('Save Changes'), disabled: !self.editing, keyboard: isString ? 'return' : 'shift return'},
],
hasManualCalendarOrMap ? [
{},
{id: 'manage', title: manageTitle, disabled: !self.options.selected || !isEventOrPlace},
] : [],
isString ? [
{},
{id: 'annotation', title: annotationTitle, disabled: true},
{id: 'find', title: Ox._('Find in This {0}', [Ox.toTitleCase(self.options.itemName.singular)])},
{id: 'findannotations', title: Ox._('Find in All {0}', [Ox.toTitleCase(self.options.itemName.plural)])}
] : [],
[
{},
{id: 'import', title: Ox._('Import Annotations...'), disabled: !self.options.enableImport},
{id: 'export', title: Ox._('Export Annotations...'), disabled: !self.options.enableExport},
]
),
maxWidth: 256,
style: 'square',
title: 'edit',
tooltip: Ox._('Editing Options'),
type: 'image'
})
.css({float: 'right'})
.bindEvent({
click: function(data) {
if (Ox.startsWith(data.id, 'add')) {
that.triggerEvent('add', {layer: data.id.slice(3), value: ''});
} else if (data.id == 'delete') {
getFolder(self.options.selected).removeItem();
} else if (data.id == 'deselect') {
getFolder(self.options.selected).options({selected: ''});
} else if (data.id == 'edit') {
getFolder(self.options.selected).editItem();
} else if (data.id == 'export') {
that.triggerEvent('exportannotations');
} else if (data.id == 'find') {
that.triggerEvent('find', {value: value});
} else if (data.id == 'findannotations') {
that.triggerEvent('findannotations', {key: key, value: value});
} else if (data.id == 'import') {
that.triggerEvent('importannotations');
} else if (data.id == 'insert') {
var id = $('.OxEditableElement div.OxInput').data('oxid'),
element = $('.OxEditableElement textarea.OxInput')[0];
insert({
end: element.selectionEnd,
id: id,
selection: element.value.slice(
element.selectionStart, element.selectionEnd
),
start: element.selectionStart,
value: element.value
});
} else if (data.id == 'manage') {
that.triggerEvent('define', {
id: getAnnotation(self.options.selected)[type].id,
type: type
});
} else if (data.id == 'save') {
// ...
} else if (data.id == 'undo') {
// ...
}
},
hide: function() {
self.options.selected
? getFolder(self.options.selected).gainFocus()
: that.triggerEvent('focus');
}
})
.appendTo(self.$menubar);
}
function renderFolder(layer) {
var index = Ox.getIndexById(self.options.layers, layer.id),
item = Ox.getObjectById(layer.items, self.options.selected),
selected = item ? item.id : '';
self.$folder[index] = Ox.AnnotationFolder(
Ox.extend({
clickLink: self.options.clickLink,
collapsed: !self.options.showLayers[layer.id],
editable: self.options.editable,
highlight: self.options.highlight,
'in': self.options['in'],
keyboard: index + 1 + '',
out: self.options.out,
position: self.options.position,
range: self.options.range,
selected: selected,
sort: self.options.sort,
width: self.options.width - Ox.UI.SCROLLBAR_SIZE
}, layer, layer.type == 'event' ? {
showWidget: self.options.showCalendar,
widgetSize: self.options.calendarSize
} : layer.type == 'place' ? {
showWidget: self.options.showMap,
widgetSize: self.options.mapSize
} : {})
)
.bindEvent({
add: function(data) {
that.triggerEvent('add', Ox.extend({layer: layer.id}, data));
},
blur: function() {
that.triggerEvent('blur');
},
change: function(data) {
that.triggerEvent('change', Ox.extend({layer: layer.id}, data));
},
edit: function() {
self.editing = true;
renderEditMenu();
that.triggerEvent('edit');
},
info: function(data) {
that.triggerEvent('info', {layer: layer.id});
},
insert: insert,
open: function() {
that.triggerEvent('open');
},
remove: function(data) {
that.triggerEvent('remove', Ox.extend({layer: layer.id}, data));
},
resizewidget: function(data) {
that.triggerEvent('resize' + (
layer.type == 'event' ? 'calendar' : 'map'
), data);
},
select: function(data) {
selectAnnotation(data, index);
},
selectnext: function() {
selectNext(layer.id, 1);
},
selectprevious: function() {
selectNext(layer.id, -1);
},
selectnone: selectNone,
submit: function(data) {
that.triggerEvent('submit', Ox.extend({layer: layer.id}, data));
},
togglelayer: function(data) {
self.options.showLayers[layer.id] = !data.collapsed;
that.triggerEvent('togglelayer', Ox.extend({layer: layer.id}, data));
},
togglewidget: function(data) {
that.triggerEvent('toggle' + (
layer.type == 'event' ? 'calendar' : 'map'
), data);
}
});
[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'b', 'backslash', 'closebracket', 'comma', 'dot',
'equal', 'f', 'g', 'h', 'i', 'minus', 'n', 'o',
'openbracket', 'p', 'shift_0', 'shift_equal',
'shift_g', 'shift_i', 'shift_minus', 'shift_o',
'slash', 'space'
].forEach(function(key) {
key = 'key_' + key;
self.$folder[index].bindEvent(key, function() {
that.triggerEvent(key);
});
});
self.$folder[index].appendTo(self.$folders);
}
function renderFolders() {
self.$folders.empty();
self.options.layers.forEach(function(layer, index) {
renderFolder(layer);
});
}
function renderOptionsMenu() {
self.$optionsMenuButton && self.$optionsMenuButton.remove();
self.$optionsMenuButton = Ox.MenuButton({
items: [].concat(
[
{id: 'showannotations', title: Ox._('Show Annotations'), disabled: true},
{group: 'range', min: 1, max: 1, items: [
{id: 'all', title: Ox._('All'), checked: self.options.range == 'all'},
{id: 'selection', title: Ox._('In Current Selection'), checked: self.options.range == 'selection'},
{id: 'position', title: Ox._('At Current Position'), checked: self.options.range == 'position'}
]},
{},
{id: 'sortannotations', title: Ox._('Sort Annotations'), disabled: true},
{group: 'sort', min: 1, max: 1, items: [
{id: 'position', title: Ox._('By Position'), checked: self.options.sort == 'position'},
{id: 'duration', title: Ox._('By Duration'), checked: self.options.sort == 'duration'},
{id: 'text', title: Ox._('By Text'), checked: self.options.sort == 'text'}
]}
],
self.languages.length > 1 ? [
{},
{id: 'languages', title: Ox._('Show Languages'), disabled: true},
{group: 'languages', min: 1, max: -1, items: self.languages.map(function(language) {
return {id: language.code, title: Ox._(language.name), checked:
self.enabledLanguages.indexOf(language.code) > -1
};
})}
] : [],
self.options.showUsers && self.users.length ? [
{},
{id: 'users', title: Ox._('Show Users'), disabled: true},
{group: 'users', min: 1, max: -1, items: self.users.map(function(user) {
return {id: user, title: Ox.encodeHTMLEntities(user), checked:
self.enabledUsers == 'all' || self.enabledUsers.indexOf(user) > -1
};
})}
] : []
),
style: 'square',
title: 'set',
tooltip: Ox._('Options'),
type: 'image'
})
.css({float: 'left'})
.bindEvent({
change: function(data) {
var set = {};
if (data.id == 'languages') {
self.enabledLanguages = data.checked.map(function(checked) {
return checked.id;
});
self.$folder.forEach(function($folder) {
$folder.options({languages: self.enabledLanguages});
});
} else if (data.id == 'users') {
self.enabledUsers = data.checked.map(function(checked) {
return checked.id;
});
self.$folder.forEach(function($folder) {
$folder.options({users: self.enabledUsers});
});
} else {
self.options[data.id] = data.checked[0].id;
set[data.id] = self.options[data.id];
self.$folder.forEach(function($folder) {
$folder.options(set);
});
that.triggerEvent('annotations' + data.id, set);
}
},
hide: function() {
self.options.selected
? getFolder(self.options.selected).gainFocus()
: that.triggerEvent('focus');
}
})
.appendTo(self.$menubar);
}
function scrollToSelected(type) {
var $item = that.find('.OxEditableElement.OxSelected'),
itemHeight = $item.height() + (type == 'text' ? 8 : 0),
itemTop = ($item.offset() || {}).top,
itemBottom = itemTop + itemHeight,
height = self.$folders.height(),
scrollTop = self.$folders.scrollTop(),
top = self.$folders.offset().top;
if (itemTop < top || itemBottom > top + height) {
if (itemTop < top) {
scrollTop += itemTop - top;
} else {
scrollTop += itemBottom - top - height;
}
self.$folders.animate({
scrollTop: scrollTop + 'px'
}, 0);
}
}
function selectAnnotation(data, index) {
if (data.id) {
Ox.forEach(self.$folder, function($folder, i) {
if (i != index && $folder.options('selected')) {
self.deselecting = true;
$folder.options({selected: ''});
self.deselecting = false;
return false; // break
}
});
scrollToSelected(self.options.layers[index].type);
}
if (!self.deselecting) {
self.options.selected = data.id;
self.options.editable && renderEditMenu();
that.triggerEvent('select', data);
}
}
function selectNone() {
if (self.options.selected) {
getFolder(self.options.selected).options({selected: ''});
}
}
function selectNext(layer, direction) {
var index = Ox.mod(
Ox.getIndexById(self.options.layers, layer) + direction,
self.options.layers.length
);
self.$folder[index].selectItem(direction == 1 ? 0 : -1);
}
function updateEditMenu() {
var action = self.options.selected ? 'enableItem' : 'disableItem';
self.$editMenuButton[action]('edit');
self.$editMenuButton[action]('delete');
}
function updateLanguages() {
var languages = self.languages,
enabledLanguages = self.enabledLanguages;
self.languages = getLanguages();
self.enabledLanguages = self.languages.filter(function(language) {
// enabled if it was enabled, was just added, or is the only
// language
return Ox.contains(enabledLanguages, language)
|| !Ox.contains(languages, language)
|| self.languages.length == 1;
}).map(function(language) {
return language.code;
});
if (
self.languages.length == 1
&& !Ox.contains(enabledLanguages, self.languages[0].code)
) {
// last remaining language was enabled by removing all other
// languages
self.$folder.forEach(function($folder) {
$folder.options({languages: self.enabledLanguages});
});
}
}
/*@
addItem <f> add item
(layer, item) -> <o> AnnotationPanel
@*/
that.addItem = function(layer, item) {
// called from addannotation callback
var i = Ox.getIndexById(self.options.layers, layer);
self.$folder[i].addItem(item);
updateLanguages();
self.users = getUsers();
if (self.enabledUsers != 'all' && self.enabledUsers.indexOf(item.user) == -1) {
self.enabledUsers.push(item.user);
self.$folder[i].options({users: self.enabledUsers});
}
renderOptionsMenu();
renderEditMenu();
return that;
};
/*@
addLayer <f> Add a layer
(layer[, index]) -> <o> AnnotationPanel
@*/
that.addLayer = function(layer, index) {
// FIXME: add/remove/updateLayer don't update users yet
index = index || self.options.layers.length;
self.options.layers.splice(index, 0, layer);
renderFolders();
return that;
};
/*@
blurItem <f> Blur selected item
() -> <o> AnnotationPanel
@*/
that.blurItem = function() {
self.editing = false;
getFolder(self.options.selected).blurItem();
renderEditMenu();
return that;
};
/*@
editItem <f> Put selected item into edit mode
() -> <o> AnnotationPanel
@*/
that.editItem = function() {
self.editing = true;
getFolder(self.options.selected).editItem();
renderEditMenu();
return that;
};
that.getCurrentAnnotations = function() {
var annotations = {};
self.options.layers.forEach(function(layer) {
annotations[layer.id] = self.$folder[
Ox.getIndexById(self.options.layers, layer.id)
].getCurrentAnnotations();
});
return annotations;
};
/*@
removeItem <f> Remove selected item
() -> <o> AnnotationPanel
@*/
that.removeItem = function(remove) {
if (remove) {
// remove initiated by video editor
getFolder(self.options.selected).removeItem();
} else {
// called from removeannotation callback
self.options.selected = '';
updateLanguages();
self.users = getUsers();
renderOptionsMenu();
renderEditMenu();
}
return that;
};
/*@
removeLayer <f> Remove a layer
(id) -> <o> AnnotationPanel
@*/
that.removeLayer = function(id) {
var $folder = getFolder(self.options.selected),
index = Ox.getIndexById(self.options.layers, id);
if (self.$folder[index] == $folder) {
$folder.blurItem();
}
self.options.layers.splice(index, 1);
renderFolders();
return that;
};
/*@
updateItem <f> Update an item
(id, item) -> <o> AnnotationPanel
@*/
that.updateItem = function(id, item) {
// called from editannotation callback
// on the first update of a new annotation, the id will change
self.options.selected = item.id;
getFolder(id).updateItem(id, item);
updateLanguages();
renderOptionsMenu();
renderEditMenu();
return that;
};
/*@
updateLayer <f> Update a layer
(id, items) -> <o> AnnotationPanel
@*/
that.updateLayer = function(id, items) {
var $folder = getFolder(self.options.selected),
index = Ox.getIndexById(self.options.layers, id);
if (self.$folder[index] == $folder) {
$folder.blurItem();
}
self.options.layers[index].items = items;
self.$folder[index].replaceWith(
self.$folder[index] = renderFolder(self.options.layers[index])
);
return that;
};
return that;
};

View file

@ -0,0 +1,340 @@
'use strict';
/*@
Ox.BlockVideoTimeline <f> Block Video Timeline
options <o> Options
self <o> Shared private variable
([options[, self]]) -> <o:Ox.Element> Block Video Timeline
edit <!> edit
select <!> select
position <!> position
@*/
Ox.BlockVideoTimeline = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
duration: 0,
find: '',
getImageURL: null,
'in': 0,
out: 0,
position: 0,
results: [],
showPointMarkers: false,
state: 'default',
subtitles: [],
type: '',
width: 0
})
.options(options || {})
.update({
'in': function() {
self.options.showPointMarkers && setPoint('in');
},
out: function() {
self.options.showPointMarkers && setPoint('out');
},
position: setPositionMarker,
results: setResults,
subtitles: setSubtitles,
state: setState,
type: setType,
width: setWidth
})
.addClass('OxBlockVideoTimeline')
.css({
position: 'absolute'
})
.on({
mousedown: mousedown,
mouseleave: mouseleave,
mousemove: mousemove
})
.bindEvent({
doubleclick: doubleclick,
drag: function(data) {
mousedown(data);
}
});
self.$images = [];
self.$interfaces = [];
self.$lines = [];
self.$tooltip = Ox.Tooltip({
animate: false
})
.css({
textAlign: 'center'
});
self.height = 16;
self.lines = getLines();
self.margin = 8;
setCSS();
self.$image = getImage();
Ox.loop(self.lines, function(i) {
addLine(i);
});
self.$positionMarker = Ox.$('<img>')
.attr({
src: Ox.UI.getImageURL('markerPosition')
})
.addClass('OxMarkerPosition')
.appendTo(that);
setPositionMarker();
if (self.options.showPointMarkers) {
self.$pointMarker = {};
['in', 'out'].forEach(function(point) {
var titlecase = Ox.toTitleCase(point);
self.$pointMarker[point] = Ox.$('<img>')
.addClass('OxMarkerPoint' + titlecase)
.attr({
src: Ox.UI.getImageURL('marker' + titlecase)
})
.appendTo(that);
setPointMarker(point);
});
}
function addLine(i) {
self.$lines[i] = Ox.$('<div>')
.css({
position: 'absolute',
left: self.margin / 2 + 'px',
top: i * (self.height + self.margin) + 'px',
width: self.options.width + 'px',
height: '24px',
overflow: 'hidden'
})
.appendTo(that);
self.$images[i] = self.$image.clone()
.css({
position: 'absolute',
marginLeft: -i * self.options.width + 'px'
})
.appendTo(self.$lines[i]);
self.$interfaces[i] = Ox.$('<div>')
// OxTarget and OxSpecialTarget are needed for InfoList
.addClass('OxInterface OxTarget OxSpecialTarget')
.css({
top: '2px',
width: Math.round(self.options.duration) + 'px',
height: '20px',
marginLeft: -i * self.options.width + 'px'
//background: 'rgba(255, 0, 0, 0.1)',
})
.appendTo(self.$lines[i]);
}
function doubleclick(e) {
var position;
if ($(e.target).is('.OxInterface')) {
position = getPosition(e);
if (
self.options.state == 'selected'
&& position >= self.options['in']
&& position <= self.options.out
) {
that.triggerEvent('edit');
} else if (self.options.state != 'editing') {
that.triggerEvent('select');
}
}
}
function getImage() {
return Ox.SmallVideoTimelineImage({
duration: self.options.duration,
editing: self.options.editing,
imageURL: self.options.getImageURL,
'in': self.options['in'],
mode: 'editor',
out: self.options.out,
results: self.options.results,
state: self.options.state,
subtitles: Ox.clone(self.options.subtitles, true),
type: self.options.type,
width: Math.round(self.options.duration)
}).bindEvent({
loaded: updateTimelines
});
}
function getLines() {
return Math.ceil(self.options.duration / self.options.width);
}
function getPosition(e) {
// FIXME: this might still be broken in opera according to
// http://acko.net/blog/mouse-handling-and-absolute-positions-in-javascript
return e.offsetX ? e.offsetX
: e.clientX - $(e.target).offset().left;
}
function getSubtitle(position) {
var subtitle = '';
Ox.forEach(self.options.subtitles, function(v) {
if (v['in'] <= position && v.out > position) {
subtitle = v;
return false; // break
}
});
return subtitle;
}
function getTooltip(e) {
}
function mousedown(e) {
if ($(e.target).is('.OxInterface')) {
self.options.position = getPosition(e);
setPositionMarker();
// fixme: check if this pattern is better
// than the one used for list selection
if (!self.triggered) {
that.triggerEvent('position', {
position: self.options.position
});
self.triggered = true;
setTimeout(function() {
self.triggered = false;
}, 250);
}
}
}
function mouseleave() {
self.$tooltip.hide();
}
function mousemove(e) {
var position, subtitle;
if ($(e.target).is('.OxInterface')) {
position = getPosition(e);
subtitle = getSubtitle(position);
self.$tooltip.options({
title: subtitle
? '<span class=\'OxBright\'>' + Ox.highlight(
subtitle.text, self.options.find, 'OxHighlight', true
).replace(/\n/g, ' ') + '</span><br/>'
+ Ox.formatDuration(subtitle['in'], 3) + ' - '
+ Ox.formatDuration(subtitle['out'], 3)
: Ox.formatDuration(position)
})
.show(e.clientX, e.clientY);
} else {
self.$tooltip.hide();
}
}
function setCSS() {
that.css({
width: (self.options.width + self.margin) + 'px',
height: ((self.height + self.margin) * self.lines) + 4 + 'px'
// fixme: the + 4 represent the margin-bottom in the video editor
// is there a better way to get a proper margin-bottom?
});
}
function setPoint(point) {
setPointMarker(point);
self.$image.options(point, self.options[point]);
updateTimelines();
}
function setPointMarker(point) {
var position = Math.round(self.options[point]);
self.$pointMarker[point].css({
left: (position % self.options.width) + 'px',
top: (Math.floor(position / self.options.width) *
(self.height + self.margin) + 15) + 'px'
});
}
function setPositionMarker() {
var position = Math.round(self.options.position);
self.$positionMarker.css({
left: (position % self.options.width) - 1 + 'px',
top: (Math.floor(position / self.options.width) *
(self.height + self.margin) + 2) + 'px'
});
}
function setResults() {
self.$image.options({results: self.options.results});
updateTimelines();
}
function setState() {
self.$image.options({state: self.options.state});
updateTimelines();
}
function setSubtitles() {
self.$image.options({subtitles: Ox.clone(self.options.subtitles, true)});
updateTimelines();
}
function setType() {
self.$image = getImage();
self.$images.forEach(function($image, i) {
self.$images[i].replaceWith(
self.$images[i] = self.$image.clone()
.css({
position: 'absolute',
marginLeft: -i * self.options.width + 'px'
})
);
});
}
function setWidth() {
self.lines = getLines();
setCSS();
Ox.loop(self.lines, function(i) {
if (self.$lines[i]) {
self.$lines[i].css({
width: self.options.width + 'px'
});
self.$images[i].css({
marginLeft: (-i * self.options.width) + 'px'
});
self.$interfaces[i].css({
marginLeft: (-i * self.options.width) + 'px'
});
} else {
addLine(i);
}
});
while (self.$lines.length > self.lines) {
self.$lines[self.$lines.length - 1].remove();
self.$lines.pop();
self.$images.pop();
}
setPositionMarker();
if (self.options.showPointMarkers) {
setPointMarker('in');
setPointMarker('out');
}
}
function updateTimelines() {
self.$lines.forEach(function($line, i) {
$($line.children()[0]).replaceWith(
self.$images[i] = self.$image.clone().css({
position: 'absolute',
marginLeft: (-i * self.options.width) + 'px'
})
);
});
}
return that;
};

View file

@ -0,0 +1,657 @@
'use strict';
Ox.ClipPanel = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
annotationsCalendarSize: 256,
annotationsMapSize: 256,
annotationsRange: 'all',
annotationsSort: 'position',
clipRatio: 16/9,
clips: [],
clickLink: null,
duration: 0,
editable: false,
formatTitle: function() {
return Ox.last(arguments);
},
getClipImageURL: null,
'in': 0,
layers: [],
out: 0,
position: 0,
selected: [],
sort: [],
sortOptions: [],
showAnnotationsCalendar: false,
showAnnotationsMap: false,
showLayers: {},
showUsers: false,
view: 'list',
width: 0
})
.options(options || {})
.update({
clips: function() {
var action = self.options.clips.length && self.options.view != 'annotations'
? 'enableItem' : 'disableItem';
self.$list.options({
items: Ox.clone(self.options.clips),
sort: getListSort(),
sortable: isSortable()
});
self.$menu[action]('selectclip');
self.$menu[action]('splitclip');
updateStatus();
},
duration: updateStatus,
height: function() {
self.$list.size();
},
position: function() {
if (self.options.view == 'annotations') {
self.$list.options({
position: self.options.position
});
}
},
selected: selectClips,
sort: function() {
updateSortElement();
self.$list.options({
sort: getListSort(),
sortable: isSortable(),
});
}
})
.bindEvent({
resize: function(data) {
self.$sortSelect.options({width: getSortSelectWidth(data.size)});
self.$list.size();
}
});
self.columns = [
{
align: 'right',
id: 'index',
operator: '+',
title: Ox._('Index'),
visible: false,
width: 60
},
{
addable: false,
id: 'id',
operator: '+',
sort: function(value, data) {
return data.sort;
},
unique: true,
},
{
addable: false,
id: 'item',
operator: '+',
sort: function(value, data) {
return data.sort;
},
},
{
id: 'title',
format: self.options.formatTitle,
operator: '+',
sort: function(value, data) {
return data.sort;
},
title: Ox._('Title'),
visible: true,
width: 120
},
{
align: 'right',
editable: isEditable,
format: function(value, data) {
return (
isEditable(data) ? ['', '']
: ['<span class="OxLight">', '</span>']
).join(Ox.formatDuration(value, 3));
},
id: 'in',
operator: '+',
sort: function(value, data) {
return data.sort;
},
title: Ox._('In'),
visible: true,
width: 90
},
{
align: 'right',
editable: isEditable,
format: function(value, data) {
return (
isEditable(data) ? ['', '']
: ['<span class="OxLight">', '</span>']
).join(Ox.formatDuration(value, 3));
},
id: 'out',
title: Ox._('Out'),
visible: true,
width: 90
},
{
align: 'right',
editable: isEditable,
format: function(value, data) {
return (
isEditable(data) ? ['', '']
: ['<span class="OxLight">', '</span>']
).join(Ox.formatDuration(value, 3));
},
id: 'duration',
operator: '+',
sort: function(value, data) {
return data.sort;
},
title: Ox._('Duration'),
visible: true,
width: 90
},
{
addable: false,
id: 'sort',
operator: '+',
// title: Ox._('Sort'),
visible: false
}
];
self.$menubar = Ox.Bar({
size: 24
})
.bindEvent({
doubleclick: function(e) {
if ($(e.target).is('.OxBar')) {
self.$list.animate({scrollTop: 0}, 250);
}
}
});
self.$menu = Ox.MenuButton({
items: [
{group: 'view', min: 1, max: 1, items: [
{id: 'list', title: Ox._('View Clips as List'), checked: self.options.view == 'list'},
{id: 'grid', title: Ox._('View Clips as Grid'), checked: self.options.view == 'grid'},
{id: 'annotations', title: Ox._('View Annotations'), checked: self.options.view == 'annotations'},
]},
{},
{id: 'selectclip', title: 'Select Clip at Current Position', keyboard: '\\', disabled: self.options.clips.length == 0 || self.options.view == 'annotations'},
{id: 'splitclip', title: 'Split Clip at Current Position', keyboard: 'shift \\', disabled: self.options.clips.length == 0 || self.options.view == 'annotations'},
{},
{id: 'split', title: Ox._('Split Selected Clips at Cuts'), disabled: !self.options.editable || self.options.selected.length == 0 || self.options.view == 'annotations'},
{id: 'join', title: Ox._('Join Selected Clips at Cuts'), disabled: !self.options.editable || self.options.selected.length < 2 || self.options.view == 'annotations'},
{id: 'replace', title: Ox._('Make Selected Clips Editable'), disabled: !self.options.editable || self.options.selected.length == 0 || self.options.view == 'annotations'}
],
title: 'set',
tooltip: Ox._('Options'),
type: 'image'
})
.css({
float: 'left',
margin: '4px 2px 4px 4px'
})
.bindEvent({
change: function(data) {
if (data.id == 'view') {
var action = self.options.editable
&& self.options.selected.length
&& data.checked[0].id != 'annotations'
? 'enableItem' : 'disableItem';
self.options.view = data.checked[0].id;
self.$menu[action]('split');
self.$menu[
self.options.editable
&& self.options.selected.length > 1
&& data.checked[0].id != 'annotations'
? 'enableItem' : 'disableItem'
]('join');
self.$menu[action]('replace');
self.$panel.replaceElement(1, self.$list = getList());
that.triggerEvent('view', {view: self.options.view});
}
},
click: function(data) {
if (data.id == 'selectclip') {
that.selectClip();
self.$list.gainFocus();
} else if (data.id == 'splitclip') {
// ...
} else if (data.id == 'split') {
splitClips();
} else if (data.id == 'join') {
joinClips();
}
}
})
.appendTo(self.$menubar),
self.$sortSelect = Ox.Select({
items: self.options.sortOptions,
value: self.options.sort[0].key,
width: getSortSelectWidth(self.options.width)
})
.bindEvent({
change: function(data) {
self.options.sort = [{
key: data.value,
operator: Ox.getObjectById(
self.options.sortOptions, data.value
).operator
}];
updateSortElement();
that.triggerEvent('sort', self.options.sort);
}
});
self.$orderButton = Ox.Button({
overlap: 'left',
title: getButtonTitle(),
tooltip: getButtonTooltip(),
type: 'image'
})
.bindEvent({
click: function() {
self.options.sort = [{
key: self.options.sort[0].key,
operator: self.options.sort[0].operator == '+' ? '-' : '+'
}];
updateSortElement();
that.triggerEvent('sort', self.options.sort);
}
});
self.$sortElement = Ox.FormElementGroup({
elements: [self.$sortSelect, self.$orderButton],
float: 'right'
})
.css({
float: 'right',
margin: '4px 4px 4px 2px'
})
.appendTo(self.$menubar);
self.$list = getList();
self.$statusbar = Ox.Bar({
size: 16
});
self.$status = Ox.Element()
.css({
marginTop: '2px',
fontSize: '9px',
textAlign: 'center',
textOverflow: 'ellipsis'
})
.appendTo(self.$statusbar);
that.setElement(
self.$panel = Ox.SplitPanel({
elements: [
{
element: self.$menubar,
size: 24
},
{
element: self.$list
},
{
element: self.$statusbar,
size: 16
}
],
orientation: 'vertical'
})
);
updateStatus();
function editClip(data) {
var value = self.$list.value(data.id, data.key);
if (data.value != value && !(data.value === '' && value === null)) {
self.$list.value(data.id, data.key, data.value || null);
that.triggerEvent('edit', data);
}
}
function getButtonTitle() {
return self.options.sort[0].operator == '+' ? 'up' : 'down';
}
function getButtonTooltip() {
return Ox._(self.options.sort[0].operator == '+' ? 'Ascending' : 'Descending');
}
function getEditable(ids) {
return ids.filter(function(id) {
return isEditable(Ox.getObjectById(self.options.clips, id));
});
}
function getList() {
var $list;
if (self.options.view == 'list') {
$list = Ox.TableList({
columns: self.columns,
columnsMovable: true,
columnsRemovable: true,
columnsResizable: true,
columnsVisible: true,
items: Ox.clone(self.options.clips),
keys: ['director', 'year'],
pageLength: 1000,
scrollbarVisible: true,
selected: self.options.selected,
sort: getListSort(),
sortable: isSortable(),
unique: 'id'
});
} else if (self.options.view == 'grid') {
$list = Ox.IconList({
draggable: true,
fixedRatio: self.options.clipRatio,
item: function(data, sort, size) {
size = size || 128; // fixme: is this needed?
var ratio = data.videoRatio,
fixedRatio = self.options.clipRatio,
width = ratio > fixedRatio ? size : Math.round(size * ratio / fixedRatio),
height = Math.round(width / ratio),
info,
title = self.options.formatTitle(data),
url = self.options.getClipImageURL(data.id, width, height);
if (['text', 'position', 'duration', 'random'].indexOf(sort[0].key) > -1) {
info = Ox.formatDuration(data['in']) + ' - '
+ Ox.formatDuration(data.out);
} else {
info = Ox.formatDuration(data['in']) + ' - '
+ Ox.formatDuration(data.out);
}
return {
height: height,
id: data.id,
info: info,
title: title,
url: url,
width: width
};
},
items: self.options.clips,
keys: ['annotation', 'id', 'in', 'out'],
orientation: 'both',
selected: self.options.selected,
sort: getListSort(),
unique: 'id'
});
} else if (self.options.view == 'annotations') {
$list = Ox.AnnotationPanel({
calendarSize: self.options.annotationsCalendarSize,
clickLink: self.options.clickLink,
editable: false,
//highlight: self.options.find,
//'in': self.options['in'],
layers: self.options.layers,
mapSize: self.options.annotationsMapSize,
//out: self.options.out,
position: self.options.position,
range: self.options.annotationsRange,
showCalendar: self.options.showAnnotationsCalendar,
showLayers: Ox.clone(self.options.showLayers),
showMap: self.options.showAnnotationsMap,
showUsers: self.options.showUsers,
sort: self.options.annotationsSort,
width: self.options.width
});
$list.size = function() {
$list.options({
width: self.options.width
});
};
return $list;
}
$list.bindEvent({
copy: function(data) {
that.triggerEvent('copy', data);
},
copyadd: function(data) {
that.triggerEvent('copyadd', data);
},
cut: function(data) {
if (self.options.editable) {
that.triggerEvent('cut', data);
self.options.selected = [];
selectClips();
that.triggerEvent('select', {ids: []});
}
},
cutadd: function(data) {
if (self.options.editable) {
that.triggerEvent('cutadd', data);
self.options.selected = [];
selectClips();
that.triggerEvent('select', {ids: []});
}
},
'delete': function(data) {
self.options.editable && that.triggerEvent('delete', data);
},
move: function(data) {
data.ids.forEach(function(id, index) {
self.$list.value(id, 'index', index);
});
that.triggerEvent('move', data);
},
open: function(data) {
that.triggerEvent('open', data);
},
paste: function() {
self.options.editable && that.triggerEvent('paste');
},
select: function(data) {
self.options.selected = data.ids;
selectClips();
that.triggerEvent('select', data);
},
sort: function(data) {
if (data.key == 'in') {
data.key = 'position';
}
self.options.sort = [data];
updateSortElement();
self.$list.options({sortable: isSortable()});
that.triggerEvent('sort', self.options.sort);
},
submit: function(data) {
var value = self.$list.value(data.id);
data.value = Ox.parseDuration(data.value);
if (
(data.key == 'in' && data.value < value.out)
|| (data.key == 'out' && data.value > value['in'])
|| (data.key == 'duration' && data.value > 0)
) {
self.$list.value(data.id, data.key, data.value);
if (data.key == 'in') {
self.$list.value(data.id, 'duration', value.out - data.value);
} else if (data.key == 'out') {
self.$list.value(data.id, 'duration', data.value - value['in']);
} else if (data.key == 'duration') {
self.$list.value(data.id, 'out', value['in'] + data.value);
}
that.triggerEvent('edit', data);
} else {
self.$list.value(data.id, data.key, value[data.key]);
}
}
});
return $list;
}
function getListSort() {
var sort = [{key: 'index', operator: '+'}];
if (self.options.sort && self.options.sort.length) {
sort[0].operator = self.options.sort[0].operator;
sort[0].key = Ox.getObjectById(self.columns, self.options.sort[0].key)
? self.options.sort[0].key
: 'sort';
if (self.options.sort[0].key == 'position') {
sort[0].key = 'in';
}
}
return sort;
}
function getSortSelectWidth(width) {
return Math.min(144, width - 52 + Ox.UI.SCROLLBAR_SIZE);
}
function isEditable(data) {
return self.options.editable && !data.annotation;
}
function isSortable() {
return self.options.editable
&& self.options.sort && self.options.sort.length
&& self.options.sort[0].key == 'index'
&& self.options.sort[0].operator == '+';
}
function joinClips() {
var clips = getEditable(self.options.selected).map(function(id) {
return Ox.clone(Ox.getObjectById(self.options.clips, id));
}),
ids = [], join = [], joined;
do {
joined = false;
Ox.forEach(clips, function(outClip) {
var outPoint = outClip.item + '/' + outClip.out;
Ox.forEach(clips, function(inClip, index) {
var inPoint = inClip.item + '/' + inClip['in'];
if (inPoint == outPoint) {
ids = Ox.unique(ids.concat([outClip.id, inClip.id]));
join = Ox.unique(join.concat([outClip.id]));
outClip.out = inClip.out;
if (Ox.contains(join, inClip.id)) {
join.splice(join.indexOf(inClip.id), 1);
}
clips.splice(index, 1);
joined = true;
return false; // break
}
});
if (joined) {
return false; // break;
}
});
} while (joined);
join = join.map(function(id) {
var clip = Ox.getObjectById(clips, id);
return {'in': clip['in'], item: clip.item, out: clip.out};
});
if (ids.length) {
that.triggerEvent('join', {ids: ids, join: join});
}
}
function selectClips() {
if (self.options.editable) {
self.$menu[
self.options.selected.length > 0 ? 'enableItem' : 'disableItem'
]('split');
self.$menu[
self.options.selected.length > 1 ? 'enableItem' : 'disableItem'
]('join');
self.$menu[
self.options.selected.length > 0 ? 'enableItem' : 'disableItem'
]('replace');
}
self.$list.options({selected: self.options.selected});
}
function splitClips() {
var ids = getEditable(self.options.selected).filter(function(id) {
var clip = Ox.getObjectById(self.options.clips, id);
return clip.cuts.length;
}),
split = Ox.flatten(ids.map(function(id) {
var clip = Ox.getObjectById(self.options.clips, id),
cuts = [clip['in']].concat(clip.cuts).concat([clip.out]);
return Ox.range(0, cuts.length - 1).map(function(i) {
return {'in': cuts[i], item: clip.item, out: cuts[i + 1]};
});
}));
if (split.length > ids.length) {
that.triggerEvent('split', {ids: ids, split: split});
}
}
function updateSortElement() {
self.$sortSelect.options({
value: self.options.sort[0].key,
});
self.$orderButton.options({
title: getButtonTitle(),
tooltip: getButtonTooltip(),
});
}
function updateStatus() {
self.$status.html(
Ox.toTitleCase(Ox.formatCount(self.options.clips.length, 'Clip'))
+ ', ' + Ox.formatDuration(self.options.duration, 3)
);
}
that.getPasteIndex = function() {
return self.$list.getPasteIndex();
};
that.invertSelection = function() {
self.$list.invertSelection();
};
that.selectAll = function() {
self.$list.selectAll();
};
that.selectClip = function() {
var index;
Ox.forEach(self.options.clips, function(clip, i) {
Ox.print('CLIP', i, clip.position, clip.duration, self.options.position)
if (clip.position <= self.options.position) {
index = i
} else {
return false; // break
}
});
self.options.selected = [self.options.clips[index].id];
selectClips();
that.triggerEvent('select', {ids: self.options.selected});
return that;
};
that.updateItem = function(id, data) {
self.options.clips[Ox.getIndexById(self.options.clips, id)] = data;
self.$list.value(id, {
duration: data.duration,
'in': data['in'],
out: data.out,
sort: data.sort
});
return that;
};
return that;
};

View file

@ -0,0 +1,283 @@
'use strict';
/*@
Ox.LargeVideoTimeline <f> LargeTimeline Object
options <o> Options object
self <o> Shared private variable
([options[, self]]) -> <o:Ox.Element> LargeTimeline Object
position <!> position
@*/
Ox.LargeVideoTimeline = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
chapters: [],
cuts: [],
disabled: false,
duration: 0,
find: '',
getImageURL: null,
'in': 0,
matches: [],
out: 0,
position: 0,
showInToOut: false,
subtitles: [],
type: '',
width: 0
})
.options(options || {})
.update({
find: setSubtitles,
'in': function() {
setPointMarker('in');
},
out: function() {
setPointMarker('out');
},
position: setPosition,
subtitles: setSubtitles,
type: setType,
width: setWidth
})
.addClass('OxLargeVideoTimeline OxMedia')
.on({
mouseleave: mouseleave,
mousemove: mousemove
});
if (!self.options.disabled) {
that.bindEvent({
anyclick: click,
dragstart: dragstart,
drag: drag,
dragend: dragend
});
}
self.$cuts = [];
self.$pointMarker = {};
self.$tiles = {};
self.$tooltip = Ox.Tooltip({animate: false});
self.center = Math.floor(self.options.width / 2);
self.fps = 25;
self.height = 64;
self.isAsync = self.options.getImageURL.length == 3;
self.tileWidth = 1500;
self.tiles = self.options.duration * self.fps / self.tileWidth;
self.$timeline = $('<div>')
.css({left: self.center + 'px'})
.appendTo(that);
setTimeout(setSubtitles);
if (self.options.showInToOut) {
if (self.options['in']) {
$('<div>')
.addClass('OxOverlay')
.css({
left: 0,
width: self.options['in'] * self.fps + 'px',
})
.appendTo(self.$timeline);
}
if (self.options.out) {
$('<div>')
.addClass('OxOverlay')
.css({
left: self.options.out * self.fps + 'px',
width: (self.options.duration - self.options.out) * self.fps + 'px',
})
.appendTo(self.$timeline);
}
}
setTimeout(function() {
var $cut = $('<img>')
.addClass('OxCut')
.attr({src: Ox.UI.getImageURL('markerCut')}),
$chapter = $('<img>')
.addClass('OxChapter')
.attr({src: Ox.UI.getImageURL('markerChapter')}),
chapters = self.options.chapters.slice(1).map(function(chapter) {
return chapter.position;
});
Ox.unique(chapters.concat(self.options.cuts)).forEach(function(v, i) {
self.$cuts[i] = (Ox.contains(chapters, v) ? $chapter : $cut)
.clone()
.css({left: (v * self.fps) + 'px'})
.appendTo(self.$timeline);
});
});
self.$markerPosition = $('<img>')
.addClass('OxMarkerPosition')
.attr({src: Ox.UI.getImageURL('markerPosition')})
.appendTo(that);
setMarker();
['in', 'out'].forEach(function(point) {
var titlecase = Ox.toTitleCase(point);
self.$pointMarker[point] = $('<img>')
.addClass('OxMarkerPoint' + titlecase)
.attr({src: Ox.UI.getImageURL('marker' + titlecase)})
.appendTo(self.$timeline);
setPointMarker(point);
});
setWidth();
setPosition();
function click(data) {
self.options.position = Ox.round(Ox.limit(
getPosition(data), 0, self.options.duration
), 3);
setPosition();
that.triggerEvent('position', {position: self.options.position});
}
function dragstart(data) {
Ox.$body.addClass('OxDragging');
self.drag = {x: data.clientX};
}
function drag(data) {
self.options.position = Ox.round(Ox.limit(
self.options.position + (self.drag.x - data.clientX) / self.fps,
0, self.options.duration
), 3);
self.drag.x = data.clientX;
setPosition();
that.triggerEvent('positioning', {position: self.options.position});
}
function dragend() {
Ox.$body.removeClass('OxDragging');
that.triggerEvent('position', {position: self.options.position});
}
function getImageURL(i, callback) {
if (!self.isAsync) {
callback(self.options.getImageURL(self.options.type, i));
} else {
self.options.getImageURL(self.options.type, i, callback);
}
}
function getPosition(e) {
return self.options.position + (
e.clientX - that.offset().left - self.center - 1
) / self.fps;
}
function mouseleave(e) {
self.clientX = 0;
self.clientY = 0;
self.$tooltip.hide();
}
function mousemove(e) {
self.clientX = e.clientX;
self.clientY = e.clientY;
updateTooltip();
}
function setMarker() {
self.$markerPosition.css({left: self.center + 'px'});
}
function setPointMarker(point) {
self.$pointMarker[point].css({
left: (self.options[point] * self.fps) + 'px'
});
}
function setPosition() {
self.tile = Math.floor(self.options.position * self.fps / self.tileWidth);
self.$timeline.css({
marginLeft: (-self.options.position * self.fps) + 'px'
});
Ox.loop(
Math.max(self.tile - 1, 0),
Math.min(self.tile + 2, self.tiles),
function(i) {
if (!self.$tiles[i]) {
if (self.isAsync) {
self.$tiles[i] = true;
}
getImageURL(i, function(url) {
self.$tiles[i] = $('<img>')
.attr({src: url})
.css({left: (i * self.tileWidth) + 'px'})
.appendTo(self.$timeline);
});
}
}
);
if (self.clientX && self.clientY) {
updateTooltip();
}
}
function setSubtitles() {
that.find('.OxSubtitle').remove();
self.$subtitles = [];
self.options.subtitles.forEach(function(subtitle, i) {
var found = self.options.find
&& subtitle.text.toLowerCase().indexOf(self.options.find.toLowerCase()) > -1;
self.$subtitles[i] = $('<div>')
.addClass('OxSubtitle' + (found ? ' OxHighlight' : ''))
.css({
left: Math.round(subtitle['in'] * self.fps) + 'px',
width: Math.round(((subtitle.out - subtitle['in']) * self.fps) - 2) + 'px'
})
.html(Ox.highlight(
subtitle.text, self.options.find, 'OxHighlight', true
))
.appendTo(self.$timeline);
});
}
function setType() {
Ox.forEach(self.$tiles, function($tile, i) {
getImageURL(i, function(url) {
$tile.attr({src: url});
});
});
}
function setWidth() {
self.center = Math.floor(self.options.width / 2);
that.css({
width: self.options.width + 'px'
});
self.$timeline.css({
left: self.center + 'px'
});
setMarker();
}
function triggerPositionEvent() {
that.triggerEvent('position', {position: self.options.position});
}
function updateTooltip() {
var position = getPosition(self);
if (position >= 0 && position <= self.options.duration) {
self.$tooltip
.options({
title: Ox.formatDuration(position, 3)
})
.show(self.clientX, self.clientY);
} else {
self.$tooltip.hide();
}
}
return that;
};

View file

@ -0,0 +1,271 @@
'use strict';
/*@
Ox.SmallVideoTimeline <f> Small video timeline
options <o> Options
self <o> Shared private variable
([options[, self]]) -> <o:Ox.Element> Small video timeline
position <!> position
@*/
Ox.SmallVideoTimeline = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
// _offset is a hack for cases where all these position: absolute
// elements have to go into a float: left
// FIXME: possibly unused and unneeded
_offset: 0,
disabled: false,
duration: 0,
find: '',
imageURL: '',
'in': 0,
invertHighlight: false,
mode: 'player',
out: 0,
paused: false,
results: [],
showInToOut: false,
showMilliseconds: 0,
state: 'default',
subtitles: [],
width: 256
})
.options(options || {})
.update({
duration: function() {
self.$image.options({duration: self.options.duration});
},
imageURL: function() {
self.$image.options({imageURL: self.options.imageURL});
},
'in': function() {
self.$image.options({'in': self.options['in']});
self.options.mode == 'editor' && setPointMarker('in');
},
out: function() {
self.$image.options({out: self.options.out});
self.options.mode == 'editor' && setPointMarker('out');
},
paused: function() {
self.$positionMarker[
self.options.paused ? 'addClass' : 'removeClass'
]('OxPaused');
},
position: function() {
setPositionMarker();
},
results: function() {
self.$image.options({results: self.options.results});
},
state: function() {
self.$image.options({state: self.options.state});
},
subtitles: function() {
self.$image.options({subtitles: self.options.subtitles});
},
width: function() {
setWidth();
}
})
.addClass('OxSmallVideoTimeline')
.css(Ox.extend({
width: self.options.width + 'px'
}, self.options.mode == 'player' ? {
background: 'rgb(0, 0, 0)',
borderRadius: '8px'
} : {}));
self.height = self.options.mode == 'player' ? 16 : 24;
self.imageLeft = self.options.mode == 'player' ? 8 : 4;
self.imageWidth = self.options.width -
(self.options.mode == 'player' ? 16 : 8)
self.imageHeight = self.options.mode == 'player' ? 16 : 18;
self.interfaceLeft = self.options.mode == 'player' ? 0 : 4;
self.interfaceTop = self.options.mode == 'player' ? 0 : 2;
self.interfaceWidth = self.options.mode == 'player' ? self.options.width : self.imageWidth;
that.css({
height: self.height + 'px'
});
self.$image = getTimelineImage().appendTo(that);
self.$interface = Ox.Element({
tooltip: getTooltip
})
.addClass('OxInterface')
.css({
left: self.interfaceLeft + 'px',
top: self.interfaceTop + 'px',
width: self.interfaceWidth + 'px',
height: '20px'
})
.bindEvent({
drag: function(data) {
mousedown(data);
},
dragend: function(data) {
self.triggered = false;
mousedown(data);
},
mousedown: mousedown
})
.appendTo(that);
self.$interface.$tooltip.css({
textAlign: 'center'
});
if (self.options.mode == 'player') {
self.$positionMarker = $('<div>')
.addClass('OxMarkerPlay' + (self.options.paused ? ' OxPaused' : ''))
.append($('<div>').append($('<div>')))
.appendTo(that);
} else {
self.$positionMarker = $('<img>')
.addClass('OxMarkerPosition')
.attr({
src: Ox.UI.getImageURL('markerPosition')
})
.appendTo(that);
}
setPositionMarker();
if (self.options.mode == 'editor') {
self.$pointMarker = {};
['in', 'out'].forEach(function(point) {
var titlecase = Ox.toTitleCase(point);
self.$pointMarker[point] = $('<img>')
.addClass('OxMarkerPoint' + titlecase)
.attr({
src: Ox.UI.getImageURL('marker' + titlecase)
})
.appendTo(that);
setPointMarker(point);
});
}
function getLeft() {
return (
self.options.showInToOut
? self.options.position - self.options['in']
: self.options.position
) * self.imageWidth / self.options.duration;
}
function getPosition(e) {
var position = (
(e.offsetX ? e.offsetX : e.clientX - $(e.target).offset().left)
- (self.options.mode == 'player' ? 8 : 0)
) * self.options.duration / self.imageWidth;
position = Ox.limit(position, 0, self.options.duration);
if (self.options.showInToOut) {
position += self.options['in'];
}
return position;
}
function getSubtitle(position) {
var subtitle = '';
Ox.forEach(self.options.subtitles, function(v) {
if (v['in'] <= position && v.out > position) {
subtitle = v;
return false; // break
}
});
return subtitle;
}
function getTimelineImage() {
return (self.options.imageURL ? Ox.SmallVideoTimelineImage({
duration: self.options.duration,
imageURL: self.options.imageURL,
'in': self.options['in'],
invertHighlight: self.options.invertHighlight,
mode: self.options.mode,
out: self.options.out,
results: self.options.results,
showInToOut: self.options.showInToOut,
subtitles: self.options.subtitles,
state: self.options.state,
type: self.options.type,
width: self.imageWidth
}) : Ox.Element()).css({
position: 'absolute',
left: self.imageLeft + 'px',
width: self.imageWidth + 'px'
});
}
function getTooltip(e) {
var position = getPosition(e),
subtitle = getSubtitle(position);
return subtitle
? '<span class=\'OxBright\'>' + Ox.highlight(
subtitle.text, self.options.find, 'OxHighlight'
).replace(/\n/g, '<br/>') + '</span><br/>' + Ox.formatDuration(
subtitle['in'], self.options.showMilliseconds
) + ' - ' + Ox.formatDuration(
subtitle['out'], self.options.showMilliseconds
)
: Ox.formatDuration(position, self.options.showMilliseconds);
}
function mousedown(e) {
if (!self.options.disabled && $(e.target).is('.OxInterface')) {
self.options.position = getPosition(e);
setPositionMarker();
if (!self.triggered) {
that.triggerEvent('position', {
position: self.options.position
});
self.triggered = true;
setTimeout(function() {
self.triggered = false;
}, 250);
}
}
}
function setPointMarker(point) {
self.$pointMarker[point].css({
left: self.imageLeft + Math.round(getLeft()) + 'px'
});
}
function setPositionMarker() {
self.$positionMarker.css({
left: self.interfaceLeft + Math.round(getLeft())
- (self.options.mode == 'editor' ? 5 : 0)
+ self.options._offset + 'px'
});
}
function setWidth() {
self.imageWidth = self.options.width -
(self.options.mode == 'player' ? 16 : 8);
self.interfaceWidth = self.options.mode == 'player' ?
self.options.width : self.imageWidth;
that.css({
width: self.options.width + 'px'
});
self.$image.options({
width: self.imageWidth
}).css({
width: self.imageWidth + 'px'
});
self.$interface.css({
width: self.interfaceWidth + 'px'
});
setPositionMarker();
if (self.options.mode == 'editor') {
setPointMarker('in');
setPointMarker('out');
}
}
return that;
};

View file

@ -0,0 +1,342 @@
'use strict';
/*@
Ox.SmallVideoTimelineImage <f> Small Video Timeline Image
options <o> Options
self <o> Shared private variable
([options[, self]]) -> <o:Ox.Element> Small Video Timeline Image
loaded <!> subtitle, result and selection overlays are loaded
@*/
Ox.SmallVideoTimelineImage = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
duration: 0,
imageURL: '',
invertHighlight: false,
'in': 0,
mode: 'player',
out: 0,
results: [],
showInToOut: false,
state: 'default',
subtitles: [],
type: '',
width: 256
})
.options(options || {})
.update({
imageURL: function() {
self.$timeline.attr({src: self.options.imageURL});
},
'in': function() {
self.$selection.attr({
src: getImageURL('selection')
});
},
out: function() {
self.$selection.attr({
src: getImageURL('selection')
});
},
results: function() {
self.$results.attr({
src: getImageURL('results')
});
},
selection: function() {
self.$selection.attr({
src: getImageURL('selection')
});
},
subtitles: function() {
self.$subtitles.attr({
src: getImageURL('subtitles')
});
},
state: function() {
self.$selection.attr({
src: getImageURL('selection')
});
},
width: function() {
var width = self.options.width;
that.css({width: width + 'px'});
self.$results
.attr({src: getImageURL('results')})
.css({width: width + 'px'});
self.$selection
.attr({src: getImageURL('selection')})
.css({width: width + 'px'});
self.$subtitles.css({width: width + 'px'});
self.$timeline.css({width: width + 'px'});
}
})
.css({
position: 'absolute',
width: self.options.width + 'px'
});
if (self.options.showInToOut) {
self.pixelsPerSecond = (
screen.width < 1024 ? 1024 : screen.width
) / self.options.duration;
}
self.images = Ox.isString(self.options.imageURL) ? 1
: Math.ceil(self.options.duration / 3600);
self.imageWidth = Ox.isString(self.options.imageURL) ? 1920
: self.options.showInToOut ? (
self.pixelsPerSecond <= 1
? Math.round(self.options.duration)
: Math.round(self.options.duration * 25)
)
: Math.round(self.options.duration);
self.height = self.options.mode == 'editor' ? 24 : 16;
self.imageHeight = self.options.mode == 'editor' ? 18 : 16;
self.imageTop = self.options.mode == 'editor' ? 3 : 0;
self.timelineTop = self.options.mode == 'editor' ? 4 : 0;
self.themeData = Ox.Theme.getThemeData();
that.css({height: self.height + 'px'});
if (Ox.isString(self.options.imageURL)) {
self.$timeline = $('<img>')
.attr({
src: self.options.imageURL
})
.css({
position: 'absolute',
top: self.timelineTop + 'px',
width: self.options.width + 'px',
height: '16px'
})
.appendTo(that);
} else if (self.options.showInToOut) {
self.$timeline = getClipTimeline()
.css({
position: 'absolute',
top: self.timelineTop + 'px',
width: self.options.width + 'px',
height: '16px'
})
.appendTo(that);
} else {
Ox.loop(self.images, function(i) {
$('<img>')
.attr({
src: self.options.imageURL(self.options.type, i)
})
.css({
position: 'absolute',
left: (i * 3600) + 'px',
top: self.timelineTop + 'px',
width: (i == self.images - 1 ? self.imageWidth % 3600 || 3600 : 3600) + 'px',
height: '16px'
})
.appendTo(that);
});
}
self.$subtitles = $('<img>');
self.$results = $('<img>');
self.$selection = $('<img>');
setTimeout(function() {
self.$subtitles
.attr({
src: getImageURL('subtitles')
})
.css({
position: 'absolute',
top: self.imageTop + 'px',
width: self.options.width + 'px',
height: self.imageHeight + 'px'
})
.appendTo(that);
self.$results
.attr({
src: getImageURL('results')
})
.css({
position: 'absolute',
top: self.imageTop + 'px',
width: self.options.width + 'px',
height: self.imageHeight + 'px'
})
.appendTo(that);
self.$selection
.attr({
src: getImageURL('selection')
})
.css({
position: 'absolute',
top: self.imageTop + 'px',
width: self.options.width + 'px',
height: self.imageHeight + 'px'
})
.appendTo(that);
that.triggerEvent('loaded');
});
function getClipTimeline() {
var $canvas, context, image,
firstTile, lastTile, tileHeight, tileOffset, tileWidth;
if (self.pixelsPerSecond <= 1) {
firstTile = Math.floor(self.options['in'] / 3600);
lastTile = Math.floor(self.options.out / 3600);
tileHeight = 16;
tileOffset = -Math.round(self.options['in'] % 3600);
tileWidth = 3600;
} else {
firstTile = Math.floor(self.options['in'] / 60);
lastTile = Math.floor(self.options.out / 60);
tileHeight = 64;
tileOffset = -Math.round(self.options['in'] % 60 * 25);
tileWidth = 1500;
}
$canvas = $('<canvas>').attr({
width: self.imageWidth,
height: tileHeight
});
context = $canvas[0].getContext('2d');
Ox.loop(firstTile, lastTile + 1, function(tileIndex) {
var $image = $('<img>')
.one({
load: function() {
context.drawImage(
$image[0],
tileOffset + (tileIndex - firstTile) * tileWidth,
0
);
}
})
.attr({
src: self.options.imageURL(tileHeight, tileIndex)
});
});
return $canvas;
}
function getImageURL(image, callback) {
var width = image == 'results' || image == 'selection'
? self.options.width : self.imageWidth,
height = self.imageHeight,
canvas = $('<canvas>').attr({
width: width,
height: height
})[0],
context = canvas.getContext('2d'),
imageData = context.createImageData(width, height),
data = imageData.data;
if (image == 'results') {
var top = 0,
bottom = height;
self.options.results.forEach(function(result) {
var left = Math.round(
result['in'] / self.options.duration * width
),
right = Math.round(
result.out / self.options.duration * width
) + 1,
rgb = self.themeData.videoTimelineResultGradient;
Ox.loop(left, right, function(x) {
Ox.loop(top, bottom, function(y) {
var alpha = self.options.mode == 'editor'
&& (y == top || y == bottom - 1) ? 255 : 128,
color = rgb[[2, 3, 6].indexOf(x % 4 + y % 4) > -1 ? 0 : 1],
index = x * 4 + y * 4 * width;
data[index] = color[0];
data[index + 1] = color[1];
data[index + 2] = color[2];
data[index + 3] = alpha;
});
});
});
} else if (
image == 'selection'
&& self.options.out > self.options['in']
&& !self.options.showInToOut
) {
var left = Math.round(
self.options['in'] / self.options.duration * width
),
right = Math.round(
self.options.out / self.options.duration * width
) + 1,
spans = self.options.invertHighlight
? [{left: 0, right: left}, {left: right, right: width}]
: [{left: left, right: right}],
top = 0,
bottom = height,
rgb = self.themeData[
self.options.state == 'editable' ? 'videoTimelineEditableGradient'
: self.options.state == 'editing' ? 'videoTimelineEditingGradient'
: self.options.state == 'selected' ? 'videoTimelineSelectedGradient'
: 'videoTimelineDefaultGradient'
];
spans.forEach(function(span) {
Ox.loop(span.left, span.right, function(x) {
Ox.loop(top, bottom, function(y) {
var alpha = self.options.mode == 'editor'
&& (y == top || y == bottom - 1) ? 255 : 128,
color = rgb[[2, 3, 6].indexOf(x % 4 + y % 4) > -1 ? 0 : 1],
index = x * 4 + y * 4 * width;
data[index] = color[0];
data[index + 1] = color[1];
data[index + 2] = color[2];
data[index + 3] = alpha;
});
});
});
} else if (image == 'subtitles') {
var bottom = self.options.mode == 'editor' ? 15 : 14,
offset = self.options.showInToOut ? (
self.pixelsPerSecond <= 1
? -self.options['in']
: -self.options['in'] * 25
) : 0,
subtitles = self.options.showInToOut
? self.options.subtitles.filter(function(subtitle) {
return (
subtitle['in'] >= self.options['in']
&& subtitle['in'] <= self.options.out
) || (
subtitle.out >= self.options['in']
&& subtitle.out <= self.options.out
) || (
subtitle['in'] < self.options['in']
&& subtitle.out > self.options.out
)
})
: self.options.subtitles;
subtitles.forEach(function(subtitle) {
var left = Math.round(
subtitle['in'] / self.options.duration * self.imageWidth
) + offset,
right = Math.round(
subtitle.out / self.options.duration * self.imageWidth
) + offset + 1,
top = bottom - subtitle.text.split('\n').length - 2;
Ox.loop(left, right, function(x) {
Ox.loop(top, bottom, function(y) {
var alpha = 128,
color = (y == top || y == bottom - 1) ? [0, 0, 0] : [255, 255, 255],
index = x * 4 + y * 4 * width;
data[index] = color[0];
data[index + 1] = color[1];
data[index + 2] = color[2];
data[index + 3] = alpha;
});
});
});
}
context.putImageData(imageData, 0, 0);
return canvas.toDataURL();
}
return that;
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,667 @@
'use strict';
Ox.VideoEditPanel = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
annotationsCalendarSize: 256,
annotationsMapSize: 256,
annotationsRange: 'all',
annotationsSort: 'position',
clipRatio: 16/9,
clips: [],
clipSize: 256,
clipTooltip: 'clips',
clipView: 'list',
clickLink: null,
controlsTooltips: {},
duration: 0,
editable: false,
enableSubtitles: false,
formatTitle: function() {
return Ox.last(arguments);
},
fps: 25,
fullscreen: false,
getClipImageURL: null,
getLargeTimelineURL: null,
height: 0,
'in': 0,
loop: false,
layers: [],
muted: false,
out: 0,
paused: true,
playInToOut: false,
position: 0,
resolution: 0,
scaleToFill: false,
selected: [],
showAnnotationsCalendar: false,
showAnnotationsMap: false,
showClips: false,
showLayers: {},
showTimeline: false,
showUsers: false,
smallTimelineURL: '',
sort: [],
sortOptions: [],
subtitles: [],
timeline: '',
timelineTooltip: 'timeline',
type: 'static',
video: [],
volume: 1,
width: 0
})
.options(options || {})
.update({
clips: function() {
self.$clipPanel.options({
clips: Ox.clone(self.options.clips),
sort: Ox.clone(self.options.sort)
});
},
duration: function() {
self.$timeline && self.$timeline.replaceWith(
self.$timeline = getTimeline()
);
},
fullscreen: function() {
self.$video.options({fullscreen: self.options.fullscreen});
},
height: function() {
self.$video.options({height: getPlayerHeight()});
self.$clipPanel.options({height: self.options.height});
},
'in': function() {
setPoint('in', self.options['in']);
},
out: function() {
setPoint('out', self.options.out);
},
paused: function() {
self.$video.options({paused: self.options.paused});
},
position: function() {
self.$video.options({position: self.options.position});
self.$timeline.options({position: self.options.position});
self.$clipPanel.options({position: self.options.position});
},
selected: function() {
self.$clipPanel.options({selected: self.options.selected});
},
showClips: function() {
self.$mainPanel.toggle(1);
},
showTimeline: function() {
self.$videoPanel.toggle(2);
},
smallTimelineURL: function() {
self.$video.options({timeline: self.options.smallTimelineURL});
},
subtitles: function() {
self.$video.options({subtitles: self.options.subtitles});
self.$timeline.options({
subtitles: self.options.enableSubtitles ? self.options.subtitles : []
});
},
timeline: function() {
self.$timeline.options({type: self.options.timeline});
},
video: function() {
self.chapters = getChapters();
self.cuts = getCuts();
self.$video.options({
chapters: self.chapters,
video: self.options.video
});
self.$timeline.options({
cuts: self.cuts
});
},
width: function() {
self.$video.options({width: getPlayerWidth()});
self.$timeline.options({width: getTimelineWidth()});
}
})
.bindEvent({
//resize: resizeElement,
key_0: toggleMuted,
key_backslash: function() {
if (self.options.view != 'annotations') {
self.$clipPanel.selectClip();
}
},
key_closebracket: function() {
movePositionTo('chapter', 1);
},
key_comma: function() {
movePositionTo('cut', -1);
},
key_control_c: function() {
// FIXME: this looks wrong, should copy getSelectedClips
that.triggerEvent('copy', [{
annotation: self.options.selected,
'in': self.options['in'],
out: self.options.out
}]);
},
key_dot: function() {
movePositionTo('cut', 1);
},
key_equal: function() {
self.$video.changeVolume(0.1);
},
key_i: function() {
setPoint('in', self.options.position, true);
},
key_left: function() {
movePositionBy(-0.04);
},
key_minus: function() {
self.$video.changeVolume(-0.1);
},
key_o: function() {
setPoint('out', self.options.position, true);
},
key_openbracket: function() {
movePositionTo('chapter', -1);
},
key_right: function() {
movePositionBy(0.04);
},
key_shift_i: function() {
goToPoint('in');
},
key_shift_left: function() {
movePositionBy(-1);
},
key_shift_o: function() {
goToPoint('out');
},
key_slash: function() {
// TODO: selectCut
},
key_shift_right: function() {
movePositionBy(1);
},
key_space: togglePaused
});
self.chapters = getChapters();
self.cuts = getCuts();
self.fullscreen = false;
self.listSizes = [
144 + Ox.UI.SCROLLBAR_SIZE,
280 + Ox.UI.SCROLLBAR_SIZE,
416 + Ox.UI.SCROLLBAR_SIZE
],
self.$menubar = Ox.Bar({size: 24});
self.$player = Ox.Element().css({overflowX: 'hidden'});
self.$video = Ox.VideoPlayer({
chapters: self.chapters,
controlsBottom: [
'play', 'playInToOut', 'volume', 'scale', 'timeline',
'previous', 'next', 'loop', 'position', 'settings'
],
controlsTooltips: self.options.controlsTooltips,
controlsTop: ['fullscreen', 'chapterTitle', 'open'],
cuts: self.cuts,
enableKeyboard: true,
enableMouse: true,
enablePosition: true,
enableSubtitles: self.options.enableSubtitles,
enableTimeline: true,
externalControls: true,
height: getPlayerHeight(),
'in': self.options['in'],
loop: self.options.loop,
muted: self.options.muted,
out: self.options.out,
paused: self.options.paused,
position: self.options.position,
resolution: self.options.resolution,
scaleToFill: self.options.scaleToFill,
showMilliseconds: 3,
subtitles: self.options.subtitles,
timeline: self.options.smallTimelineURL,
video: self.options.video,
volume: self.options.volume,
width: getPlayerWidth()
})
.bindEvent({
durationchange: function(data) {
self.options.duration = data.duration;
self.$timeline && self.$timeline.replaceWith(
self.$timeline = getTimeline()
);
setPosition(self.$video.options('position'), true);
self.$clipPanel.options({duration: self.options.duration});
},
fullscreen: function(data) {
self.options.fullscreen = data.fullscreen;
},
loop: function(data) {
that.triggerEvent('loop', data);
},
muted: function(data) {
that.triggerEvent('muted', data);
},
open: function(data) {
var clip = Ox.last(self.options.clips.filter(function(clip) {
return clip.position <= self.options.position;
}));
that.triggerEvent('openlink', {
annotation: clip.annotation,
'in': clip['in'],
item: clip.item,
out: clip.out,
position: clip['in'] + self.options.position - clip['position'],
});
},
paused: function(data) {
self.options.paused = data.paused;
that.triggerEvent('paused', data);
},
playing: function(data) {
setPosition(data.position, true);
},
position: function(data) {
setPosition(data.position);
},
positioning: function(data) {
setPosition(data.position, false, true);
},
resolution: function(data) {
that.triggerEvent('resolution', data);
},
scale: function(data) {
that.triggerEvent('scale', data);
},
subtitles: function(data) {
self.$timeline.options({
subtitles: data.subtitles ? self.options.subtitles : []
});
that.triggerEvent('subtitles', data);
},
volume: function(data) {
that.triggerEvent('volume', data);
}
})
.appendTo(self.$player);
self.$controls = Ox.Element()
.addClass('OxMedia')
.bindEvent({
toggle: toggleControls
});
self.$timeline = getTimeline()
.appendTo(self.$controls);
self.$videoPanel = Ox.SplitPanel({
elements: [
{
element: self.$menubar,
size: 24
},
{
element: self.$player
},
{
collapsed: !self.options.showTimeline,
collapsible: true,
element: self.$controls,
size: 80,
tooltip: self.options.timelineTooltip
}
],
orientation: 'vertical'
});
self.$clipPanel = Ox.ClipPanel({
annotationsCalendarSize: self.options.annotationsCalendarSize,
annotationsMapSize: self.options.annotationsMapSize,
annotationsRange: self.options.annotationsRange,
annotationsSort: self.options.annotationsSort,
clipRatio: self.options.clipRatio,
clips: Ox.clone(self.options.clips),
clickLink: self.options.clickLink,
duration: self.options.duration,
editable: self.options.editable,
formatTitle: self.options.formatTitle,
getClipImageURL: self.options.getClipImageURL,
'in': self.options['in'],
layers: Ox.clone(self.options.layers),
out: self.options.out,
position: self.options.position,
selected: self.options.selected,
showAnnotationsCalendar: self.options.showAnnotationsCalendar,
showAnnotationsMap: self.options.showAnnotationsMap,
showLayers: self.options.showLayers,
showUsers: self.options.showUsers,
sort: Ox.clone(self.options.sort),
sortOptions: self.options.sortOptions,
view: self.options.clipView,
width: self.options.clipSize
})
.bindEvent({
copy: function(data) {
that.triggerEvent('copy', data);
},
copyadd: function(data) {
that.triggerEvent('copyadd', data);
},
cut: function(data) {
that.triggerEvent('cut', data);
},
cutadd: function(data) {
that.triggerEvent('cutadd', data);
},
'delete': function(data) {
that.triggerEvent('delete', data);
},
edit: function(data) {
that.triggerEvent('edit', data);
},
join: function(data) {
that.triggerEvent('join', data);
},
move: function(data) {
that.triggerEvent('move', data);
},
open: function(data) {
setPosition(getClipById(data.ids[0])['position']);
that.triggerEvent('open', data);
},
paste: function() {
that.triggerEvent('paste');
},
resize: resizeClips,
resizeend: resizeendClips,
select: function(data) {
self.options.selected = data.ids;
that.triggerEvent('select', data);
},
sort: function(data) {
self.options.sort = data;
that.triggerEvent('sort', data);
},
split: function(data) {
that.triggerEvent('split', data);
},
toggle: toggleClips,
view: function(data) {
that.triggerEvent('view', data);
}
});
that.setElement(
self.$mainPanel = Ox.SplitPanel({
elements: [
{
element: self.$videoPanel
},
{
collapsed: !self.options.showClips,
collapsible: true,
element: self.$clipPanel,
resizable: true,
resize: self.listSizes,
size: self.options.clipSize,
tooltip: self.options.clipTooltip
}
],
orientation: 'horizontal'
})
);
function dragTimeline(data) {
self.options.position = data.position;
self.$video.options({position: self.options.position});
self.$clipPanel.options({position: self.options.position});
}
function dragendTimeline(data) {
dragTimeline(data);
that.triggerEvent('position', {position: self.options.position});
}
function getChapters() {
return self.options.clips.map(function(clip) {
return {
position: clip.position,
title: self.options.formatTitle(clip)
};
});
}
function getClipById(id) {
return Ox.getObjectById(self.options.clips, id);
}
function getClipsInSelection() {
// FIXME: there should be a library function (intersect?) for this
var clips = self.options.clips.filter(function(clip) {
var endPosition = clip.position + clip.duration;
return (
clip.position >= self.options['in']
&& endPosition < self.options.out
) || (
clip.position <= self.options['in']
&& endPosition > self.options['in']
) || (
clip.position < self.options.out
&& endPosition >= self.options.out
)
});
return clips.map(function(clip, index) {
var dereference = false,
endPosition = clip.position + clip.duration,
inPoint = clip['in'],
outPoint = clip.out;
if (index == 0 && clip.position < self.options['in']) {
dereference = true;
inPoint = Ox.round(clip['in'] + self.options['in'] - clip.position, 3);
}
if (index == clips.length - 1 && endPosition > self.options.out) {
dereference = true;
outPoint = Ox.round(clip.out + self.options.out - endPosition, 3);
}
return clip.annotation && !dereference ? clip.annotation
: clip.item + '/' + inPoint + '-' + outPoint;
});
}
function getCuts() {
var cuts = [];
self.options.clips.forEach(function(clip, i) {
if (i > 0) {
cuts.push(clip.position);
}
clip.cuts.forEach(function(cut) {
cuts.push(clip.position + cut - clip['in']);
});
});
return cuts;
}
// fixme: why not goToNextPosition()?
function getNextPosition(type, direction) {
// type can be 'chapter' or 'cut'
var positions;
if (type == 'chapter') {
positions = self.chapters.map(function(chapter) {
return chapter.position;
});
} else if (type == 'cut') {
positions = [0].concat(self.cuts, self.options.duration);
}
return Ox.nextValue(positions, self.options.position, direction);
}
function getPlayerHeight() {
return self.options.height - 24 - 32
- self.options.showTimeline * 80 - 1;
}
function getPlayerWidth() {
return self.options.width
- (self.options.showClips && !self.fullscreen)
* self.options.clipSize - 1;
}
function getTimeline() {
return Ox.LargeVideoTimeline({
chapters: self.chapters,
cuts: self.cuts,
duration: self.options.duration,
getImageURL: self.options.getLargeTimelineURL,
'in': self.options['in'],
out: self.options.out,
position: self.options.position,
subtitles: self.options.enableSubtitles ? self.options.subtitles : [],
type: self.options.timeline,
width: getTimelineWidth()
})
.css({left: '4px', top: '4px'})
.bindEvent({
mousedown: that.gainFocus,
position: dragendTimeline,
positioning: dragTimeline,
});
}
function getTimelineWidth() {
return self.options.width
- (self.options.showClips && !self.fullscreen)
* self.options.clipSize - 16 - 1;
}
function goToPoint(point) {
setPosition(self.options[point]);
}
function movePositionBy(sec) {
setPosition(Ox.limit(self.options.position + sec, 0, self.options.duration));
}
function movePositionTo(type, direction) {
setPosition(getNextPosition(type, direction));
}
function resizeClips(data) {
// called on clips resize
self.options.clipSize = data.size;
self.$video.options({
width: getPlayerWidth()
});
self.$timeline.options({
width: getTimelineWidth()
});
self.$clipPanel.options({width: data.size});
}
function resizeendClips(data) {
that.triggerEvent('size', data.size);
}
function setPoint(point, position, triggerEvent) {
self.options[point] = position;
self.$video.options(point, position);
self.$timeline.options(point, position);
self.$clipPanel.options(point, position);
if (self.options['in'] > self.options.out) {
setPoint(point == 'in' ? 'out' : 'in', position);
}
if (triggerEvent) {
that.triggerEvent('points', {
'in': self.options['in'],
out: self.options.out,
position: self.options.position
});
}
}
function setPosition(position, playing, dragging) {
var minute = Math.floor(position / 60),
previousMinute = Math.floor(self.options.position / 60);
self.options.position = position;
!playing && self.$video.options({position: self.options.position});
self.$timeline.options({position: self.options.position});
self.$clipPanel.options({position: self.options.position});
if ((!playing || minute != previousMinute) && !dragging) {
that.triggerEvent('position', {
position: !playing ? self.options.position : minute * 60
});
}
}
function toggleClips(data) {
self.options.showClips = !data.collapsed;
self.$video.options({
width: getPlayerWidth()
});
self.$timeline.options({
width: getTimelineWidth()
});
that.triggerEvent('toggleclips', {
showClips: self.options.showClips
});
}
function toggleControls(data) {
self.options.showTimeline = !data.collapsed;
self.$video.options({
height: getPlayerHeight()
});
that.triggerEvent('toggletimeline', {
showTimeline: self.options.showTimeline
});
}
function toggleMuted() {
self.$video.toggleMuted();
}
function togglePaused() {
self.$video.togglePaused();
self.$video.options('paused') && that.triggerEvent('position', {
position: self.$video.options('position')
});
}
that.getPasteIndex = function() {
return self.$clipPanel.getPasteIndex();
};
that.getSelectedClips = function() {
return self.options.selected.length ? self.options.selected.map(function(id) {
var clip = getClipById(id);
return (
clip.annotation || clip.item + '/' + clip['in'] + '-' + clip.out
) + '/' + id;
}) : getClipsInSelection();
};
that.invertSelection = function() {
self.$clipPanel.invertSelection();
};
that.selectAll = function() {
self.$clipPanel.selectAll();
};
that.updateClip = function(id, data) {
self.options.clips[Ox.getIndexById(self.options.clips, id)] = data;
self.$clipPanel.updateItem(id, data);
};
return that;
};

View file

@ -0,0 +1,506 @@
'use strict';
/*@
Ox.VideoElement <f> VideoElement Object
options <o> Options object
autoplay <b|false> autoplay
items <a|[]> array of objects with src,in,out,duration
loop <b|false> loop playback
self <o> Shared private variable
([options[, self]]) -> <o:Ox.Element> VideoElement Object
loadedmetadata <!> loadedmetadata
itemchange <!> itemchange
seeked <!> seeked
seeking <!> seeking
sizechange <!> sizechange
ended <!> ended
@*/
Ox.VideoElement = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
autoplay: false,
loop: false,
items: []
})
.options(options || {})
.update({
items: function() {
self.loadedMetadata = false;
loadItems(function() {
self.loadedMetadata = true;
var update = true;
if (self.currentItem >= self.numberOfItems) {
self.currentItem = 0;
}
if (!self.numberOfItems) {
self.video.src = '';
that.triggerEvent('durationchange', {
duration: that.duration()
});
} else {
if (self.currentItemId != self.items[self.currentItem].id) {
// check if current item is in new items
self.items.some(function(item, i) {
if (item.id == self.currentItemId) {
self.currentItem = i;
loadNextVideo();
update = false;
return true;
}
});
if (update) {
self.currentItem = 0;
self.currentItemId = self.items[self.currentItem].id;
}
}
if (!update) {
that.triggerEvent('seeked');
that.triggerEvent('durationchange', {
duration: that.duration()
});
} else {
setCurrentVideo(function() {
that.triggerEvent('seeked');
that.triggerEvent('durationchange', {
duration: that.duration()
});
});
}
}
});
}
})
.css({width: '100%', height: '100%'});
Ox.Log('Video', 'VIDEO ELEMENT OPTIONS', self.options);
self.currentItem = 0;
self.currentTime = 0;
self.currentVideo = 0;
self.loadedMetadata = false;
self.paused = true;
self.seeking = false;
self.loading = true;
self.buffering = true;
self.$videos = [getVideo(), getVideo()];
self.$video = self.$videos[self.currentVideo];
self.video = self.$video[0];
self.$brightness = $('<div>').css({
width: '100%',
height: '100%',
background: 'rgb(0, 0, 0)',
opacity: 0
})
.appendTo(that);
self.timeupdate = setInterval(function() {
if (!self.paused
&& !self.loading
&& self.loadedMetadata
&& self.items[self.currentItem]
&& self.items[self.currentItem].out
&& self.video.currentTime >= self.items[self.currentItem].out) {
setCurrentItem(self.currentItem + 1);
}
}, 30);
loadItems(function() {
setCurrentItem(0);
self.options.autoplay && setTimeout(function() {
that.play();
});
});
function getCurrentTime() {
var item = self.items[self.currentItem];
return self.seeking || self.loading
? self.currentTime
: item ? item.position + self.video.currentTime - item['in'] : 0;
}
function getset(key, value) {
var ret;
if (Ox.isUndefined(value)) {
ret = self.video[key];
} else {
self.video[key] = value;
ret = that;
}
return ret;
}
function getVideo() {
return $('<video>')
.css({position: 'absolute'})
.on({
ended: function() {
if (self.video == this) {
setCurrentItem(self.currentItem + 1);
}
},
loadedmetadata: function() {
// metadata loaded in loadItems
},
progress: function() {
// stop buffering if buffered to end point
if (self.video == this && self.buffering) {
var item = self.items[self.currentItem];
Ox.range(self.video.buffered.length).forEach(function(i) {
if (self.video.buffered.start(i) <= item['in']
&& self.video.buffered.end(i) >= item.out) {
self.video.preload = 'none';
self.buffering = false;
}
});
}
},
seeking: function() {
//seeking event triggered in setCurrentTime
},
stop: function() {
if (self.video == this) {
self.video.pause();
that.triggerEvent('ended');
}
}
})
.attr({
preload: 'auto'
})
.hide()
.appendTo(that);
}
function isReady($video, callback) {
if ($video[0].seeking) {
$video.one('seeked', function(event) {
callback($video[0]);
});
} else if ($video[0].readyState) {
callback($video[0]);
} else {
$video.one('loadedmetadata', function(event) {
callback($video[0]);
});
}
}
function loadItems(callback) {
var currentTime = 0,
items = self.options.items.map(function(item) {
return Ox.isObject(item) ? Ox.clone(item, true) : {src: item};
});
Ox.serialForEach(items,
function(item) {
var callback = Ox.last(arguments);
item['in'] = item['in'] || 0;
item.position = currentTime;
if (item.out) {
item.duration = item.out - item['in'];
}
if (item.duration) {
if (!item.out) {
item.out = item.duration;
}
currentTime += item.duration;
item.id = getId(item);
callback()
} else {
Ox.getVideoInfo(item.src, function(info) {
item.duration = info.duration;
if (!item.out) {
item.out = item.duration;
}
currentTime += item.duration;
item.id = getId(item);
callback();
});
}
},
function() {
self.items = items;
self.numberOfItems = self.items.length;
callback && callback();
}
);
function getId(item) {
return item.id || item.src + '/' + item['in'] + '-' + item.out;
}
}
function loadNextVideo() {
if (self.numberOfItems <= 1) {
return;
}
var item = self.items[self.currentItem],
nextItem = Ox.mod(self.currentItem + 1, self.numberOfItems),
next = self.items[nextItem],
$nextVideo = self.$videos[Ox.mod(self.currentVideo + 1, self.$videos.length)],
nextVideo = $nextVideo[0];
$nextVideo.one('loadedmetadata', function() {
if (self.video != nextVideo) {
nextVideo.currentTime = next['in'] || 0;
}
});
nextVideo.src = next.src;
nextVideo.preload = 'auto';
}
function setCurrentItem(item) {
Ox.Log('Video', 'sCI', item, self.numberOfItems);
var interval;
if (item >= self.numberOfItems || item < 0) {
if (self.options.loop) {
item = Ox.mod(item, self.numberOfItems);
} else {
self.seeking = false;
self.ended = true;
self.paused = true;
self.video && self.video.pause();
that.triggerEvent('ended');
return;
}
}
self.video && self.video.pause();
self.currentItem = item;
self.currentItemId = self.items[self.currentItem].id;
setCurrentVideo(function() {
if (!self.loadedMetadata) {
self.loadedMetadata = true;
that.triggerEvent('loadedmetadata');
}
Ox.Log('Video', 'sCI', 'trigger itemchange',
self.items[self.currentItem]['in'], self.video.currentTime, self.video.seeking);
that.triggerEvent('sizechange');
that.triggerEvent('itemchange', {
item: self.currentItem
});
});
}
function setCurrentVideo(callback) {
var css = {},
muted = false,
volume = 1,
item = self.items[self.currentItem],
next;
Ox.Log('Video', 'sCV', item);
['left', 'top', 'width', 'height'].forEach(function(key) {
css[key] = self.$videos[self.currentVideo].css(key);
});
self.currentTime = item.position;
self.loading = true;
if (self.video) {
self.$videos[self.currentVideo].hide();
self.video.pause();
volume = self.video.volume;
muted = self.video.muted;
}
self.currentVideo = Ox.mod(self.currentVideo + 1, self.$videos.length);
self.$video = self.$videos[self.currentVideo];
self.video = self.$video[0];
if (self.$video.attr('src') != item.src) {
self.loadedMetadata && Ox.Log('Video', 'caching next item failed, reset src');
self.video.src = item.src;
self.video.preload = 'auto';
}
self.video.volume = volume;
self.video.muted = muted;
self.$video.css(css);
self.buffering = true;
Ox.Log('Video', 'sCV', self.video.src, item['in'],
self.video.currentTime, self.video.seeking);
isReady(self.$video, function(video) {
self.$video.one('seeked', function() {
self.loading = false;
!self.paused && self.video.play();
self.$video.show();
callback && callback();
loadNextVideo();
});
if (!self.seeking) {
Ox.Log('Video', 'sCV set in', video.src, item['in'] || 0, video.currentTime, video.seeking);
video.currentTime = item['in'] || 0;
}
});
}
function setCurrentItemTime(currentTime) {
Ox.Log('Video', 'sCIT', currentTime, self.video.currentTime,
'delta', currentTime - self.video.currentTime);
isReady(self.$video, function(video) {
if (self.video == video) {
if(self.seeking) {
self.$video.one('seeked', function() {
that.triggerEvent('seeked');
self.seeking = false;
});
}
video.currentTime = currentTime;
}
});
}
function setCurrentTime(time) {
Ox.Log('Video', 'sCT', time);
var currentTime, currentItem;
self.items.forEach(function(item, i) {
if (time >= item.position
&& time < item.position + item.duration) {
currentItem = i;
currentTime = time - item.position + item['in'];
return false;
}
});
if (self.items.length) {
// Set to end of items if time > duration
if (Ox.isUndefined(currentItem) && Ox.isUndefined(currentTime)) {
currentItem = self.items.length - 1;
currentTime = self.items[currentItem].duration + self.items[currentItem]['in'];
}
Ox.Log('Video', 'sCT', time, '=>', currentItem, currentTime);
if (currentItem != self.currentItem) {
setCurrentItem(currentItem);
}
self.seeking = true;
self.currentTime = time;
that.triggerEvent('seeking');
setCurrentItemTime(currentTime);
} else {
self.currentTime = 0;
}
}
/*@
animate <f> animate
@*/
that.animate = function() {
self.$video.animate.apply(self.$video, arguments);
return that;
};
/*@
brightness <f> get/set brightness
@*/
that.brightness = function() {
var ret;
if (arguments.length == 0) {
ret = 1 - parseFloat(self.$brightness.css('opacity'));
} else {
self.$brightness.css({opacity: 1 - arguments[0]});
ret = that;
}
return ret;
};
/*@
buffered <f> buffered
@*/
that.buffered = function() {
return self.video.buffered;
};
/*@
currentTime <f> get/set currentTime
@*/
that.currentTime = function() {
var ret;
if (arguments.length == 0) {
ret = getCurrentTime();
} else {
self.ended = false;
setCurrentTime(arguments[0]);
ret = that;
}
return ret;
};
/*@
css <f> css
@*/
that.css = function() {
self.$video.css.apply(self.$video, arguments);
return that;
};
/*@
duration <f> duration
@*/
that.duration = function() {
return self.items ? Ox.sum(self.items.map(function(item) {
return item.duration;
})) : NaN;
};
/*@
muted <f> get/set muted
@*/
that.muted = function() {
return getset('muted', arguments[0]);
};
/*@
pause <f> pause
@*/
that.pause = function() {
self.paused = true;
self.video.pause();
return that;
};
/*@
play <f> play
@*/
that.play = function() {
if (self.ended) {
that.currentTime(0);
}
isReady(self.$video, function(video) {
self.ended = false;
self.paused = false;
self.seeking = false;
video.play();
});
return that;
};
that.removeElement = function() {
self.currentTime = getCurrentTime();
self.loading = true;
clearInterval(self.timeupdate);
//Chrome does not properly release resources, reset manually
//http://code.google.com/p/chromium/issues/detail?id=31014
self.$videos.forEach(function($video) {
$video.attr({src: ''});
});
return Ox.Element.prototype.removeElement.apply(that, arguments);
};
/*@
videoHeight <f> get videoHeight
@*/
that.videoHeight = function() {
return self.video.videoHeight;
};
/*@
videoWidth <f> get videoWidth
@*/
that.videoWidth = function() {
return self.video.videoWidth;
};
/*@
volume <f> get/set volume
@*/
that.volume = function(value) {
return getset('volume', arguments[0]);
};
return that;
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,83 @@
Ox.VideoPlayerMenu = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
items: []
})
.options(options || {})
.update({
// ...
})
.on({
click: function(e) {
var $target = $(e.target), group, id;
that.hide();
if (
!$target.is('.OxLine')
&& !$target.is('.OxSpace')
&& !$target.is('.OxDisabled')
) {
group = $target.parent().data().group;
id = $target.parent().data().id;
self.$items.filter(function($item) {
return $item.data().group == group;
}).forEach(function($item) {
$($item.children()[1]).attr({
src: Ox.UI.getImageURL('symbol' + (
$item.data().id == id ? 'Check' : 'None'
))
});
});
that.triggerEvent('click', {
group: group,
id: id
});
}
}
});
self.$items = [];
self.height = 2;
self.options.items.forEach(function(item) {
var $item;
if (!Ox.isEmpty(item)) {
$item = $('<div>')
.addClass('OxItem' + (item.disabled ? ' OxDisabled' : ''))
.data({
group: item.group,
id: item.id
})
.appendTo(that);
if (!item.disabled) {
$item.on({
mouseenter: function() {
$(this).addClass('OxSelected');
},
mouseleave: function() {
$(this).removeClass('OxSelected');
}
});
}
$('<div>').html(item.title).appendTo($item);
$('<img>').attr({
src: Ox.UI.getImageURL(
'symbol' + (item.checked ? 'Check' : 'None')
)
}).appendTo($item);
self.$items.push($item);
self.height += 14;
} else {
$('<div>').addClass('OxSpace').appendTo(that);
$('<div>').addClass('OxLine').appendTo(that);
$('<div>').addClass('OxSpace').appendTo(that);
self.height += 5;
}
});
that.css({height: self.height + 'px'});
return that;
};

View file

@ -0,0 +1,696 @@
'use strict';
/*@
Ox.VideoPlayerPanel <f> VideoPlayerPanel Object
options <o> Options object
self <o> Shared private variable
([options[, self]]) -> <o:Ox.SplitPanel> VideoPlayerPanel Object
annotationsrange <!> annotationsrange
annotationssize <!> annotationssize
annotationssort <!> annotationssort
audioTrack <s|''> Two-letter ISO 639-1 language code or track name
censored <!> censored
downloadvideo <!> downloadvideo
find <!> find
info <!> info
key_* <!> key_*
muted <!> muted
paused <!> paused
position <!> position
resizecalendar <!> resizecalendar
resolution <!> resolution
scaleToFill <!> scale
selected <!> select
subtitles <!> subtitles
toggleannotations <!> toggleannotations
togglecalendar <!> togglecalendar
togglelayer <!> togglelayer
togglemap <!> togglemap
toggletimeline <!> toggletimeline
volume <!> volume
@*/
Ox.VideoPlayerPanel = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
annotationsCalendarSize: 256,
annotationsMapSize: 256,
annotationsRange: 'all',
annotationsSize: 256,
annotationsSort: 'position',
annotationsTooltip: 'annotations',
audioTrack: '',
censored: [],
censoredIcon: '',
censoredTooltip: '',
clickLink: null,
cuts: [],
duration: 0,
enableDownload: false,
enableSubtitles: false,
find: '',
fps: 25,
fullscreen: false,
getLargeTimelineURL: null,
height: 0,
'in': 0,
itemName: {singular: 'video', plural: 'videos'},
layers: [],
loop: false,
muted: false,
out: 0,
paused: true,
playInToOut: false,
position: 0,
poster: '',
resolution: 0,
scaleToFill: false,
selected: '',
showAnnotations: false,
showAnnotationsCalendar: false,
showAnnotationsMap: false,
showLayers: {},
showTimeline: false,
showUsers: false,
smallTimelineURL: '',
subtitles: [],
subtitlesDefaultTrack: 'English',
subtitlesLayer: null,
subtitlesTrack: 'English',
timeline: '',
timelineTooltip: 'timeline',
video: '',
volume: 1,
width: 0
})
.options(options || {})
.update({
fullscreen: function() {
self.$video.options({fullscreen: self.options.fullscreen});
},
height: function() {
self.$video.options({height: getPlayerHeight()});
},
'in': function() {
setPoint('in', self.options['in']);
},
loop: function() {
self.$video.options({loop: self.options.loop});
},
out: function() {
setPoint('out', self.options.out);
},
paused: function() {
self.$video.options({paused: self.options.paused});
},
position: function() {
self.$video.options({position: self.options.position});
self.$timeline.options({position: self.options.position});
self.$annotationPanel.options({position: self.options.position});
},
selected: function() {
self.$annotationPanel.options({selected: self.options.selected});
},
showAnnotations: function() {
self.$mainPanel.toggle(1);
},
showTimeline: function() {
self.$videoPanel.toggle(1);
},
timeline: function() {
self.$timeline.options({type: self.options.timeline});
},
width: function() {
self.$video.options({width: getPlayerWidth()});
self.$timeline.options({width: getTimelineWidth()});
}
})
.css({
height: self.options.height + 'px',
width: self.options.width + 'px'
})
.bindEvent({
resize: resizeElement,
key_0: toggleMuted,
key_comma: function() {
movePositionTo('cut', -1);
},
key_control_c: function() {
that.triggerEvent('copy', [{
annotation: self.options.selected,
'in': self.options['in'],
out: self.options.out
}]);
},
key_control_shift_c: function() {
that.triggerEvent('copyadd', [{
annotation: self.options.selected,
'in': self.options['in'],
out: self.options.out
}]);
},
key_dot: function() {
movePositionTo('cut', 1);
},
key_equal: function() {
self.$video.changeVolume(0.1);
},
key_i: function() {
self.$annotationPanel.options({selected: ''});
setPoint('in', self.options.position, false, true);
},
key_l: toggleLoop,
key_left: function() {
movePositionBy(-1 / self.options.fps);
},
key_minus: function() {
self.$video.changeVolume(-0.1);
},
key_o: function() {
self.$annotationPanel.options({selected: ''});
setPoint('out', self.options.position, false, true);
},
key_p: playInToOut,
key_right: function() {
movePositionBy(1 / self.options.fps);
},
key_shift_down: function() {
movePositionBy(self.options.duration);
},
key_shift_i: function() {
goToPoint('in');
},
key_shift_left: function() {
movePositionBy(-1);
},
key_shift_o: function() {
goToPoint('out');
},
key_shift_right: function() {
movePositionBy(1);
},
key_shift_up: function() {
movePositionBy(-self.options.position);
},
key_slash: selectCut,
key_space: togglePaused
});
self.options.subtitles = options.subtitles !== void 0
? self.options.subtitles : parseSubtitles();
self.fullscreen = false;
self.results = [];
self.$player = Ox.Element().css({overflow: 'hidden'});
self.$video = Ox.VideoPlayer({
annotations: getAnnotations(),
audioTrack: self.options.audioTrack,
censored: self.options.censored,
censoredIcon: self.options.censoredIcon,
censoredTooltip: self.options.censoredTooltip,
controlsTop: ['fullscreen', 'title', 'find'],
controlsBottom: [
'play', 'playInToOut', 'volume', 'scale',
'timeline', 'loop', 'position', 'settings'
],
enableDownload: self.options.enableDownload,
enableFind: true,
enableKeyboard: true,
enableMouse: true,
enablePosition: true,
enableSubtitles: self.options.enableSubtitles,
enableTimeline: true,
find: self.options.find,
fullscreen: self.options.fullscreen,
height: getPlayerHeight(),
'in': self.options['in'],
loop: self.options.loop,
muted: self.options.muted,
out: self.options.out,
paused: self.options.paused,
position: self.options.position,
resolution: self.options.resolution,
scaleToFill: self.options.scaleToFill,
subtitles: Ox.clone(self.options.subtitles, true),
subtitlesDefaultTrack: self.options.subtitlesDefaultTrack,
subtitlesTrack: self.options.subtitlesTrack,
timeline: self.options.smallTimelineURL,
video: self.options.video,
volume: self.options.volume,
width: getPlayerWidth()
})
.bindEvent({
censored: function() {
that.triggerEvent('censored');
},
download: function(data) {
that.triggerEvent('downloadvideo', data);
},
find: function(data) {
self.$timeline.options({find: data.find});
self.$annotationPanel.options({highlight: data.find});
that.triggerEvent('find', data);
},
fullscreen: function(data) {
self.options.fullscreen = data.fullscreen;
},
loop: function(data) {
that.triggerEvent('loop', data);
},
muted: function(data) {
that.triggerEvent('muted', data);
},
paused: function(data) {
self.options.paused = data.paused;
that.triggerEvent('paused', data);
},
playing: function(data) {
setPosition(data.position, true);
},
position: function(data) {
setPosition(data.position);
},
positioning: function(data) {
setPosition(data.position, false, true);
},
resolution: function(data) {
that.triggerEvent('resolution', data);
},
scale: function(data) {
that.triggerEvent('scale', data);
},
select: selectAnnotation,
subtitles: function(data) {
self.options.enableSubtitles = data.subtitles;
self.$timeline.options({subtitles: getSubtitles()});
that.triggerEvent('subtitles', data);
},
subtitlestrack: function(data) {
self.options.subtitlesTrack = data.track;
self.$timeline.options({subtitles: getSubtitles()});
},
volume: function(data) {
that.triggerEvent('volume', data);
}
})
.appendTo(self.$player);
self.$controls = Ox.Element()
.addClass('OxMedia')
.bindEvent({
toggle: toggleControls
});
self.$timeline = Ox.LargeVideoTimeline({
cuts: self.options.cuts,
duration: self.options.duration,
find: self.options.find,
getImageURL: self.options.getLargeTimelineURL,
'in': self.options['in'],
out: self.options.out,
position: self.options.position,
subtitles: getSubtitles(),
videoId: self.options.videoId, // fixme: not in defaults
type: self.options.timeline,
width: getTimelineWidth()
})
.css({left: '4px', top: '4px'})
.bindEvent({
mousedown: that.gainFocus,
position: dragendTimeline,
positioning: dragTimeline
})
.appendTo(self.$controls);
self.$videoPanel = Ox.SplitPanel({
elements: [
{
element: self.$player
},
{
collapsed: !self.options.showTimeline,
collapsible: true,
element: self.$controls,
size: 80,
tooltip: self.options.timelineTooltip
}
],
orientation: 'vertical'
});
self.$annotationPanel = Ox.AnnotationPanel({
calendarSize: self.options.annotationsCalendarSize,
clickLink: self.options.clickLink,
editable: false,
highlight: self.options.find,
'in': self.options['in'],
itemName: self.options.itemName,
layers: self.options.layers,
mapSize: self.options.annotationsMapSize,
out: self.options.out,
position: self.options.position,
range: self.options.annotationsRange,
selected: self.options.selected,
showCalendar: self.options.showAnnotationsCalendar,
showLayers: Ox.clone(self.options.showLayers),
showMap: self.options.showAnnotationsMap,
showUsers: self.options.showUsers,
sort: self.options.annotationsSort,
width: self.options.annotationsSize
})
.bindEvent({
annotationsrange: function(data) {
self.options.annotationsRange = data.range;
that.triggerEvent('annotationsrange', data);
},
annotationssort: function(data) {
self.options.annotationsSort = data.sort;
that.triggerEvent('annotationssort', data);
},
info: function(data) {
that.triggerEvent('info', data);
},
open: function() {
setPosition(self.options['in']);
},
resize: resizeAnnotations,
resizeend: resizeendAnnotations,
resizecalendar: function(data) {
that.triggerEvent('resizecalendar', data);
},
resizemap: function(data) {
that.triggerEvent('resizemap', data);
},
select: selectAnnotation,
toggle: toggleAnnotations,
togglecalendar: function(data) {
self.options.showAnnotationsCalendar = !data.collapsed;
that.triggerEvent('togglecalendar', data);
},
togglelayer: function(data) {
that.triggerEvent('togglelayer', {
collapsed: data.collapsed,
layer: data.layer
});
},
togglemap: function(data) {
self.options.showAnnotationsMap = !data.collapsed;
that.triggerEvent('togglemap', data);
}
});
[
'0', 'b', 'backslash', 'closebracket', 'comma', 'dot', 'equal', 'f',
'g', 'i', 'minus', 'n', 'o', 'openbracket', 'p', 'shift_0', 'shift_g',
'shift_i', 'shift_o', 'slash', 'space'
].forEach(function(key) {
key = 'key_' + key;
// FIXME: video player and timeline, too
self.$annotationPanel.bindEvent(key, function() {
that.triggerEvent(key);
});
});
that.setElement(
self.$mainPanel = Ox.SplitPanel({
elements: [
{
element: self.$videoPanel
},
{
collapsed: !self.options.showAnnotations,
collapsible: true,
element: self.$annotationPanel,
resizable: true,
resize: [192, 256, 320, 384],
size: self.options.annotationsSize,
tooltip: self.options.annotationsTooltip
}
],
orientation: 'horizontal'
})
);
function dragTimeline(data) {
self.options.position = data.position;
self.$video.options({position: self.options.position});
self.$annotationPanel.options({position: self.options.position});
}
function dragendTimeline(data) {
dragTimeline(data);
that.triggerEvent('position', {position: self.options.position});
}
function getAnnotations() {
return Ox.flatten(self.options.layers.map(function(layer) {
return layer.items.map(function(item) {
return {id: item.id, 'in': item['in'], out: item.out, text: item.value};
});
})).sort(sortAnnotations);
}
// fixme: why not goToNextPosition()?
function getNextPosition(type, direction) {
// type can only be 'cut'
var positions;
if (type == 'cut') {
positions = [0].concat(self.options.cuts, self.options.duration);
}
return Ox.nextValue(positions, self.options.position, direction);
}
function getPlayerHeight() {
return self.options.height
- self.options.showTimeline * 80 - 1;
}
function getPlayerWidth() {
return self.options.width
- (self.options.showAnnotations && !self.fullscreen)
* self.options.annotationsSize - 1;
}
function getSubtitles() {
return self.options.enableSubtitles
? self.options.subtitles.filter(function(v) {
return Ox.contains(v.tracks, self.options.subtitlesTrack);
})
: [];
}
function getTimelineWidth() {
return self.options.width
- (self.options.showAnnotations && !self.fullscreen)
* self.options.annotationsSize - 16 - 1;
}
function goToPoint(point) {
setPosition(self.options[point]);
}
function playInToOut() {
self.$video.playInToOut();
}
function movePositionBy(sec) {
setPosition(Ox.limit(self.options.position + sec, 0, self.options.duration));
}
function movePositionTo(type, direction) {
setPosition(getNextPosition(type, direction));
}
function parseSubtitles() {
return self.options.subtitlesLayer ? self.options.layers.filter(function(layer) {
return layer.id == self.options.subtitlesLayer;
})[0].items.map(function(subtitle) {
return {
id: subtitle.id,
'in': subtitle['in'],
out: subtitle.out,
text: subtitle.value.replace(/\n/g, ' ').replace(/<br\/?>/g, '\n'),
tracks: subtitle.languages || [self.options.subtitlesDefaultTrack]
};
}) : [];
}
function resizeAnnotations(data) {
// called on annotations resize
self.options.annotationsSize = data.size;
self.$video.options({
width: getPlayerWidth()
});
self.$timeline.options({
width: getTimelineWidth()
});
self.$annotationPanel.options({width: data.size});
}
function resizeendAnnotations(data) {
that.triggerEvent('annotationssize', data.size);
}
function resizeElement(data) {
// called on browser toggle
self.options.height = data.size;
self.$video.options({
height: getPlayerHeight()
});
}
/*
function resizePanel(data) {
// called on annotations toggle <-- FIXME: NOT TRUE
self.$video.options({
width: getPlayerWidth()
});
self.$timeline.options({
width: getTimelineWidth()
});
}
*/
function selectAnnotation(data) {
self.options.selected = data.id;
if (self.options.selected) {
setPosition(data['in']);
setPoint('in', data['in'], true);
setPoint('out', data.out, true);
}
self.$annotationPanel.options({selected: self.options.selected});
that.triggerEvent('select', {id: self.options.selected});
}
function selectCut() {
var points = {
'in': Ox.last(self.options.cuts),
out: self.options.duration
};
Ox.forEach(self.options.cuts, function(cut, i) {
if (cut > self.options.position) {
points = {
'in': i == 0 ? 0 : self.options.cuts[i - 1],
out: cut - 1 / self.options.fps
};
return false; // break
}
});
setPoint('in', points['in']);
setPoint('out', points.out);
}
function setPoint(point, position, keepSelected, triggerEvent) {
self.options[point] = position;
if (self.options.selected && !keepSelected) {
selectAnnotation({id: ''});
}
self.$video.options(point, position);
self.$timeline.options(point, position);
self.$annotationPanel.options(point, position);
if (self.options['in'] > self.options.out) {
setPoint(point == 'in' ? 'out' : 'in', position, keepSelected);
} else if (triggerEvent) {
that.triggerEvent('points', {
'in': self.options['in'],
out: self.options.out,
position: self.options.position
});
}
}
function setPosition(position, playing, dragging) {
var minute = Math.floor(position / 60),
previousMinute = Math.floor(self.options.position / 60);
self.options.position = position;
!playing && self.$video.options({position: self.options.position});
self.$timeline.options({position: self.options.position});
self.$annotationPanel.options({position: self.options.position});
if ((!playing || minute != previousMinute) && !dragging) {
that.triggerEvent('position', {
position: !playing ? self.options.position : minute * 60
});
}
}
function sortAnnotations(a, b) {
var ret = 0;
if (a['in'] < b['in']) {
ret = -1;
} else if (a['in'] > b['in']) {
ret = 1;
} else if (a.out < b.out) {
ret = -1;
} else if (a.out > b.out) {
ret = 1;
} else if (a.value < b.value) {
ret = -1;
} else if (a.value > b.value) {
ret = 1;
}
return ret;
}
function toggleAnnotations(data) {
self.options.showAnnotations = !data.collapsed;
self.$video.options({
width: getPlayerWidth()
});
self.$timeline.options({
width: getTimelineWidth()
});
that.triggerEvent('toggleannotations', {
showAnnotations: self.options.showAnnotations
});
}
function toggleControls(data) {
self.options.showTimeline = !data.collapsed;
self.$video.options({
height: getPlayerHeight()
});
that.triggerEvent('toggletimeline', {
showTimeline: self.options.showTimeline
});
}
function toggleLoop() {
self.$video.toggleLoop();
}
function toggleMuted() {
self.$video.toggleMuted();
}
function togglePaused() {
self.$video.togglePaused();
self.$video.options('paused') && that.triggerEvent('position', {
position: self.$video.options('position')
});
}
// fixme: can these be removed?
/*@
toggleAnnotations <f> toggle annotations
() -> <o> toggle visibility of annotations
@*/
that.toggleAnnotations = function() {
self.$mainPanel.toggle(1);
};
/*@
toggleTimeline <f> toggle timeline
() -> <o> toggle visibility of timeline
@*/
that.toggleTimeline = function() {
self.$videoPanel.toggle(1);
};
return that;
}

View file

@ -0,0 +1,179 @@
'use strict';
/*@
Ox.VideoPreview <f> Video Preview
options <o> Options
self <o> Shared private variable
([options[, self]]) -> <o:Ox.Element> Video Preview
click <!> click
@*/
Ox.VideoPreview = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
duration: 0,
getFrame: null,
fps: 25,
frameRatio: 16/9,
height: 256,
position: void 0,
scaleToFill: false,
timeline: '',
videoTooltip: null,
width: 256
})
.options(options || {})
.update({
height: function() {
that.css({height: self.options.height + 'px'});
self.$frame.css(getFrameCSS());
},
position: function() {
self.$frame.attr({src: self.options.getFrame(self.options.position)});
},
width: function() {
that.css({width: self.options.width + 'px'});
stopLoading();
self.$frame.attr({src: self.options.getFrame()})
.css(getFrameCSS());
self.$timeline && self.$timeline.css({width: self.options.width + 'px'});
}
})
.addClass('OxVideoPreview')
.css({
width: self.options.width + 'px',
height: self.options.height + 'px'
});
self.loaded = [];
self.queue = [];
self.$frameElement = Ox.$('<div>')
.addClass('OxFrame')
.appendTo(that);
self.$frame = Ox.$('<img>')
.attr({src: self.options.getFrame(self.options.position)})
.css(getFrameCSS())
.appendTo(self.$frameElement);
if (self.options.timeline) {
self.$timeline = $('<img>')
.addClass('OxTimeline')
.attr({src: self.options.timeline})
.css({width: self.options.width + 'px'})
.appendTo(that);
}
self.$interface = Ox.Element({
tooltip: function(event) {
// e.offsetX does not work in Firefox
var position = getPosition(event.clientX - that.offset().left),
tooltip = Ox.isFunction(self.options.videoTooltip)
? self.options.videoTooltip() : self.options.videoTooltip;
self.$frame.attr({src: getClosestFrame(position)});
self.timeout && clearTimeout(self.timeout);
self.timeout = setTimeout(function() {
self.$frame.attr({src: self.options.getFrame(position)});
}, 250);
return '<div style="text-align: center">'
+ (tooltip ? tooltip + '<br>' : '')
+ '<span class="OxLight">' + Ox.formatDuration(position, 2) + '</span>'
+ '</div>';
}
})
.addClass('OxInterface')
.on({
click: click,
mouseenter: startLoading,
mouseleave: function() {
stopLoading();
self.$frame.attr({src: self.options.getFrame(self.options.position)});
}
})
.appendTo(that);
function click(e) {
that.triggerEvent('click', {
// e.offsetX does not work in Firefox
position: getPosition(e.clientX - that.offset().left)
});
}
function getClosestFrame(position) {
return self.loaded.length == 0
? self.options.getFrame(self.options.position)
: self.loaded.sort(function(a, b) {
return Math.abs(a.position - position) - Math.abs(b.position - position);
})[0].frame;
}
function getFrameCSS() {
var css = {},
elementWidth = self.options.width,
elementHeight = self.options.height - (self.options.timeline ? 16 : 0),
elementRatio = elementWidth / elementHeight,
frameRatio = self.options.frameRatio,
frameIsWider = frameRatio > elementRatio;
if (self.options.scaleToFill) {
css.width = frameIsWider ? elementHeight * frameRatio : elementWidth;
css.height = frameIsWider ? elementHeight : elementWidth / frameRatio;
css.marginLeft = frameIsWider ? (elementWidth - css.width) / 2 : 0;
css.marginTop = frameIsWider ? 0 : (elementHeight - css.height) / 2;
} else {
css.width = frameIsWider ? elementWidth : elementHeight * frameRatio;
css.height = frameIsWider ? elementWidth / frameRatio : elementHeight;
css.marginLeft = frameIsWider ? 0 : (elementWidth - css.width) / 2;
css.marginTop = frameIsWider ? (elementHeight - css.height) / 2 : 0;
}
return Ox.map(css, function(value) {
return Math.round(value) + 'px';
});
}
function getPosition(x) {
return Math.round(
self.options.duration * x / self.options.width * self.options.fps
) / self.options.fps;
}
function startLoading() {
var last,
steps = [Math.round(self.options.width / 2)];
while ((last = steps[steps.length - 1]) > 1) {
steps.push(Math.round(last / 2));
}
steps.forEach(function(step) {
Ox.loop(0, self.options.width, step, function(x) {
var position = getPosition(x),
frame = self.options.getFrame(position);
if (!self.loaded.some(function(image) {
return image.frame == frame;
}) && !self.queue.some(function(image) {
return image.frame == frame;
})) {
self.queue.push({frame: frame, position: position});
}
});
});
self.queue.length && loadFrame();
function loadFrame() {
var image = self.queue.shift();
$('<img>')
.load(function() {
self.loaded.push(image);
self.queue.length && loadFrame();
})
.attr({src: image.frame})
}
}
function stopLoading() {
self.queue = [];
self.timeout && clearTimeout(self.timeout);
}
return that;
};

View file

@ -0,0 +1,323 @@
'use strict';
/*@
Ox.VideoTimelinePanel <f> Video timeline panel
options <o> Options
self <o> Shared private variable
([options[, self]]) -> <o:Ox.SplitPanel> Video timeline panel
annoationssize <!> annoationssize
annotationsrange <!> annotationsrange
annotationssort <!> annotationssort
audioTrack <s|''> Two-letter ISO 639-1 language code or track name
censored <!> censored
follow <!> follow
info <!> info
muted <!> muted
paused <!> paused
position <!> position
resizecalendar <!> resizecalendar
resizemap <!> resizemap
select <!> select
timeline <!> timeline
toggleannotations <!> toggleannotations
togglecalendar <!> togglecalendar
togglelayer <!> togglelayer
togglemap <!> togglemap
volume <!> volume
@*/
Ox.VideoTimelinePanel = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
annotationsCalendarSize: 256,
annotationsMapSize: 256,
annotationsRange: 'all',
annotationsSize: 256,
annotationsSort: 'position',
annotationsTooltip: 'annotations',
audioTrack: '',
censored: [],
censoredIcon: '',
censoredTooltip: '',
clickLink: null,
cuts: [],
duration: 0,
followPlayer: false,
getFrameURL: null,
getLargeTimelineURL: null,
height: 0,
'in': 0,
itemName: {singular: 'video', plural: 'videos'},
layers: [],
loop: false, // fixme: used?
muted: false,
out: 0,
paused: true,
position: 0,
resolution: 0,
selected: '',
showAnnotations: false,
showAnnotationsCalendar: false,
showAnnotationsMap: false,
showLayers: {},
showUsers: false,
smallTimelineURL: '',
subtitles: [],
timeline: '',
timelines: [],
video: '',
volume: 1,
width: 0
})
.options(options || {})
.update({
height: function() {
self.$player.options({height: self.options.height});
},
paused: function() {
self.$player.options({paused: self.options.paused});
},
position: function() {
setPosition(self.options.position);
},
showAnnotations: function() {
self.$panel.toggleElement(1);
},
timeline: function() {
self.$player.options({timeline: self.options.timeline});
},
width: function() {
self.$player.options({width: getPlayerWidth()});
}
})
.css({
height: self.options.height + 'px',
width: self.options.width + 'px'
})
.bindEvent({
resize: resizeElement,
key_0: toggleMuted,
key_equal: function() {
self.$video.changeVolume(0.1);
},
key_minus: function() {
self.$video.changeVolume(-0.1);
},
key_space: togglePaused
});
self.$player = Ox.VideoTimelinePlayer({
audioTrack: self.options.audioTrack,
censored: self.options.censored,
censoredIcon: self.options.censoredIcon,
censoredTooltip: self.options.censoredTooltip,
cuts: self.options.cuts,
duration: self.options.duration,
followPlayer: self.options.followPlayer,
getFrameURL: self.options.getFrameURL,
getLargeTimelineURL: self.options.getLargeTimelineURL,
height: self.options.height,
muted: self.options.muted,
paused: self.options.paused,
position: self.options.position,
resolution: self.options.resolution,
smallTimelineURL: self.options.smallTimelineURL,
subtitles: self.options.subtitles,
timeline: self.options.timeline,
timelines: self.options.timelines,
video: self.options.video,
videoRatio: self.options.videoRatio,
volume: self.options.volume,
width: getPlayerWidth()
})
.bindEvent({
censored: function() {
that.triggerEvent('censored');
},
follow: function(data) {
that.triggerEvent('follow', data);
},
muted: function(data) {
that.triggerEvent('muted', data);
},
paused: function(data) {
self.options.paused = data.paused;
that.triggerEvent('paused', data);
},
playing: function(data) {
setPosition(data.position, true);
},
position: function(data) {
setPosition(data.position);
},
timeline: function(data) {
that.triggerEvent('timeline', data);
},
volume: function(data) {
that.triggerEvent('volume', data);
}
});
self.$annotationPanel = Ox.AnnotationPanel({
calendarSize: self.options.annotationsCalendarSize,
clickLink: self.options.clickLink,
editable: false,
highlight: self.options.find,
'in': self.options['in'],
itemName: self.options.itemName,
layers: self.options.layers,
mapSize: self.options.annotationsMapSize,
out: self.options.out,
position: self.options.position,
range: self.options.annotationsRange,
selected: self.options.selected,
showCalendar: self.options.showAnnotationsCalendar,
showLayers: Ox.clone(self.options.showLayers),
showMap: self.options.showAnnotationsMap,
showUsers: self.options.showUsers,
sort: self.options.annotationsSort,
width: self.options.annotationsSize
})
.bindEvent({
annotationsrange: function(data) {
self.options.annotationsRange = data.range;
that.triggerEvent('annotationsrange', data);
},
annotationssort: function(data) {
self.options.annotationsSort = data.sort;
that.triggerEvent('annotationssort', data);
},
info: function(data) {
that.triggerEvent('info', data);
},
open: function() {
setPosition(self.options['in']);
},
resize: resizeAnnotations,
resizeend: resizeendAnnotations,
resizecalendar: function(data) {
that.triggerEvent('resizecalendar', data);
},
resizemap: function(data) {
that.triggerEvent('resizemap', data);
},
select: selectAnnotation,
toggle: toggleAnnotations,
togglecalendar: function(data) {
self.options.showAnnotationsCalendar = !data.collapsed;
that.triggerEvent('togglecalendar', data);
},
togglelayer: function(data) {
that.triggerEvent('togglelayer', {
collapsed: data.collapsed,
layer: data.layer
});
},
togglemap: function(data) {
self.options.showAnnotationsMap = !data.collapsed;
that.triggerEvent('togglemap', data);
}
});
that.setElement(
self.$panel = Ox.SplitPanel({
elements: [
{
element: self.$player
},
{
collapsed: !self.options.showAnnotations,
collapsible: true,
element: self.$annotationPanel,
resizable: true,
resize: [192, 256, 320, 384],
size: self.options.annotationsSize,
tooltip: self.options.annotationsTooltip
}
],
orientation: 'horizontal'
})
);
function getPlayerWidth() {
return self.options.width -
self.options.showAnnotations * self.options.annotationsSize - 1;
}
function resizeAnnotations(data) {
// called on annotations resize
self.options.annotationsSize = data.size;
self.$player.options({
width: getPlayerWidth()
});
self.$annotationPanel.options({width: data.size});
}
function resizeendAnnotations(data) {
that.triggerEvent('annotationssize', data.size);
}
function resizeElement(data) {
// called on browser toggle
self.options.height = data.size;
self.$player.options({
height: self.options.height
});
}
function selectAnnotation(data) {
self.options.selected = data.id;
if (self.options.selected) {
setPosition(data['in']);
}
self.$annotationPanel.options({selected: self.options.selected});
that.triggerEvent('select', {id: self.options.selected});
}
function setPosition(position, playing) {
var minute = Math.floor(position / 60),
previousMinute = Math.floor(self.options.position / 60);
self.options.position = position;
!playing && self.$player.options({position: self.options.position});
self.$annotationPanel.options({position: self.options.position});
if (!playing || minute != previousMinute) {
that.triggerEvent('position', {
position: !playing ? self.options.position : minute * 60
});
}
}
function toggleAnnotations(data) {
self.options.showAnnotations = !data.collapsed;
self.$player.options({
width: getPlayerWidth()
});
that.triggerEvent('toggleannotations', {
showAnnotations: self.options.showAnnotations
});
}
function toggleMuted() {
self.$player.toggleMuted();
}
function togglePaused() {
self.$player.togglePaused();
self.$player.options('paused') && that.triggerEvent('position', {
position: self.$player.options('position')
});
}
/*@
toggleAnnotations <f> toggle annotations
() -> <o> toggle annotations
@*/
that.toggleAnnotations = function() {
self.$panel.toggleElement(1);
};
return that;
};

View file

@ -0,0 +1,873 @@
'use strict';
/*@
Ox.VideoTimelinePlayer <f> Video Timeline Player
options <o> Options
self <o> Shared private variable
([options[, self]]) -> <o:Ox.SplitPanel> Video Timeline Player
censored <!> censored
follow <!> follow
muted <!> muted
playing <!> playing
position <!> position
timeline <!> timeline
volume <!> volume
@*/
Ox.VideoTimelinePlayer = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
audioTrack: '',
censored: [],
censoredIcon: '',
censoredTooltip: '',
cuts: [],
duration: 0,
find: '',
followPlayer: false,
getFrameURL: null,
getLargeTimelineURL: null,
height: 0,
'in': 0,
matches: [],
muted: false,
out: 0,
paused: false,
position: 0,
showMilliseconds: false,
smallTimelineURL: '',
subtitles: [],
timeline: '',
timelines: [],
video: '',
videoRatio: 1,
volume: 1,
width: 0
})
.options(options || {})
.update({
height: setHeight,
paused: function() {
self.options.paused = !self.options.paused;
togglePaused();
},
position: setPosition,
timeline: function() {
self.$menuButton.checkItem(self.options.timeline);
updateTimeline();
},
width: setWidth
});
self.fps = 25;
self.frame = self.options.position * self.fps;
self.frames = self.options.duration * self.fps;
self.tileWidth = 1500;
self.tileHeight = 64;
self.margin = 8;
self.contentWidth = self.options.width - 2 * self.margin;
self.contentHeight = self.options.height - 32;
self.positionWidth = 48
+ !!self.options.showMilliseconds * 2
+ self.options.showMilliseconds * 6;
self.tiles = Math.ceil(self.frames / self.tileWidth);
self.videoWidth = Math.round(self.tileHeight * self.options.videoRatio);
self.lines = getLines(); // may update self.contentWidth
self.videoLines = getVideoLines();
if (Ox.isObject(self.options.video[0])) {
self.audioTracks = Ox.sort(Ox.unique(
self.options.video.map(function(video) {
return video.track;
})
)).map(function(track) {
return {
id: track,
title: Ox._(track),
checked: self.options.audioTrack == track
};
});
}
self.$menubar = Ox.Bar({size: 16});
self.$menuButton = Ox.MenuButton({
items: [].concat(
self.audioTracks.length > 1 ? [
{id: 'audioTracks', title: Ox._('Audio'), items: [
{group: 'audioTrack', min: 1, max: 1, items: self.audioTracks}
]}
] : [],
[
{id: 'timelines', title: Ox._('Timeline'), items: [
{group: 'timeline', min: 1, max: 1, items: Ox.map(
self.options.timelines,
function(timeline) {
return Ox.extend({
checked: timeline.id == self.options.timeline
}, timeline);
}
)}
]},
{},
{
id: 'followPlayer',
title: 'Follow Player While Playing',
checked: self.options.followPlayer
}
]
),
style: 'square',
title: 'set',
tooltip: Ox._('Options'),
type: 'image'
})
.css({float: 'left'})
.bindEvent({
change: function(data) {
var id = data.id;
if (id == 'audioTrack') {
self.options.audioTrack = data.checked[0].id;
self.$video.options({audioTrack: self.options.audioTrack});
} else if (id == 'timeline') {
self.options.timeline = data.checked[0].id;
updateTimeline();
that.triggerEvent('timeline', {
timeline: self.options.timeline
});
} else if (id == 'followPlayer') {
self.options.followPlayer = data.checked;
if (!self.options.paused && self.options.followPlayer) {
self.scrollTimeout && clearTimeout(self.scrollTimeout);
scrollToPosition();
}
that.triggerEvent('follow', {
follow: self.options.followPlayer
});
}
}
})
.appendTo(self.$menubar);
self.$scrollButton = Ox.Button({
style: 'symbol',
title: 'arrowDown',
tooltip: Ox._('Scroll to Player'),
type: 'image'
})
.css({float: 'right'})
.hide()
.bindEvent({
click: function() {
self.scrollTimeout && clearTimeout(self.scrollTimeout);
scrollToPosition();
}
})
.appendTo(self.$menubar);
self.$timelinePlayer = Ox.Element()
.addClass('OxMedia')
.css({overflowX: 'hidden', overflowY: 'auto'})
.on({
mousedown: mousedown,
mouseleave: mouseleave,
mousemove: mousemove,
scroll: scroll
})
.bindEvent({
mousedown: function() {
this.gainFocus();
},
key_0: toggleMuted,
key_down: function() {
self.options.position += self.contentWidth / self.fps;
setPosition();
},
key_equal: function() {
changeVolume(0.1);
},
key_enter: function() {
scrollToPosition();
},
key_left: function() {
self.options.position -= self.videoWidth / self.fps;
setPosition();
},
key_minus: function() {
changeVolume(-0.1);
},
key_right: function() {
self.options.position += self.videoWidth / self.fps;
setPosition();
},
key_shift_left: function() {
self.options.position -= 1 / self.fps;
setPosition();
},
key_shift_right: function() {
self.options.position += 1 / self.fps;
setPosition();
},
key_space: function() {
togglePaused()
},
key_up: function() {
self.options.position -= self.contentWidth / self.fps;
setPosition();
}
});
self.$playerbar = Ox.Bar({size: 16});
self.$playButton = Ox.Button({
style: 'symbol',
title: 'play',
tooltip: Ox._('Play'),
type: 'image'
})
.css({
float: 'left'
})
.bindEvent({
click: function() {
togglePaused();
}
})
.appendTo(self.$playerbar);
self.$muteButton = Ox.Button({
style: 'symbol',
title: self.options.muted ? 'unmute' : 'mute',
tooltip: self.options.muted ? Ox._('Unmute') : Ox._('Mute'),
type: 'image'
})
.css({float: 'left'})
.bindEvent({
click: toggleMuted
})
.appendTo(self.$playerbar);
self.$smallTimeline = getSmallTimeline().appendTo(self.$playerbar);
self.$position = Ox.Element()
.addClass('OxPosition')
.css({
width: self.positionWidth - 4 + 'px'
})
.html(formatPosition())
.on({
click: function() {
if (!self.options.paused) {
self.playOnSubmit = true;
togglePaused();
}
self.$position.hide();
self.$positionInput
.value(formatPosition())
.show()
.focusInput(false);
}
})
.appendTo(self.$playerbar);
self.$positionInput = Ox.Input({
value: formatPosition(),
width: self.positionWidth
})
.addClass('OxPositionInput')
.bindEvent({
blur: submitPositionInput,
submit: submitPositionInput
})
.appendTo(self.$playerbar);
self.$positionInput.children('input').css({
width: (self.positionWidth - 6) + 'px',
fontSize: '9px'
});
self.$panel = Ox.SplitPanel({
elements: [
{
element: self.$menubar,
size: 16
},
{
element: self.$timelinePlayer
},
{
element: self.$playerbar,
size: 16
}
],
orientation: 'vertical'
})
.addClass('OxVideoTimelinePlayer');
that.setElement(self.$panel);
self.$lines = [];
self.$timelines = [];
self.$timeline = renderTimeline();
//setSubtitles();
Ox.loop(self.lines, function(i) {
addLine(i);
});
Ox.last(self.$lines).css({
height: self.tileHeight + 1.5 * self.margin + 'px'
});
self.$frameBox = $('<div>')
.addClass('OxVideoBox')
.css({
position: 'absolute',
right: 0,
top: self.margin / 2 - 1 + 'px',
width: self.videoWidth + 'px',
height: self.tileHeight + 'px'
})
.appendTo(self.$timelines[self.videoLines[1]][0]);
self.$frame = Ox.VideoPlayer({
audioTrack: self.options.audioTrack,
censored: self.options.censored,
censoredIcon: self.options.censoredIcon,
censoredTooltip: self.options.censoredTooltip,
duration: self.options.duration,
height: self.tileHeight,
position: self.options.position,
scaleToFill: true,
type: 'in',
video: self.options.getFrameURL,
width: self.videoWidth
})
.bindEvent({
censored: function() {
that.triggerEvent('censored');
}
})
.appendTo(self.$frameBox);
$('<div>')
.addClass('OxFrameInterface')
.css({
position: 'absolute',
left: 0,
top: 0,
width: self.videoWidth + 'px',
height: self.tileHeight + 'px'
})
.appendTo(self.$frameBox);
self.$videoBox = $('<div>')
.addClass('OxVideoBox')
.css({
position: 'absolute',
right: 0,
top: self.margin / 2 - 1 + 'px',
width: self.videoWidth + 'px',
height: self.tileHeight + 'px',
zIndex: 5
})
.appendTo(self.$timelines[self.videoLines[0]][0]);
self.$video = Ox.VideoPlayer({
audioTrack: self.options.audioTrack,
censored: self.options.censored,
censoredIcon: self.options.censoredIcon,
censoredTooltip: self.options.censoredTooltip,
duration: self.options.duration,
height: self.tileHeight,
muted: self.options.muted,
paused: self.options.paused,
position: self.options.position,
scaleToFill: true,
video: self.options.video,
width: self.videoWidth
})
.bindEvent({
censored: function() {
that.triggerEvent('censored');
},
ended: function() {
togglePaused(true);
},
playing: function(data) {
self.options.position = data.position;
setPosition(true);
}
})
.appendTo(self.$videoBox);
$('<div>')
.addClass('OxFrameInterface OxVideoInterface')
.css({
position: 'absolute',
left: 0,
top: 0,
width: self.videoWidth + 'px',
height: self.tileHeight + 'px'
})
.appendTo(self.$videoBox);
self.$tooltip = Ox.Tooltip({
animate: false
})
.css({
textAlign: 'center'
});
setTimeout(function() {
scrollToPosition();
});
function addLine(i) {
self.$lines[i] = $('<div>')
.css({
position: 'absolute',
left: self.margin + 'px',
top: self.margin / 2 + i * (self.tileHeight + self.margin) + 'px',
width: self.contentWidth + 'px',
height: self.tileHeight + self.margin + 'px',
overflowX: 'hidden'
})
.appendTo(self.$timelinePlayer);
self.$timelines[i] = [
self.$timeline.clone(true)
.css({
width: self.frame + self.videoWidth + 'px',
marginLeft: -i * self.contentWidth + 'px'
}),
self.$timeline.clone(true)
.css({
marginLeft: -i * self.contentWidth + self.videoWidth - 1 + 'px'
})
];
self.$lines[i]
.append(self.$timelines[i][1])
.append(self.$timelines[i][0]);
}
function changeVolume(num) {
self.options.volume = Ox.limit(self.options.volume + num, 0, 1);
setVolume();
}
function formatPosition(position) {
position = Ox.isUndefined(position) ? self.options.position : position;
return Ox.formatDuration(position, self.options.showMilliseconds);
}
function getLines(scrollbarIsVisible) {
// Returns the number of lines
var lines;
if (scrollbarIsVisible) {
self.contentWidth -= Ox.UI.SCROLLBAR_SIZE;
}
lines = Math.ceil(
(self.frames - 1 + self.videoWidth) / self.contentWidth
);
return !scrollbarIsVisible && lines * (
self.tileHeight + self.margin
) + self.margin > self.contentHeight ? getLines(true) : lines;
}
function getPosition(e) {
return (
e.offsetX ? e.offsetX
: e.clientX - $(e.target).offset().left
) / self.fps;
}
function getPositionScrollTop() {
// Returns the target scrolltop if the player is not fully visible,
// otherwise returns null
var scrollTop = self.$timelinePlayer.scrollTop(),
videoTop = [
self.margin + Ox.min(self.videoLines) * (self.tileHeight + self.margin),
self.margin + Ox.max(self.videoLines) * (self.tileHeight + self.margin)
],
offset = self.contentHeight - self.tileHeight - self.margin;
return videoTop[0] < scrollTop + self.margin ? videoTop[0] - self.margin
: videoTop[1] > scrollTop + offset ? videoTop[1] - offset
: null;
}
function getSmallTimeline() {
var $timeline = Ox.SmallVideoTimeline({
duration: self.options.duration,
imageURL: self.options.smallTimelineURL,
mode: 'player',
paused: self.options.paused,
position: self.options.position,
showMilliseconds: self.options.showMilliseconds,
width: getSmallTimelineWidth()
})
.css({float: 'left'})
.css({background: '-moz-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(64, 64, 64, 0.5))'})
.css({background: '-o-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(64, 64, 64, 0.5))'})
.css({background: '-webkit-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(64, 64, 64, 0.5))'})
.bindEvent({
position: function(data) {
self.options.position = data.position;
setPosition();
that.triggerEvent('position', {
position: self.options.position
});
}
});
$timeline.children().css({marginLeft: '32px'});
$timeline.find('.OxInterface').css({marginLeft: '32px'});
return $timeline;
}
function getSmallTimelineWidth() {
return self.options.width - 32 - self.positionWidth;
}
function getSubtitle(position) {
var subtitle = '';
Ox.forEach(self.options.subtitles, function(v) {
if (v['in'] <= position && v.out > position) {
subtitle = v;
return false; // break
}
});
return subtitle;
}
function getVideoLine() {
self.videoLine = Math.floor(getVideoFrame() / self.contentWidth);
}
function getVideoLines() {
// Returns the lines the video is at, as an array of two line numbers.
// If the video fits in one line, they're both the same, otherwise, the
// line that has most of the video (and thus the player) comes first.
var videoFrame = getVideoFrame(),
videoLeft = videoFrame % self.contentWidth,
lines = [];
lines[0] = Math.floor(videoFrame / self.contentWidth);
lines[1] = lines[0] + (
videoLeft + self.videoWidth > self.contentWidth ? 1 : 0
)
if (videoLeft + Math.floor(self.videoWidth / 2) > self.contentWidth) {
lines.reverse();
}
return lines;
}
function getVideoFrame() {
return Math.floor(self.options.position * self.fps);
}
function mousedown(e) {
var $target = $(e.target),
isTimeline = $target.is('.OxTimelineInterface'),
isVideo = $target.is('.OxFrameInterface');
if (isTimeline) {
self.options.position = getPosition(e);
setPosition();
if (!self.triggered) {
that.triggerEvent('position', {
position: self.options.position
});
self.triggered = true;
setTimeout(function() {
self.triggered = false;
}, 250);
}
} else if (isVideo) {
togglePaused();
}
}
function mouseleave() {
self.$tooltip.hide();
}
function mousemove(e) {
var $target = $(e.target),
isTimeline = $target.is('.OxTimelineInterface'),
isVideo = $target.is('.OxFrameInterface'),
position, subtitle;
if (isTimeline || isVideo) {
position = isTimeline ? getPosition(e) : self.options.position;
subtitle = getSubtitle(position);
self.$tooltip.options({
title: (
subtitle
? '<span class=\'OxBright\'>' + Ox.highlight(
subtitle.text, self.options.find, 'OxHighlight'
).replace(/\n/g, '<br/>') + '</span><br/>'
: ''
) + Ox.formatDuration(position, 3)
})
.show(e.clientX, e.clientY);
} else {
self.$tooltip.hide();
}
}
function renderTimeline() {
var $timeline = $('<div>')
.css({
position: 'absolute',
width: self.frames + 'px',
height: self.tileHeight + self.margin + 'px',
overflow: 'hidden'
});
Ox.loop(self.tiles, function(i) {
$('<img>')
.attr({
src: self.options.getLargeTimelineURL(self.options.timeline, i)
})
.css({
position: 'absolute',
left: i * self.tileWidth + 'px',
top: self.margin / 2 + 'px'
})
.data({index: i})
.appendTo($timeline);
});
$('<div>')
.addClass('OxTimelineInterface')
.css({
position: 'absolute',
left: 0,
top: self.margin / 2 + 'px',
width: self.frames + 'px',
height: self.tileHeight + 'px'
})
.appendTo($timeline);
return $timeline;
}
function scroll() {
updateScrollButton();
if (!self.options.paused && self.options.followPlayer) {
self.scrollTimeout && clearTimeout(self.scrollTimeout);
self.scrollTimeout = setTimeout(function() {
scrollToPosition();
self.scrollTimeout = 0;
}, 2500);
}
}
function scrollToPosition() {
var positionScrollTop = getPositionScrollTop();
positionScrollTop && self.$timelinePlayer.stop().animate({
scrollTop: positionScrollTop
}, 250, function() {
self.$scrollButton.hide();
});
}
function setHeight() {
self.contentHeight = self.options.height - 32;
if (!self.options.paused && self.options.followPlayer) {
self.scrollTimeout && clearTimeout(self.scrollTimeout);
scrollToPosition();
}
}
function setPosition(fromVideo) {
var isPlaying = !self.options.paused,
max, min, videoLines, videoLines_;
self.options.position = Ox.limit(self.options.position, 0, self.options.duration);
self.frame = Math.floor(self.options.position * self.fps);
videoLines = getVideoLines();
// current and previous video lines
videoLines_ = Ox.flatten([self.videoLines, videoLines]);
min = Ox.min(videoLines_);
max = Ox.max(videoLines_);
Ox.loop(min, max + 1, function(i) {
self.$timelines[i][0].css({
width: self.frame + self.videoWidth + 'px'
});
});
if (videoLines[1] != self.videoLines[1]) {
// move frame to new line
self.$frameBox.detach().appendTo(self.$timelines[videoLines[1]][0]);
}
if (videoLines[0] != self.videoLines[0]) {
// move video to new line
isPlaying && self.$video.togglePaused();
self.$videoBox.detach().appendTo(self.$timelines[videoLines[0]][0]);
isPlaying && self.$video.togglePaused();
}
if (videoLines[0] != videoLines[1]) {
// if the player spans two lines, update the frame
// (but only once per second if the video is playing)
self.$frame.options({
position: self.paused
? self.options.position
: Math.floor(self.options.position)
});
}
if (
fromVideo && !self.scrollTimeout
&& videoLines[1] != self.videoLines[1]
&& videoLines[1] > videoLines[0]
) {
// if the video is moving, the user has not scrolled away
// and the video has reached a line break, then either
// follow the video or update the scroll button
self.videoLines = videoLines;
self.options.followPlayer ? scrollToPosition() : updateScrollButton();
} else {
self.videoLines = videoLines;
}
if (!fromVideo) {
self.$video.options({position: self.options.position});
self.$frame.options({position: self.options.position});
scrollToPosition();
}
self.$smallTimeline.options({position: self.options.position});
self.$position.html(formatPosition());
that.triggerEvent(fromVideo ? 'playing' : 'position', {
position: self.options.position
});
}
function setSubtitles() {
self.$timeline.find('.OxSubtitle').remove();
self.$subtitles = [];
self.options.subtitles.forEach(function(subtitle, i) {
var found = self.options.find
&& subtitle.text.toLowerCase().indexOf(self.options.find.toLowerCase()) > -1;
self.$subtitles[i] = $('<div>')
.addClass('OxSubtitle' + (found ? ' OxHighlight' : ''))
.css({
position: 'absolute',
left: (subtitle['in'] * self.fps) + 'px',
width: (((subtitle.out - subtitle['in']) * self.fps) - 2) + 'px'
})
.html(Ox.highlight(
subtitle.text, self.options.find, 'OxHighlight'
))
.appendTo(self.$timeline);
});
}
function setTimeline() {
self.$timelinePlayer.empty();
}
function setVolume() {
self.$video.options({volume: self.options.volume});
that.triggerEvent('volume', {
volume: self.options.volume
});
}
function setWidth() {
self.contentWidth = self.options.width - 2 * self.margin;
self.lines = getLines();
Ox.loop(self.lines, function(i) {
if (self.$lines[i]) {
self.$lines[i].css({
width: self.contentWidth + 'px'
});
self.$timelines[i][0].css({
marginLeft: -i * self.contentWidth + 'px'
});
self.$timelines[i][1].css({
marginLeft: -i * self.contentWidth + self.videoWidth - 1 + 'px'
});
} else {
addLine(i);
}
});
while (self.$lines.length > self.lines) {
self.$lines[self.$lines.length - 1].remove();
self.$lines.pop();
self.$timelines.pop();
}
Ox.last(self.$lines).css({
height: self.tileHeight + 1.5 * self.margin + 'px'
});
if (!self.options.paused && self.options.followPlayer) {
self.scrollTimeout && clearTimeout(self.scrollTimeout);
scrollToPosition();
}
self.$smallTimeline.options({width: getSmallTimelineWidth()})
}
function submitPositionInput() {
self.$positionInput.hide();
self.$position.html('').show();
self.options.position = Ox.parseDuration(self.$positionInput.value());
setPosition();
if (self.playOnSubmit) {
togglePaused();
self.playOnSubmit = false;
}
that.triggerEvent('position', {
position: self.options.position
});
}
function toggleMuted() {
self.options.muted = !self.options.muted;
self.$video.options({muted: self.options.muted});
self.$muteButton.options({
title: self.options.muted ? 'unmute' : 'mute',
tooltip: self.options.muted ? Ox._('Unmute') : Ox._('Mute')
});
that.triggerEvent('muted', {
muted: self.options.muted
});
}
function togglePaused(fromVideo) {
self.options.paused = !self.options.paused;
if (!self.options.paused && self.options.followPlayer) {
self.scrollTimeout && clearTimeout(self.scrollTimeout);
scrollToPosition();
}
!fromVideo && self.$video.options({
paused: self.options.paused
});
self.$playButton.options({
title: self.options.paused ? 'play' : 'pause'
});
}
function updateScrollButton() {
var scrollTop = self.$timelinePlayer.scrollTop(),
positionScrollTop = getPositionScrollTop();
if (positionScrollTop === null) {
self.$scrollButton.hide();
} else {
self.$scrollButton.options({
title: positionScrollTop < scrollTop ? 'arrowUp' : 'arrowDown'
}).show();
}
}
function updateTimeline() {
self.$timelinePlayer.find('img').each(function() {
var $this = $(this);
$this.attr({
src: self.options.getLargeTimelineURL(
self.options.timeline, $this.data('index')
)
});
});
}
/*@
togglePaused <f> toggle paused
() -> <o> toggle paused
@*/
that.togglePaused = function() {
togglePaused();
return that;
};
return that;
};